Переглянути джерело

Merge pull request #1346 from FreshRSS/dev

Merge 1.6.0-dev in master
Alexandre Alapetite 9 роки тому
батько
коміт
1d3e5bdee0
100 змінених файлів з 1995 додано та 1048 видалено
  1. 52 1
      CHANGELOG.md
  2. 55 25
      README.fr.md
  3. 58 28
      README.md
  4. 2 3
      app/Controllers/categoryController.php
  5. 1 1
      app/Controllers/configureController.php
  6. 165 153
      app/Controllers/feedController.php
  7. 241 138
      app/Controllers/importExportController.php
  8. 38 6
      app/Controllers/indexController.php
  9. 29 7
      app/Controllers/statsController.php
  10. 84 77
      app/Controllers/userController.php
  11. 14 0
      app/Exceptions/AlreadySubscribedException.php
  12. 14 0
      app/Exceptions/FeedNotAddedException.php
  13. 14 0
      app/Exceptions/ZipException.php
  14. 4 0
      app/Exceptions/ZipMissingException.php
  15. 1 1
      app/FreshRSS.php
  16. 14 7
      app/Models/CategoryDAO.php
  17. 1 0
      app/Models/ConfigurationSetter.php
  18. 16 4
      app/Models/Context.php
  19. 43 0
      app/Models/DatabaseDAOPGSQL.php
  20. 103 65
      app/Models/EntryDAO.php
  21. 31 0
      app/Models/EntryDAOPGSQL.php
  22. 14 6
      app/Models/EntryDAOSQLite.php
  23. 26 16
      app/Models/Factory.php
  24. 29 12
      app/Models/Feed.php
  25. 39 18
      app/Models/FeedDAO.php
  26. 2 2
      app/Models/FeedDAOSQLite.php
  27. 33 69
      app/Models/StatsDAO.php
  28. 67 0
      app/Models/StatsDAOPGSQL.php
  29. 4 55
      app/Models/StatsDAOSQLite.php
  30. 40 14
      app/Models/UserDAO.php
  31. 6 1
      app/SQL/install.sql.mysql.php
  32. 63 0
      app/SQL/install.sql.pgsql.php
  33. 20 16
      app/SQL/install.sql.sqlite.php
  34. 2 1
      app/actualize_script.php
  35. 4 4
      app/i18n/cz/admin.php
  36. 3 3
      app/i18n/cz/feedback.php
  37. 3 3
      app/i18n/cz/install.php
  38. 2 2
      app/i18n/cz/sub.php
  39. 4 4
      app/i18n/de/admin.php
  40. 3 3
      app/i18n/de/feedback.php
  41. 4 4
      app/i18n/de/install.php
  42. 1 1
      app/i18n/de/sub.php
  43. 10 10
      app/i18n/en/admin.php
  44. 3 3
      app/i18n/en/feedback.php
  45. 13 13
      app/i18n/en/install.php
  46. 2 2
      app/i18n/en/sub.php
  47. 14 14
      app/i18n/fr/admin.php
  48. 3 3
      app/i18n/fr/feedback.php
  49. 13 13
      app/i18n/fr/install.php
  50. 2 2
      app/i18n/fr/sub.php
  51. 4 4
      app/i18n/it/admin.php
  52. 3 3
      app/i18n/it/feedback.php
  53. 6 6
      app/i18n/it/install.php
  54. 2 2
      app/i18n/it/sub.php
  55. 4 4
      app/i18n/nl/admin.php
  56. 3 3
      app/i18n/nl/feedback.php
  57. 5 5
      app/i18n/nl/install.php
  58. 2 2
      app/i18n/nl/sub.php
  59. 4 4
      app/i18n/ru/admin.php
  60. 3 3
      app/i18n/ru/feedback.php
  61. 5 5
      app/i18n/ru/install.php
  62. 2 2
      app/i18n/ru/sub.php
  63. 4 4
      app/i18n/tr/admin.php
  64. 3 3
      app/i18n/tr/feedback.php
  65. 5 5
      app/i18n/tr/install.php
  66. 2 2
      app/i18n/tr/sub.php
  67. 58 138
      app/install.php
  68. 2 2
      app/layout/aside_feed.phtml
  69. 1 1
      app/layout/aside_subscription.phtml
  70. 3 0
      app/layout/layout.phtml
  71. 5 2
      app/layout/nav_menu.phtml
  72. 1 1
      app/views/auth/index.phtml
  73. 2 2
      app/views/configure/sharing.phtml
  74. 3 3
      app/views/feed/add.phtml
  75. 3 3
      app/views/helpers/feed/update.phtml
  76. 2 2
      app/views/helpers/index/normal/entry_bottom.phtml
  77. 2 2
      app/views/helpers/index/normal/entry_header.phtml
  78. 2 1
      app/views/helpers/pagination.phtml
  79. 1 1
      app/views/importExport/index.phtml
  80. 4 1
      app/views/index/global.phtml
  81. 1 1
      app/views/index/normal.phtml
  82. 6 6
      app/views/stats/index.phtml
  83. 4 4
      app/views/stats/repartition.phtml
  84. 2 2
      app/views/user/manage.phtml
  85. 3 0
      cli/.htaccess
  86. 58 0
      cli/README.md
  87. 49 0
      cli/_cli.php
  88. 23 0
      cli/actualize-user.php
  89. 48 0
      cli/create-user.php
  90. 32 0
      cli/delete-user.php
  91. 101 0
      cli/do-install.php
  92. 24 0
      cli/export-opml-for-user.php
  93. 30 0
      cli/export-zip-for-user.php
  94. 35 0
      cli/import-for-user.php
  95. 0 0
      cli/index.html
  96. 14 0
      cli/list-users.php
  97. 1 1
      constants.php
  98. 3 7
      data/.gitignore
  99. 3 1
      data/PubSubHubbub/feeds/.gitignore
  100. 3 0
      data/config.default.php

+ 52 - 1
CHANGELOG.md

@@ -1,5 +1,56 @@
 # Changelog
 # Changelog
 
 
+## 2016-10-30 FreshRSS 1.6.0
+
+* CLI
+	* New Command-Line Interface (CLI) [#1095](https://github.com/FreshRSS/FreshRSS/issues/1095)
+		* Install, add/delete users, actualize, import/export. See [CLI documentation](./cli/README.md).
+* API
+	* Support for editing feeds and categories from client applications [#1254](https://github.com/FreshRSS/FreshRSS/issues/1254)
+* Compatibility:
+	* Support for PostgreSQL [#416](https://github.com/FreshRSS/FreshRSS/issues/416)
+	* New client supporting FreshRSS on Linux: FeedReader 2.0+ [#1252](https://github.com/FreshRSS/FreshRSS/issues/1252)
+* Features
+	* Rework the “mark as read during scroll” option, enabled by default for new users [#1258](https://github.com/FreshRSS/FreshRSS/issues/1258), [#1309](https://github.com/FreshRSS/FreshRSS/pull/1309)
+		* Including a *keep unread* function [#1327](https://github.com/FreshRSS/FreshRSS/pull/1327)
+	* In a multi-user context, take better advantage of other users’ refreshes [#1280](https://github.com/FreshRSS/FreshRSS/pull/1280)
+	* Better control of number of entries per page or RSS feed [#1249](https://github.com/FreshRSS/FreshRSS/issues/1249)
+		* Since X hours: `https://freshrss.example/i/?a=rss&hours=3`
+		* Explicit number: `https://freshrss.example/i/?a=rss&nb=10`
+		* Limited by `min_posts_per_rss` and `max_posts_per_rss` in user config
+	* Support custom ports `localhost:3306` for database servers [#1241](https://github.com/FreshRSS/FreshRSS/issues/1241)
+	* Add date to exported files [#1240](https://github.com/FreshRSS/FreshRSS/issues/1240)
+	* Auto-refresh favicons once or twice a month [#1181](https://github.com/FreshRSS/FreshRSS/issues/1181), [#1298](https://github.com/FreshRSS/FreshRSS/issues/1298)
+		* Cron updates will also refresh favicons every 2 weeks [#1306](https://github.com/FreshRSS/FreshRSS/pull/1306)
+* Bug fixing
+	* Correction of bugs related to CSRF tokens introduced in version 1.5.0 [#1253](https://github.com/FreshRSS/FreshRSS/issues/1253), [44f22ab](https://github.com/FreshRSS/FreshRSS/pull/1261/commits/d9bf9b2c6f0b2cc9dec3b638841b7e3040dcf46f)
+	* Fix bug in Global view introduced in version 1.5.0 [#1269](https://github.com/FreshRSS/FreshRSS/pull/1269)
+	* Fix sharing bug [#1289](https://github.com/FreshRSS/FreshRSS/issues/1289)
+	* Fix bug in auto-loading more articles after marking an article as un-read [#1318](https://github.com/FreshRSS/FreshRSS/issues/1318)
+	* Fix bug during import of favourites [#1315](https://github.com/FreshRSS/FreshRSS/pull/1315), [#1312](https://github.com/FreshRSS/FreshRSS/issues/1312)
+	* Fix bug not respecting language option for new users [#1273](https://github.com/FreshRSS/FreshRSS/issues/1273)
+	* Bug in example of URL for FreshRSS RSS output with token [#1274](https://github.com/FreshRSS/FreshRSS/issues/1274)
+* Security
+	* Prevent `<a target="_blank">` attacks with `window.opener` [#1245](https://github.com/FreshRSS/FreshRSS/issues/1245)
+	* Updated gitignore rules to keep user directories during a `git clean -f -d` [#1307](https://github.com/FreshRSS/FreshRSS/pull/1307)
+* Extensions
+	* Allow extensions for default account in anonymous mode [#1288](https://github.com/FreshRSS/FreshRSS/pull/1288)
+	* Trigger a `freshrss:load-more` JavaScript event to help extensions [#1278](https://github.com/FreshRSS/FreshRSS/issues/1278)
+* SQL
+	* Slightly modified several SQL requests (MySQL, SQLite) to simplify support of PostgreSQL [#1195](https://github.com/FreshRSS/FreshRSS/pull/1195)
+	* Increase performances by removing a superfluous category request [#1316](https://github.com/FreshRSS/FreshRSS/pull/1316)
+* I18n
+	* Fix some messages during installation [#1339](https://github.com/FreshRSS/FreshRSS/pull/1339)
+* UI
+	* Fix CSS line-height bug with `<sup>` in dates (English, Russian, Turkish) [#1340](https://github.com/FreshRSS/FreshRSS/pull/1340)
+	* Disable *Mark all as read* before confirmation script is loaded [#1342](https://github.com/FreshRSS/FreshRSS/issues/1342)
+	* Download icon 💾 for podcasts [#1236](https://github.com/FreshRSS/FreshRSS/issues/1236)
+* SimplePie
+	* Fix auto-discovery of RSS feeds in Web pages served as `text/xml` [#1264](https://github.com/FreshRSS/FreshRSS/issues/1264)
+* Mics.
+	* Removed *resource-priorities* attributes (`defer`, `lazyload`), deprecated by W3C [#1222](https://github.com/FreshRSS/FreshRSS/pull/1222)
+
+
 ## 2016-08-29 FreshRSS 1.5.0
 ## 2016-08-29 FreshRSS 1.5.0
 
 
 * Compatibility
 * Compatibility
@@ -343,7 +394,7 @@
 	* Possibility to combine search filters, e.g. `date:2014-05 intitle:FreshRSS intitle:Open great reader #Internet`
 	* Possibility to combine search filters, e.g. `date:2014-05 intitle:FreshRSS intitle:Open great reader #Internet`
 * Change nav menu with more buttons instead of dropdown menus and add some filters
 * Change nav menu with more buttons instead of dropdown menus and add some filters
 * New system of import / export
 * New system of import / export
-	* Support OPML, Json (like Google Reader) and Zip archives
+	* Support OPML, Json (like Google Reader) and ZIP archives
 	* Can export and import articles (specific option for favorites)
 	* Can export and import articles (specific option for favorites)
 * Refactor "Origine" theme
 * Refactor "Origine" theme
 	* Some improvements
 	* Some improvements

+ 55 - 25
README.fr.md

@@ -7,6 +7,7 @@ Il se veut léger et facile à prendre en main tout en étant un outil puissant
 
 
 Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme.
 Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme.
 Il supporte [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) pour des notifications instantanées depuis les sites compatibles.
 Il supporte [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) pour des notifications instantanées depuis les sites compatibles.
+Il y a une API pour les clients (mobiles), ainsi qu’une [interface en ligne de commande](./cli/README.md).
 
 
 * Site officiel : http://freshrss.org
 * Site officiel : http://freshrss.org
 * Démo : http://demo.freshrss.org/
 * Démo : http://demo.freshrss.org/
@@ -17,11 +18,9 @@ Il supporte [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) pour des not
 # Téléchargement
 # Téléchargement
 Voir la [liste des versions](../../releases).
 Voir la [liste des versions](../../releases).
 
 
-## Note sur les branches
-**Ce logiciel est en développement permanent !** Veuillez vous assurer d'utiliser la branche qui vous correspond :
-
+## À propos des branches
 * Utilisez [la branche master](https://github.com/FreshRSS/FreshRSS/tree/master/) si vous visez la stabilité.
 * Utilisez [la branche master](https://github.com/FreshRSS/FreshRSS/tree/master/) si vous visez la stabilité.
-* Pour les développeurs et ceux qui veulent aider à tester les toutes dernières fonctionnalités, [la branche dev](https://github.com/FreshRSS/FreshRSS/tree/dev) vous ouvre les bras !
+* Pour ceux qui veulent bien aider à tester ou déveloper les dernières fonctionnalités, [la branche dev](https://github.com/FreshRSS/FreshRSS/tree/dev) vous ouvre les bras !
 
 
 # Avertissements
 # Avertissements
 Cette application a été développée pour s’adapter principalement à des besoins personnels, et aucune garantie n'est fournie.
 Cette application a été développée pour s’adapter principalement à des besoins personnels, et aucune garantie n'est fournie.
@@ -33,9 +32,9 @@ Nous sommes une communauté amicale.
 	* Fonctionne même sur un Raspberry Pi 1 avec des temps de réponse < 1s (testé sur 150 flux, 22k articles)
 	* Fonctionne même sur un Raspberry Pi 1 avec des temps de réponse < 1s (testé sur 150 flux, 22k articles)
 * Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres)
 * Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres)
 * PHP 5.3.3+ (PHP 5.4+ recommandé, et PHP 5.5+ pour les performances, et PHP 7+ pour d’encore meilleures performances)
 * PHP 5.3.3+ (PHP 5.4+ recommandé, et PHP 5.5+ pour les performances, et PHP 7+ pour d’encore meilleures performances)
-	* Requis : [DOM](http://php.net/dom), [XML](http://php.net/xml), [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl)
-	* Recommandés : [JSON](http://php.net/json), [GMP](http://php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](http://php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](http://php.net/mbstring) et/ou [iconv](http://php.net/iconv) (pour conversion d’encodages), [Zip](http://php.net/zip) (pour import/export), [zlib](http://php.net/zlib) (pour les flux compressés)
-* MySQL 5.5.3+ (recommandé) ou SQLite 3.7.4+
+	* Requis : [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), et [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite) ou [PDO_PGSQL](http://php.net/pdo-pgsql)
+	* Recommandés : [JSON](http://php.net/json), [GMP](http://php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](http://php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](http://php.net/mbstring) et/ou [iconv](http://php.net/iconv) (pour conversion d’encodages), [ZIP](http://php.net/zip) (pour import/export), [zlib](http://php.net/zlib) (pour les flux compressés)
+* MySQL 5.5.3+ (recommandé), ou SQLite 3.7.4+, ou PostgreSQL (experimental)
 * Un navigateur Web récent tel Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
 * Un navigateur Web récent tel Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
 	* Fonctionne aussi sur mobile
 	* Fonctionne aussi sur mobile
 
 
@@ -46,48 +45,59 @@ Nous sommes une communauté amicale.
 2. Placez l’application sur votre serveur (la partie à exposer au Web est le répertoire `./p/`)
 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/`
 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
 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.
+	* ou utilisez [l’interface en ligne de commande](./cli/README.md)
+5. Tout devrait fonctionner :) En cas de problème, n’hésitez pas à [nous contacter](https://github.com/FreshRSS/FreshRSS/issues).
 6. Des paramètres de configuration avancée peuvent être accédés depuis [config.php](./data/config.default.php).
 6. Des paramètres de configuration avancée peuvent être accédés depuis [config.php](./data/config.default.php).
 
 
 ## Installation automatisée
 ## Installation automatisée
-[![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
+* [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
+* [YunoHost](https://github.com/YunoHost-Apps/freshrss_ynh)
 
 
 ## Exemple d’installation complète sur Linux Debian/Ubuntu
 ## Exemple d’installation complète sur Linux Debian/Ubuntu
 ```sh
 ```sh
 # Si vous utilisez le serveur Web Apache (sinon il faut un autre serveur Web)
 # Si vous utilisez le serveur Web Apache (sinon il faut un autre serveur Web)
 sudo apt-get install apache2
 sudo apt-get install apache2
-sudo a2enmod headers expires rewrite ssl
-# (optionnel) Si vous voulez un serveur de base de données MySQL
-sudo apt-get install mysql-server mysql-client php5-mysql
-# Composants principaux (pour Ubuntu <= 15.10, Debian <= 8 Jessie)
+sudo a2enmod headers expires rewrite ssl	#Modules Apache
+
+# Pour Ubuntu <= 15.10, Debian <= 8 Jessie
 sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
 sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
-# Composants principaux (pour Ubuntu >= 16.04, Debian >= 9 Stretch)
-sudo apt install php libapache2-mod-php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
-# Redémarrage du serveur Web
+sudo apt-get install libapache2-mod-php5	#Pour Apache
+sudo apt-get install mysql-server mysql-client php5-mysql	#Base de données MySQL optionnelle
+sudo apt-get install postgresql php5-pgsql	#Base de données PostgreSQL optionnelle
+
+# Pour Ubuntu >= 16.04, Debian >= 9 Stretch
+sudo apt install php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
+sudo apt install libapache2-mod-php	#Pour Apache
+sudo apt install mysql-server mysql-client php-mysql	#Base de données MySQL optionnelle
+sudo apt install postgresql php-pgsql	#Base de données PostgreSQL optionnelle
+
+## Redémarrage du serveur Web
 sudo service apache2 restart
 sudo service apache2 restart
 
 
 # Pour FreshRSS lui-même (git est optionnel si vous déployez manuellement les fichiers d’installation)
 # Pour FreshRSS lui-même (git est optionnel si vous déployez manuellement les fichiers d’installation)
 cd /usr/share/
 cd /usr/share/
 sudo apt-get install git
 sudo apt-get install git
 sudo git clone https://github.com/FreshRSS/FreshRSS.git
 sudo git clone https://github.com/FreshRSS/FreshRSS.git
-# Mettre les droits d’accès pour le serveur Web
 cd FreshRSS
 cd FreshRSS
-sudo chown -R :www-data .
-sudo chmod -R g+w ./data/
+
+# Si vous souhaitez utiliser la branche développement de FreshRSS
+sudo git checkout -b dev origin/dev
+
+# Mettre les droits d’accès pour le serveur Web
+sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
 # Publier FreshRSS dans votre répertoire HTML public
 # Publier FreshRSS dans votre répertoire HTML public
 sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
 sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
-# Naviguez vers http://example.net/FreshRSS pour terminer l’installation.
+# Naviguez vers http://example.net/FreshRSS pour terminer l’installation
 # (Si vous le faite depuis localhost, vous pourrez avoir à ajuster le réglage de votre adresse publique)
 # (Si vous le faite depuis localhost, vous pourrez avoir à ajuster le réglage de votre adresse publique)
+# ou utilisez l’interface en ligne de commande
 
 
 # Mettre à jour FreshRSS vers une nouvelle version
 # Mettre à jour FreshRSS vers une nouvelle version
 cd /usr/share/FreshRSS
 cd /usr/share/FreshRSS
-sudo git reset --hard
 sudo git pull
 sudo git pull
-sudo chown -R :www-data .
-sudo chmod -R g+w ./data/
+sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
 ```
 ```
 
 
-# Contrôle d’accès
+## Contrôle d’accès
 Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix :
 Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix :
 * En utilisant l’identification par formulaire (requiert JavaScript, et PHP 5.3.7+ recommandé – fonctionne avec certaines versions de PHP 5.3.3+)
 * En utilisant l’identification par formulaire (requiert JavaScript, et PHP 5.3.7+ recommandé – fonctionne avec certaines versions de PHP 5.3.3+)
 * En utilisant un contrôle d’accès HTTP défini par votre serveur Web
 * En utilisant un contrôle d’accès HTTP défini par votre serveur Web
@@ -101,18 +111,28 @@ C’est une bonne idée d’utiliser le même utilisateur que votre serveur Web
 Par exemple, pour exécuter le script toutes les heures :
 Par exemple, pour exécuter le script toutes les heures :
 
 
 ```
 ```
-7 * * * * php /votre-chemin/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
+8 * * * * php /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
 ```
 ```
 
 
+### Exemple pour Debian / Ubuntu
+Créer `/etc/cron.d/FreshRSS` avec :
+
+```
+7,37 * * * * www-data php -f /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
+```
+
+
 # Conseils
 # 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/`.
 * 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/`.
 	* 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.
 * 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`.
 * En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/log/*.log`.
 
 
+
 # Sauvegarde
 # Sauvegarde
 * Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/*_user.php`
 * Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/*_user.php`
 * Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML
 * Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML
+	* soit depuis l’interface Web, soit [en ligne de commande](./cli/README.md)
 * Pour sauvegarder les articles eux-mêmes, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL :
 * Pour sauvegarder les articles eux-mêmes, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL :
 
 
 ```bash
 ```bash
@@ -138,3 +158,13 @@ mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
 ## Si les fonctions natives ne sont pas disponibles
 ## Si les fonctions natives ne sont pas disponibles
 * [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198)
 * [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198)
 * [password_compat](https://github.com/ircmaxell/password_compat)
 * [password_compat](https://github.com/ircmaxell/password_compat)
+
+
+# Clients compatibles
+Tout client supportant une API de type Google Reader. Sélection :
+
+* Android
+	* [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) avec [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Propriétaire)
+	* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Libre, F-Droid)
+* Linux
+	* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre)

+ 58 - 28
README.md

@@ -7,6 +7,7 @@ It is at the same time lightweight, easy to work with, powerful and customizable
 
 
 It is a multi-user application with an anonymous reading mode.
 It is a multi-user application with an anonymous reading mode.
 It supports [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) for instant notifications from compatible Web sites.
 It supports [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) for instant notifications from compatible Web sites.
+There is an API for (mobile) clients, and a [Command-Line Interface](./cli/README.md).
 
 
 * Official website: http://freshrss.org
 * Official website: http://freshrss.org
 * Demo: http://demo.freshrss.org/
 * Demo: http://demo.freshrss.org/
@@ -17,15 +18,13 @@ It supports [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) for instant
 # Releases
 # Releases
 See the [list of releases](../../releases).
 See the [list of releases](../../releases).
 
 
-## Note on branches
-**This application is under continuous development!** Please use the branch that suits your needs:
-
+## About branches
 * Use [the master branch](https://github.com/FreshRSS/FreshRSS/tree/master/) if you need a stable version.
 * Use [the master branch](https://github.com/FreshRSS/FreshRSS/tree/master/) if you need a stable version.
-* For developers and tech savvy persons willing to help testing the latest features, [the dev branch](https://github.com/FreshRSS/FreshRSS/tree/dev) is waiting for you!
+* For those willing to help testing or developing the latest features, [the dev branch](https://github.com/FreshRSS/FreshRSS/tree/dev) is waiting for you!
 
 
 # Disclaimer
 # Disclaimer
 This application was developed to fulfil personal needs primarily, and comes with absolutely no warranty.
 This application was developed to fulfil personal needs primarily, and comes with absolutely no warranty.
-Feature requests, bug reports, and other contributions are welcome. The best way is to [open issues on GitHub](https://github.com/FreshRSS/FreshRSS/issues).
+Feature requests, bug reports, and other contributions are welcome. The best way is to [open an issue on GitHub](https://github.com/FreshRSS/FreshRSS/issues).
 We are a friendly community.
 We are a friendly community.
 
 
 # Requirements
 # Requirements
@@ -33,9 +32,9 @@ We are a friendly community.
 	* It even works on Raspberry Pi 1 with response time under a second (tested with 150 feeds, 22k articles)
 	* It even works on Raspberry Pi 1 with response time under a second (tested with 150 feeds, 22k articles)
 * A web server: Apache2 (recommended), nginx, lighttpd (not tested on others)
 * A web server: Apache2 (recommended), nginx, lighttpd (not tested on others)
 * PHP 5.3.3+ (PHP 5.4+ recommended, and PHP 5.5+ for performance, and PHP 7 for even higher performance)
 * PHP 5.3.3+ (PHP 5.4+ recommended, and PHP 5.5+ for performance, and PHP 7 for even higher performance)
-	* Required extensions: [DOM](http://php.net/dom), [XML](http://php.net/xml), [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl)
-	* Recommended extensions: [JSON](http://php.net/json), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names), [mbstring](http://php.net/mbstring) and/or [iconv](http://php.net/iconv) (for charset conversion), [Zip](http://php.net/zip) (for import/export), [zlib](http://php.net/zlib) (for compressed feeds)
-* MySQL 5.5.3+ (recommended) or SQLite 3.7.4+
+	* Required extensions: [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), and [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite) or [PDO_PGSQL](http://php.net/pdo-pgsql)
+	* Recommended extensions: [JSON](http://php.net/json), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names), [mbstring](http://php.net/mbstring) and/or [iconv](http://php.net/iconv) (for charset conversion), [ZIP](http://php.net/zip) (for import/export), [zlib](http://php.net/zlib) (for compressed feeds)
+* MySQL 5.5.3+ (recommended), or SQLite 3.7.4+, or PostgreSQL (experimental)
 * A recent browser like Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
 * A recent browser like Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
 	* Works on mobile
 	* Works on mobile
 
 
@@ -46,23 +45,32 @@ We are a friendly community.
 2. Dump the application on your server (expose only the `./p/` folder)
 2. Dump the application on your server (expose only the `./p/` folder)
 3. Add write access on `./data/` folder to the webserver user
 3. Add write access on `./data/` folder to the webserver user
 4. Access FreshRSS with your browser and follow the installation process
 4. Access FreshRSS with your browser and follow the installation process
-5. Everything should be working :) If you encounter any problem, feel free to contact me.
+	* or use the [Command-Line Interface](./cli/README.md)
+5. Everything should be working :) If you encounter any problem, feel free [contact us](https://github.com/FreshRSS/FreshRSS/issues).
 6. Advanced configuration settings can be seen in [config.php](./data/config.default.php).
 6. Advanced configuration settings can be seen in [config.php](./data/config.default.php).
 
 
 ## Automated install
 ## Automated install
-[![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
+* [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
+* [YunoHost](https://github.com/YunoHost-Apps/freshrss_ynh)
 
 
 ## Example of full installation on Linux Debian/Ubuntu
 ## Example of full installation on Linux Debian/Ubuntu
 ```sh
 ```sh
 # If you use an Apache Web server (otherwise you need another Web server)
 # If you use an Apache Web server (otherwise you need another Web server)
 sudo apt-get install apache2
 sudo apt-get install apache2
-sudo a2enmod headers expires rewrite ssl
-# (Optional) If you want a MySQL database server
-sudo apt-get install mysql-server mysql-client php5-mysql
-# Main components (for Ubuntu <= 15.10, Debian <= 8 Jessie)
+sudo a2enmod headers expires rewrite ssl	#Apache modules
+
+# For Ubuntu <= 15.10, Debian <= 8 Jessie
 sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
 sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
-# Main components (for Ubuntu >= 16.04, Debian >= 9 Stretch)
-sudo apt install php libapache2-mod-php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
+sudo apt-get install libapache2-mod-php5	#For Apache
+sudo apt-get install mysql-server mysql-client php5-mysql	#Optional MySQL database
+sudo apt-get install postgresql php5-pgsql	#Optional PostgreSQL database
+
+# For Ubuntu >= 16.04, Debian >= 9 Stretch
+sudo apt install php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
+sudo apt install libapache2-mod-php	#For Apache
+sudo apt install mysql-server mysql-client php-mysql	#Optional MySQL database
+sudo apt install postgresql php-pgsql	#Optional PostgreSQL database
+
 # Restart Web server
 # Restart Web server
 sudo service apache2 restart
 sudo service apache2 restart
 
 
@@ -70,49 +78,61 @@ sudo service apache2 restart
 cd /usr/share/
 cd /usr/share/
 sudo apt-get install git
 sudo apt-get install git
 sudo git clone https://github.com/FreshRSS/FreshRSS.git
 sudo git clone https://github.com/FreshRSS/FreshRSS.git
-# Set the rights so that your Web browser can access the files
 cd FreshRSS
 cd FreshRSS
-sudo chown -R :www-data .
-sudo chmod -R g+w ./data/
+
+# If you want to use the development version of FreshRSS
+sudo git checkout -b dev origin/dev
+
+# Set the rights so that your Web server can access the files
+sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
 # Publish FreshRSS in your public HTML directory
 # Publish FreshRSS in your public HTML directory
 sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
 sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
-# Navigate to http://example.net/FreshRSS to complete the installation.
+# Navigate to http://example.net/FreshRSS to complete the installation
 # (If you do it from localhost, you may have to adjust the setting of your public address later)
 # (If you do it from localhost, you may have to adjust the setting of your public address later)
+# or use the Command-Line Interface
 
 
 # Update to a newer version of FreshRSS
 # Update to a newer version of FreshRSS
 cd /usr/share/FreshRSS
 cd /usr/share/FreshRSS
-sudo git reset --hard
 sudo git pull
 sudo git pull
-sudo chown -R :www-data .
-sudo chmod -R g+w ./data/
+sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
 ```
 ```
 
 
-# Access control
+## Access control
 It is needed for the multi-user mode to limit access to FreshRSS. You can:
 It is needed for the multi-user mode to limit access to FreshRSS. You can:
 * use form authentication (need JavaScript and PHP 5.3.7+, works with some PHP 5.3.3+)
 * use form authentication (need JavaScript and PHP 5.3.7+, works with some PHP 5.3.3+)
 * use HTTP authentication supported by your web server
 * use HTTP authentication supported by your web server
 	* See [Apache documentation](http://httpd.apache.org/docs/trunk/howto/auth.html)
 	* See [Apache documentation](http://httpd.apache.org/docs/trunk/howto/auth.html)
 		* In that case, create a `./p/i/.htaccess` file with a matching `.htpasswd` file.
 		* In that case, create a `./p/i/.htaccess` file with a matching `.htpasswd` file.
 
 
-# Automatic feed update
+## Automatic feed update
 * You can add a Cron job to launch the update script.
 * You can add a Cron job to launch the update script.
 Check the Cron documentation related to your distribution ([Debian/Ubuntu](https://help.ubuntu.com/community/CronHowto), [Red Hat/Fedora](https://fedoraproject.org/wiki/Administration_Guide_Draft/Cron), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](https://wiki.gentoo.org/wiki/Cron), [Arch Linux](https://wiki.archlinux.org/index.php/Cron)…).
 Check the Cron documentation related to your distribution ([Debian/Ubuntu](https://help.ubuntu.com/community/CronHowto), [Red Hat/Fedora](https://fedoraproject.org/wiki/Administration_Guide_Draft/Cron), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](https://wiki.gentoo.org/wiki/Cron), [Arch Linux](https://wiki.archlinux.org/index.php/Cron)…).
-Its a good idea to use the Web server user.
-For example, if you want to run the script every hour:
+It is a good idea to use the Web server user.
+For instance, if you want to run the script every hour:
 
 
 ```
 ```
-7 * * * * php /your-path/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
+9 * * * * php /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
 ```
 ```
 
 
+### Example on Debian / Ubuntu
+Create `/etc/cron.d/FreshRSS` with:
+
+```
+6,36 * * * * www-data php -f /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
+```
+
+
 # Advices
 # Advices
 * For a better security, expose only the `./p/` folder on the web.
 * For a better security, expose only the `./p/` folder on the web.
 	* Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it.
 	* Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it.
 * The `./constants.php` file defines access to application folder. If you want to customize your installation, every thing happens here.
 * The `./constants.php` file defines access to application folder. If you want to customize your installation, every thing happens here.
 * If you encounter any problem, logs are accessible from the interface or manually in `./data/log/*.log` files.
 * If you encounter any problem, logs are accessible from the interface or manually in `./data/log/*.log` files.
 
 
+
 # Backup
 # Backup
 * You need to keep `./data/config.php`, and `./data/*_user.php` files
 * You need to keep `./data/config.php`, and `./data/*_user.php` files
 * You can export your feed list in OPML format from FreshRSS
 * You can export your feed list in OPML format from FreshRSS
+	* either from the Web interface, or from the [Command-Line Interface](./cli/README.md)
 * To save articles, you can use [phpMyAdmin](http://www.phpmyadmin.net) or MySQL tools:
 * To save articles, you can use [phpMyAdmin](http://www.phpmyadmin.net) or MySQL tools:
 
 
 ```bash
 ```bash
@@ -138,3 +158,13 @@ mysqldump -u user -p --databases freshrss > freshrss.sql
 ## If native functions are not available
 ## If native functions are not available
 * [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198)
 * [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198)
 * [password_compat](https://github.com/ircmaxell/password_compat)
 * [password_compat](https://github.com/ircmaxell/password_compat)
+
+
+# Compatible clients
+Any client supporting a Google Reader-like API. Selection:
+
+* Android
+	* [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) with [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Closed source)
+	* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Open source, F-Droid)
+* Linux
+	* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source)

+ 2 - 3
app/Controllers/categoryController.php

@@ -117,7 +117,6 @@ class FreshRSS_category_Controller extends Minz_ActionController {
 	public function deleteAction() {
 	public function deleteAction() {
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$catDAO = new FreshRSS_CategoryDAO();
 		$catDAO = new FreshRSS_CategoryDAO();
-		$default_category = $catDAO->getDefault();
 		$url_redirect = array('c' => 'subscription', 'a' => 'index');
 		$url_redirect = array('c' => 'subscription', 'a' => 'index');
 
 
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
@@ -128,11 +127,11 @@ class FreshRSS_category_Controller extends Minz_ActionController {
 				Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect);
 				Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect);
 			}
 			}
 
 
-			if ($id === $default_category->id()) {
+			if ($id === FreshRSS_CategoryDAO::defaultCategoryId) {
 				Minz_Request::bad(_t('feedback.sub.category.not_delete_default'), $url_redirect);
 				Minz_Request::bad(_t('feedback.sub.category.not_delete_default'), $url_redirect);
 			}
 			}
 
 
-			if ($feedDAO->changeCategory($id, $default_category->id()) === false) {
+			if ($feedDAO->changeCategory($id, FreshRSS_CategoryDAO::defaultCategoryId) === false) {
 				Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
 				Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
 			}
 			}
 
 

+ 1 - 1
app/Controllers/configureController.php

@@ -139,7 +139,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 	 */
 	 */
 	public function sharingAction() {
 	public function sharingAction() {
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
-			$params = Minz_Request::fetchGET();
+			$params = Minz_Request::fetchPOST();
 			FreshRSS_Context::$user_conf->sharing = $params['share'];
 			FreshRSS_Context::$user_conf->sharing = $params['share'];
 			FreshRSS_Context::$user_conf->save();
 			FreshRSS_Context::$user_conf->save();
 			invalidateHttpCache();
 			invalidateHttpCache();

+ 165 - 153
app/Controllers/feedController.php

@@ -26,6 +26,62 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 	}
 	}
 
 
+	public static function addFeed($url, $title = '', $cat_id = 0, $new_cat_name = '', $http_auth = '') {
+		@set_time_limit(300);
+
+		$catDAO = new FreshRSS_CategoryDAO();
+
+		$cat = null;
+		if ($cat_id > 0) {
+			$cat = $catDAO->searchById($cat_id);
+		}
+		if ($cat == null && $new_cat_name != '') {
+			$cat = $catDAO->addCategory(array('name' => $new_cat_name));
+		}
+		if ($cat == null) {
+			$catDAO->checkDefault();
+		}
+		$cat_id = $cat == null ? FreshRSS_CategoryDAO::defaultCategoryId : $cat->id();
+
+		$feed = new FreshRSS_Feed($url);	//Throws FreshRSS_BadUrl_Exception
+		$feed->_httpAuth($http_auth);
+		$feed->load(true);	//Throws FreshRSS_Feed_Exception, Minz_FileNotExistException
+		$feed->_category($cat_id);
+
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		if ($feedDAO->searchByUrl($feed->url())) {
+			throw new FreshRSS_AlreadySubscribed_Exception($url, $feed->name());
+		}
+
+		// Call the extension hook
+		$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
+		if ($feed === null) {
+			throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
+		}
+
+		$values = array(
+			'url' => $feed->url(),
+			'category' => $feed->category(),
+			'name' => $title != '' ? $title : $feed->name(),
+			'website' => $feed->website(),
+			'description' => $feed->description(),
+			'lastUpdate' => time(),
+			'httpAuth' => $feed->httpAuth(),
+		);
+
+		$id = $feedDAO->addFeed($values);
+		if (!$id) {
+			// There was an error in database... we cannot say what here.
+			throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
+		}
+		$feed->_id($id);
+
+		// Ok, feed has been added in database. Now we have to refresh entries.
+		self::actualizeFeed($id, $url, false, null, true);
+
+		return $feed;
+	}
+
 	/**
 	/**
 	 * This action subscribes to a feed.
 	 * This action subscribes to a feed.
 	 *
 	 *
@@ -59,7 +115,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
-		$this->catDAO = new FreshRSS_CategoryDAO();
 		$url_redirect = array(
 		$url_redirect = array(
 			'c' => 'subscription',
 			'c' => 'subscription',
 			'a' => 'index',
 			'a' => 'index',
@@ -74,26 +129,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 
 
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
-			@set_time_limit(300);
-
 			$cat = Minz_Request::param('category');
 			$cat = Minz_Request::param('category');
+			$new_cat_name = '';
 			if ($cat === 'nc') {
 			if ($cat === 'nc') {
 				// User want to create a new category, new_category parameter
 				// User want to create a new category, new_category parameter
 				// must exist
 				// must exist
 				$new_cat = Minz_Request::param('new_category');
 				$new_cat = Minz_Request::param('new_category');
-				if (empty($new_cat['name'])) {
-					$cat = false;
-				} else {
-					$cat = $this->catDAO->addCategory($new_cat);
-				}
-			}
-
-			if ($cat === false) {
-				// If category was not given or if creating new category failed,
-				// get the default category
-				$this->catDAO->checkDefault();
-				$def_cat = $this->catDAO->getDefault();
-				$cat = $def_cat->id();
+				$new_cat_name = isset($new_cat['name']) ? $new_cat['name'] : '';
 			}
 			}
 
 
 			// HTTP information are useful if feed is protected behind a
 			// HTTP information are useful if feed is protected behind a
@@ -105,103 +147,24 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				$http_auth = $user . ':' . $pass;
 				$http_auth = $user . ':' . $pass;
 			}
 			}
 
 
-			$transaction_started = false;
 			try {
 			try {
-				$feed = new FreshRSS_Feed($url);
+				$feed = self::addFeed($url, '', $cat, $new_cat_name, $http_auth);
 			} catch (FreshRSS_BadUrl_Exception $e) {
 			} catch (FreshRSS_BadUrl_Exception $e) {
 				// Given url was not a valid url!
 				// Given url was not a valid url!
 				Minz_Log::warning($e->getMessage());
 				Minz_Log::warning($e->getMessage());
 				Minz_Request::bad(_t('feedback.sub.feed.invalid_url', $url), $url_redirect);
 				Minz_Request::bad(_t('feedback.sub.feed.invalid_url', $url), $url_redirect);
-			}
-
-			$feed->_httpAuth($http_auth);
-
-			try {
-				$feed->load(true);
 			} catch (FreshRSS_Feed_Exception $e) {
 			} catch (FreshRSS_Feed_Exception $e) {
 				// Something went bad (timeout, server not found, etc.)
 				// Something went bad (timeout, server not found, etc.)
 				Minz_Log::warning($e->getMessage());
 				Minz_Log::warning($e->getMessage());
-				Minz_Request::bad(
-					_t('feedback.sub.feed.internal_problem', _url('index', 'logs')),
-					$url_redirect
-				);
+				Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect);
 			} catch (Minz_FileNotExistException $e) {
 			} catch (Minz_FileNotExistException $e) {
 				// Cache directory doesn't exist!
 				// Cache directory doesn't exist!
 				Minz_Log::error($e->getMessage());
 				Minz_Log::error($e->getMessage());
-				Minz_Request::bad(
-					_t('feedback.sub.feed.internal_problem', _url('index', 'logs')),
-					$url_redirect
-				);
-			}
-
-			if ($feedDAO->searchByUrl($feed->url())) {
-				Minz_Request::bad(
-					_t('feedback.sub.feed.already_subscribed', $feed->name()),
-					$url_redirect
-				);
-			}
-
-			$feed->_category($cat);
-
-			// Call the extension hook
-			$name = $feed->name();
-			$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
-			if ($feed === null) {
-				Minz_Request::bad(_t('feedback.sub.feed.not_added', $name), $url_redirect);
-			}
-
-			$values = array(
-				'url' => $feed->url(),
-				'category' => $feed->category(),
-				'name' => $feed->name(),
-				'website' => $feed->website(),
-				'description' => $feed->description(),
-				'lastUpdate' => time(),
-				'httpAuth' => $feed->httpAuth(),
-			);
-
-			$id = $feedDAO->addFeed($values);
-			if (!$id) {
-				// There was an error in database... we cannot say what here.
-				Minz_Request::bad(_t('feedback.sub.feed.not_added', $feed->name()), $url_redirect);
-			}
-
-			// Ok, feed has been added in database. Now we have to refresh entries.
-			$feed->_id($id);
-			$feed->faviconPrepare();
-			//$feed->pubSubHubbubPrepare();	//TODO: prepare PubSubHubbub already when adding the feed
-
-			$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
-
-			$entryDAO = FreshRSS_Factory::createEntryDao();
-			// We want chronological order and SimplePie uses reverse order.
-			$entries = array_reverse($feed->entries());
-
-			// Calculate date of oldest entries we accept in DB.
-			$nb_month_old = FreshRSS_Context::$user_conf->old_entries;
-			$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
-
-			// Use a shared statement and a transaction to improve a LOT the
-			// performances.
-			$feedDAO->beginTransaction();
-			foreach ($entries as $entry) {
-				// Entries are added without any verification.
-				$entry->_feed($feed->id());
-				$entry->_id(min(time(), $entry->date(true)) . uSecString());
-				$entry->_isRead($is_read);
-
-				$entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
-				if ($entry === null) {
-					// An extension has returned a null value, there is nothing to insert.
-					continue;
-				}
-
-				$values = $entry->toArray();
-				$entryDAO->addEntry($values);
-			}
-			$feedDAO->updateLastUpdate($feed->id());
-			if ($feedDAO->inTransaction()) {
-				$feedDAO->commit();
+				Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect);
+			} catch (FreshRSS_AlreadySubscribed_Exception $e) {
+				Minz_Request::bad(_t('feedback.sub.feed.already_subscribed', $e->feedName()), $url_redirect);
+			} catch (FreshRSS_FeedNotAdded_Exception $e) {
+				Minz_Request::bad(_t('feedback.sub.feed.not_added', $e->feedName()), $url_redirect);
 			}
 			}
 
 
 			// Entries are in DB, we redirect to feed configuration page.
 			// Entries are in DB, we redirect to feed configuration page.
@@ -211,6 +174,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			// GET request: we must ask confirmation to user before adding feed.
 			// GET request: we must ask confirmation to user before adding feed.
 			Minz_View::prependTitle(_t('sub.feed.title_add') . ' · ');
 			Minz_View::prependTitle(_t('sub.feed.title_add') . ' · ');
 
 
+			$this->catDAO = new FreshRSS_CategoryDAO();
 			$this->view->categories = $this->catDAO->listCategories(false);
 			$this->view->categories = $this->catDAO->listCategories(false);
 			$this->view->feed = new FreshRSS_Feed($url);
 			$this->view->feed = new FreshRSS_Feed($url);
 			try {
 			try {
@@ -261,38 +225,23 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 	}
 	}
 
 
-	/**
-	 * This action actualizes entries from one or several feeds.
-	 *
-	 * Parameters are:
-	 *   - id (default: false): Feed ID
-	 *   - url (default: false): Feed URL
-	 *   - force (default: false)
-	 * If id and url are not specified, all the feeds are actualized. But if force is
-	 * false, process stops at 10 feeds to avoid time execution problem.
-	 */
-	public function actualizeAction($simplePiePush = null) {
+	public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false) {
 		@set_time_limit(300);
 		@set_time_limit(300);
 
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 
 
-		Minz_Session::_param('actualize_feeds', false);
-		$id = Minz_Request::param('id');
-		$url = Minz_Request::param('url');
-		$force = Minz_Request::param('force');
-
 		// Create a list of feeds to actualize.
 		// Create a list of feeds to actualize.
-		// If id is set and valid, corresponding feed is added to the list but
+		// If feed_id is set and valid, corresponding feed is added to the list but
 		// alone in order to automatize further process.
 		// alone in order to automatize further process.
 		$feeds = array();
 		$feeds = array();
-		if ($id || $url) {
-			$feed = $id ? $feedDAO->searchById($id) : $feedDAO->searchByUrl($url);
+		if ($feed_id > 0 || $feed_url) {
+			$feed = $feed_id > 0 ? $feedDAO->searchById($feed_id) : $feedDAO->searchByUrl($feed_url);
 			if ($feed) {
 			if ($feed) {
 				$feeds[] = $feed;
 				$feeds[] = $feed;
 			}
 			}
 		} else {
 		} else {
-			$feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
+			$feeds = $feedDAO->listFeedsOrderUpdate(-1);
 		}
 		}
 
 
 		// Calculate date of oldest entries we accept in DB.
 		// Calculate date of oldest entries we accept in DB.
@@ -309,13 +258,29 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$url = $feed->url();	//For detection of HTTP 301
 			$url = $feed->url();	//For detection of HTTP 301
 
 
 			$pubSubHubbubEnabled = $pubsubhubbubEnabledGeneral && $feed->pubSubHubbubEnabled();
 			$pubSubHubbubEnabled = $pubsubhubbubEnabledGeneral && $feed->pubSubHubbubEnabled();
-			if ((!$simplePiePush) && (!$id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) {
+			if ((!$simplePiePush) && (!$feed_id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) {
 				//$text = 'Skip pull of feed using PubSubHubbub: ' . $url;
 				//$text = 'Skip pull of feed using PubSubHubbub: ' . $url;
 				//Minz_Log::debug($text);
 				//Minz_Log::debug($text);
 				//file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
 				//file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
 				continue;	//When PubSubHubbub is used, do not pull refresh so often
 				continue;	//When PubSubHubbub is used, do not pull refresh so often
 			}
 			}
 
 
+			$mtime = 0;
+			$ttl = $feed->ttl();
+			if ($ttl == -1) {
+				continue;	//Feed refresh is disabled
+			}
+			if ((!$simplePiePush) && (!$feed_id) &&
+				($feed->lastUpdate() + 10 >= time() - ($ttl == -2 ? FreshRSS_Context::$user_conf->ttl_default : $ttl))) {
+				//Too early to refresh from source, but check whether the feed was updated by another user
+				$mtime = $feed->cacheModifiedTime();
+				if ($feed->lastUpdate() + 10 >= $mtime) {
+					continue;	//Nothing newer from other users
+				}
+				//Minz_Log::debug($feed->url() . ' was updated at ' . date('c', $mtime) . ' by another user');
+				//Will take advantage of the newer cache
+			}
+
 			if (!$feed->lock()) {
 			if (!$feed->lock()) {
 				Minz_Log::notice('Feed already being actualized: ' . $feed->url());
 				Minz_Log::notice('Feed already being actualized: ' . $feed->url());
 				continue;
 				continue;
@@ -325,7 +290,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				if ($simplePiePush) {
 				if ($simplePiePush) {
 					$feed->loadEntries($simplePiePush);	//Used by PubSubHubbub
 					$feed->loadEntries($simplePiePush);	//Used by PubSubHubbub
 				} else {
 				} else {
-					$feed->load(false);
+					$feed->load(false, $isNewFeed);
 				}
 				}
 			} catch (FreshRSS_Feed_Exception $e) {
 			} catch (FreshRSS_Feed_Exception $e) {
 				Minz_Log::warning($e->getMessage());
 				Minz_Log::warning($e->getMessage());
@@ -335,7 +300,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			}
 			}
 
 
 			$feed_history = $feed->keepHistory();
 			$feed_history = $feed->keepHistory();
-			if ($feed_history == -2) {
+			if ($isNewFeed) {
+				$feed_history = -1; //∞
+			} elseif ($feed_history == -2) {
 				// TODO: -2 must be a constant!
 				// TODO: -2 must be a constant!
 				// -2 means we take the default value from configuration
 				// -2 means we take the default value from configuration
 				$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
 				$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
@@ -346,7 +313,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			if (count($entries) > 0) {
 			if (count($entries) > 0) {
 				$newGuids = array();
 				$newGuids = array();
 				foreach ($entries as $entry) {
 				foreach ($entries as $entry) {
-					$newGuids[] = $entry->guid();
+					$newGuids[] = safe_ascii($entry->guid());
 				}
 				}
 				// For this feed, check existing GUIDs already in database.
 				// For this feed, check existing GUIDs already in database.
 				$existingHashForGuids = $entryDAO->listHashForFeedGuids($feed->id(), $newGuids);
 				$existingHashForGuids = $entryDAO->listHashForFeedGuids($feed->id(), $newGuids);
@@ -375,7 +342,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 						// This entry should not be added considering configuration and date.
 						// This entry should not be added considering configuration and date.
 						$oldGuids[] = $entry->guid();
 						$oldGuids[] = $entry->guid();
 					} else {
 					} else {
-						if ($entry_date < $date_min) {
+						if ($isNewFeed) {
+							$id = min(time(), $entry_date) . uSecString();
+						} elseif ($entry_date < $date_min) {
 							$id = min(time(), $entry_date) . uSecString();
 							$id = min(time(), $entry_date) . uSecString();
 							$entry->_isRead(true);	//Old article that was not in database. Probably an error, so mark as read
 							$entry->_isRead(true);	//Old article that was not in database. Probably an error, so mark as read
 						} else {
 						} else {
@@ -404,7 +373,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 						$entryDAO->addEntry($entry->toArray());
 						$entryDAO->addEntry($entry->toArray());
 					}
 					}
 				}
 				}
-				$entryDAO->updateLastSeen($feed->id(), $oldGuids);
+				$entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime);
 			}
 			}
 
 
 			if ($feed_history >= 0 && rand(0, 30) === 1) {
 			if ($feed_history >= 0 && rand(0, 30) === 1) {
@@ -423,7 +392,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				}
 				}
 			}
 			}
 
 
-			$feedDAO->updateLastUpdate($feed->id(), 0, $entryDAO->inTransaction());
+			$feedDAO->updateLastUpdate($feed->id(), false, $entryDAO->inTransaction(), $mtime);
 			if ($entryDAO->inTransaction()) {
 			if ($entryDAO->inTransaction()) {
 				$entryDAO->commit();
 				$entryDAO->commit();
 			}
 			}
@@ -464,6 +433,26 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				break;
 				break;
 			}
 			}
 		}
 		}
+		return array($updated_feeds, reset($feeds));
+	}
+
+	/**
+	 * This action actualizes entries from one or several feeds.
+	 *
+	 * Parameters are:
+	 *   - id (default: false): Feed ID
+	 *   - url (default: false): Feed URL
+	 *   - force (default: false)
+	 * If id and url are not specified, all the feeds are actualized. But if force is
+	 * false, process stops at 10 feeds to avoid time execution problem.
+	 */
+	public function actualizeAction() {
+		Minz_Session::_param('actualize_feeds', false);
+		$id = Minz_Request::param('id');
+		$url = Minz_Request::param('url');
+		$force = Minz_Request::param('force');
+
+		list($updated_feeds, $feed) = self::actualizeFeed($id, $url, $force);
 
 
 		if (Minz_Request::param('ajax')) {
 		if (Minz_Request::param('ajax')) {
 			// Most of the time, ajax request is for only one feed. But since
 			// Most of the time, ajax request is for only one feed. But since
@@ -479,7 +468,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		} else {
 		} else {
 			// Redirect to the main page with correct notification.
 			// Redirect to the main page with correct notification.
 			if ($updated_feeds === 1) {
 			if ($updated_feeds === 1) {
-				$feed = reset($feeds);
 				Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array(
 				Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array(
 					'params' => array('get' => 'f_' . $feed->id())
 					'params' => array('get' => 'f_' . $feed->id())
 				));
 				));
@@ -492,6 +480,36 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		return $updated_feeds;
 		return $updated_feeds;
 	}
 	}
 
 
+	public static function renameFeed($feed_id, $feed_name) {
+		if ($feed_id <= 0 || $feed_name == '') {
+			return false;
+		}
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		return $feedDAO->updateFeed($feed_id, array('name' => $feed_name));
+	}
+
+	public static function moveFeed($feed_id, $cat_id, $new_cat_name = '') {
+		if ($feed_id <= 0 || ($cat_id <= 0 && $new_cat_name == '')) {
+			return false;
+		}
+
+		$catDAO = new FreshRSS_CategoryDAO();
+		if ($cat_id > 0) {
+			$cat = $catDAO->searchById($cat_id);
+			$cat_id = $cat == null ? 0 : $cat->id();
+		}
+		if ($cat_id <= 1 && $new_cat_name != '') {
+			$cat_id = $catDAO->addCategory(array('name' => $new_cat_name));
+		}
+		if ($cat_id <= 1) {
+			$catDAO->checkDefault();
+			$cat_id = FreshRSS_CategoryDAO::defaultCategoryId;
+		}
+
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		return $feedDAO->updateFeed($feed_id, array('category' => $cat_id));
+	}
+
 	/**
 	/**
 	 * This action changes the category of a feed.
 	 * This action changes the category of a feed.
 	 *
 	 *
@@ -512,20 +530,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		$feed_id = Minz_Request::param('f_id');
 		$feed_id = Minz_Request::param('f_id');
 		$cat_id = Minz_Request::param('c_id');
 		$cat_id = Minz_Request::param('c_id');
 
 
-		if ($cat_id === false) {
-			// If category was not given get the default one.
-			$catDAO = new FreshRSS_CategoryDAO();
-			$catDAO->checkDefault();
-			$def_cat = $catDAO->getDefault();
-			$cat_id = $def_cat->id();
-		}
-
-		$feedDAO = FreshRSS_Factory::createFeedDao();
-		$values = array('category' => $cat_id);
-
-		$feed = $feedDAO->searchById($feed_id);
-		if ($feed && ($feed->category() == $cat_id ||
-		              $feedDAO->updateFeed($feed_id, $values))) {
+		if (self::moveFeed($feed_id, $cat_id)) {
 			// TODO: return something useful
 			// TODO: return something useful
 		} else {
 		} else {
 			Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' .
 			Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' .
@@ -534,6 +539,21 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 	}
 	}
 
 
+	public static function deleteFeed($feed_id) {
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		if ($feedDAO->deleteFeed($feed_id)) {
+			// TODO: Delete old favicon
+
+			// Remove related queries
+			FreshRSS_Context::$user_conf->queries = remove_query_by_get(
+				'f_' . $feed_id, FreshRSS_Context::$user_conf->queries);
+			FreshRSS_Context::$user_conf->save();
+
+			return true;
+		}
+		return false;
+	}
+
 	/**
 	/**
 	 * This action deletes a feed.
 	 * This action deletes a feed.
 	 *
 	 *
@@ -552,21 +572,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		if (!$redirect_url) {
 		if (!$redirect_url) {
 			$redirect_url = array('c' => 'subscription', 'a' => 'index');
 			$redirect_url = array('c' => 'subscription', 'a' => 'index');
 		}
 		}
-
 		if (!Minz_Request::isPost()) {
 		if (!Minz_Request::isPost()) {
 			Minz_Request::forward($redirect_url, true);
 			Minz_Request::forward($redirect_url, true);
 		}
 		}
 
 
 		$id = Minz_Request::param('id');
 		$id = Minz_Request::param('id');
-		$feedDAO = FreshRSS_Factory::createFeedDao();
-		if ($feedDAO->deleteFeed($id)) {
-			// TODO: Delete old favicon
-
-			// Remove related queries
-			FreshRSS_Context::$user_conf->queries = remove_query_by_get(
-				'f_' . $id, FreshRSS_Context::$user_conf->queries);
-			FreshRSS_Context::$user_conf->save();
 
 
+		if (self::deleteFeed($id)) {
 			Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url);
 			Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url);
 		} else {
 		} else {
 			Minz_Request::bad(_t('feedback.sub.feed.error'), $redirect_url);
 			Minz_Request::bad(_t('feedback.sub.feed.error'), $redirect_url);

+ 241 - 138
app/Controllers/importExportController.php

@@ -29,32 +29,14 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		Minz_View::prependTitle(_t('sub.import_export.title') . ' · ');
 		Minz_View::prependTitle(_t('sub.import_export.title') . ' · ');
 	}
 	}
 
 
-	/**
-	 * This action handles import action.
-	 *
-	 * It must be reached by a POST request.
-	 *
-	 * Parameter is:
-	 *   - file (default: nothing!)
-	 * Available file types are: zip, json or xml.
-	 */
-	public function importAction() {
-		if (!Minz_Request::isPost()) {
-			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
-		}
-
-		$file = $_FILES['file'];
-		$status_file = $file['error'];
-
-		if ($status_file !== 0) {
-			Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file);
-			Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'),
-			                  array('c' => 'importExport', 'a' => 'index'));
-		}
+	public function importFile($name, $path, $username = null) {
+		require_once(LIB_PATH . '/lib_opml.php');
 
 
-		@set_time_limit(300);
+		$this->catDAO = new FreshRSS_CategoryDAO($username);
+		$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
+		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
 
 
-		$type_file = $this->guessFileType($file['name']);
+		$type_file = self::guessFileType($name);
 
 
 		$list_files = array(
 		$list_files = array(
 			'opml' => array(),
 			'opml' => array(),
@@ -65,21 +47,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		// We try to list all files according to their type
 		// We try to list all files according to their type
 		$list = array();
 		$list = array();
 		if ($type_file === 'zip' && extension_loaded('zip')) {
 		if ($type_file === 'zip' && extension_loaded('zip')) {
-			$zip = zip_open($file['tmp_name']);
-
+			$zip = zip_open($path);
 			if (!is_resource($zip)) {
 			if (!is_resource($zip)) {
 				// zip_open cannot open file: something is wrong
 				// zip_open cannot open file: something is wrong
-				Minz_Log::warning('Zip archive cannot be imported. Error code: ' . $zip);
-				Minz_Request::bad(_t('feedback.import_export.zip_error'),
-				                  array('c' => 'importExport', 'a' => 'index'));
+				throw new FreshRSS_Zip_Exception($zip);
 			}
 			}
-
 			while (($zipfile = zip_read($zip)) !== false) {
 			while (($zipfile = zip_read($zip)) !== false) {
 				if (!is_resource($zipfile)) {
 				if (!is_resource($zipfile)) {
 					// zip_entry() can also return an error code!
 					// zip_entry() can also return an error code!
-					Minz_Log::warning('Zip file cannot be imported. Error code: ' . $zipfile);
+					throw new FreshRSS_Zip_Exception($zipfile);
 				} else {
 				} else {
-					$type_zipfile = $this->guessFileType(zip_entry_name($zipfile));
+					$type_zipfile = self::guessFileType(zip_entry_name($zipfile));
 					if ($type_file !== 'unknown') {
 					if ($type_file !== 'unknown') {
 						$list_files[$type_zipfile][] = zip_entry_read(
 						$list_files[$type_zipfile][] = zip_entry_read(
 							$zipfile,
 							$zipfile,
@@ -88,29 +66,88 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 					}
 					}
 				}
 				}
 			}
 			}
-
 			zip_close($zip);
 			zip_close($zip);
 		} elseif ($type_file === 'zip') {
 		} elseif ($type_file === 'zip') {
-			// Zip extension is not loaded
-			Minz_Request::bad(_t('feedback.import_export.no_zip_extension'),
-			                  array('c' => 'importExport', 'a' => 'index'));
+			// ZIP extension is not loaded
+			throw new FreshRSS_ZipMissing_Exception();
 		} elseif ($type_file !== 'unknown') {
 		} elseif ($type_file !== 'unknown') {
-			$list_files[$type_file][] = file_get_contents($file['tmp_name']);
+			$list_files[$type_file][] = file_get_contents($path);
 		}
 		}
 
 
 		// Import file contents.
 		// Import file contents.
 		// OPML first(so categories and feeds are imported)
 		// OPML first(so categories and feeds are imported)
 		// Starred articles then so the "favourite" status is already set
 		// Starred articles then so the "favourite" status is already set
 		// And finally all other files.
 		// And finally all other files.
-		$error = false;
+		$ok = true;
 		foreach ($list_files['opml'] as $opml_file) {
 		foreach ($list_files['opml'] as $opml_file) {
-			$error = $this->importOpml($opml_file);
+			if (!$this->importOpml($opml_file)) {
+				$ok = false;
+				if (FreshRSS_Context::$isCli) {
+					fwrite(STDERR, 'FreshRSS error during OPML import' . "\n");
+				} else {
+					Minz_Log::warning('Error during OPML import');
+				}
+			}
 		}
 		}
 		foreach ($list_files['json_starred'] as $article_file) {
 		foreach ($list_files['json_starred'] as $article_file) {
-			$error = $this->importJson($article_file, true);
+			if (!$this->importJson($article_file, true)) {
+				$ok = false;
+				if (FreshRSS_Context::$isCli) {
+					fwrite(STDERR, 'FreshRSS error during JSON stars import' . "\n");
+				} else {
+					Minz_Log::warning('Error during JSON stars import');
+				}
+			}
 		}
 		}
 		foreach ($list_files['json_feed'] as $article_file) {
 		foreach ($list_files['json_feed'] as $article_file) {
-			$error = $this->importJson($article_file);
+			if (!$this->importJson($article_file)) {
+				$ok = false;
+				if (FreshRSS_Context::$isCli) {
+					fwrite(STDERR, 'FreshRSS error during JSON feeds import' . "\n");
+				} else {
+					Minz_Log::warning('Error during JSON feeds import');
+				}
+			}
+		}
+
+		return $ok;
+	}
+
+	/**
+	 * This action handles import action.
+	 *
+	 * It must be reached by a POST request.
+	 *
+	 * Parameter is:
+	 *   - file (default: nothing!)
+	 * Available file types are: zip, json or xml.
+	 */
+	public function importAction() {
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+		}
+
+		$file = $_FILES['file'];
+		$status_file = $file['error'];
+
+		if ($status_file !== 0) {
+			Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file);
+			Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'),
+			                  array('c' => 'importExport', 'a' => 'index'));
+		}
+
+		@set_time_limit(300);
+
+		$error = false;
+		try {
+			$error = !$this->importFile($file['name'], $file['tmp_name']);
+		} catch (FreshRSS_ZipMissing_Exception $zme) {
+			Minz_Request::bad(_t('feedback.import_export.no_zip_extension'),
+				array('c' => 'importExport', 'a' => 'index'));
+		} catch (FreshRSS_Zip_Exception $ze) {
+			Minz_Log::warning('ZIP archive cannot be imported. Error code: ' . $ze->zipErrorCode());
+			Minz_Request::bad(_t('feedback.import_export.zip_error'),
+				array('c' => 'importExport', 'a' => 'index'));
 		}
 		}
 
 
 		// And finally, we get import status and redirect to the home page
 		// And finally, we get import status and redirect to the home page
@@ -126,7 +163,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	 * Itis a *very* basic guess file type function. Only based on filename.
 	 * Itis a *very* basic guess file type function. Only based on filename.
 	 * That's could be improved but should be enough for what we have to do.
 	 * That's could be improved but should be enough for what we have to do.
 	 */
 	 */
-	private function guessFileType($filename) {
+	private static function guessFileType($filename) {
 		if (substr_compare($filename, '.zip', -4) === 0) {
 		if (substr_compare($filename, '.zip', -4) === 0) {
 			return 'zip';
 			return 'zip';
 		} elseif (substr_compare($filename, '.opml', -5) === 0 ||
 		} elseif (substr_compare($filename, '.opml', -5) === 0 ||
@@ -146,15 +183,19 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	 * This method parses and imports an OPML file.
 	 * This method parses and imports an OPML file.
 	 *
 	 *
 	 * @param string $opml_file the OPML file content.
 	 * @param string $opml_file the OPML file content.
-	 * @return boolean true if an error occured, false else.
+	 * @return boolean false if an error occured, true otherwise.
 	 */
 	 */
 	private function importOpml($opml_file) {
 	private function importOpml($opml_file) {
 		$opml_array = array();
 		$opml_array = array();
 		try {
 		try {
 			$opml_array = libopml_parse_string($opml_file, false);
 			$opml_array = libopml_parse_string($opml_file, false);
 		} catch (LibOPML_Exception $e) {
 		} catch (LibOPML_Exception $e) {
-			Minz_Log::warning($e->getMessage());
-			return true;
+			if (FreshRSS_Context::$isCli) {
+				fwrite(STDERR, 'FreshRSS error during OPML parsing: ' . $e->getMessage() . "\n");
+			} else {
+				Minz_Log::warning($e->getMessage());
+			}
+			return false;
 		}
 		}
 
 
 		$this->catDAO->checkDefault();
 		$this->catDAO->checkDefault();
@@ -167,51 +208,49 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	 *
 	 *
 	 * @param array $opml_elements an OPML element (body or outline).
 	 * @param array $opml_elements an OPML element (body or outline).
 	 * @param string $parent_cat the name of the parent category.
 	 * @param string $parent_cat the name of the parent category.
-	 * @return boolean true if an error occured, false else.
+	 * @return boolean false if an error occured, true otherwise.
 	 */
 	 */
 	private function addOpmlElements($opml_elements, $parent_cat = null) {
 	private function addOpmlElements($opml_elements, $parent_cat = null) {
-		$error = false;
+		$ok = true;
 
 
 		$nb_feeds = count($this->feedDAO->listFeeds());
 		$nb_feeds = count($this->feedDAO->listFeeds());
 		$nb_cats = count($this->catDAO->listCategories(false));
 		$nb_cats = count($this->catDAO->listCategories(false));
 		$limits = FreshRSS_Context::$system_conf->limits;
 		$limits = FreshRSS_Context::$system_conf->limits;
 
 
 		foreach ($opml_elements as $elt) {
 		foreach ($opml_elements as $elt) {
-			$is_error = false;
 			if (isset($elt['xmlUrl'])) {
 			if (isset($elt['xmlUrl'])) {
 				// If xmlUrl exists, it means it is a feed
 				// If xmlUrl exists, it means it is a feed
-				if ($nb_feeds >= $limits['max_feeds']) {
+				if (FreshRSS_Context::$isCli && $nb_feeds >= $limits['max_feeds']) {
 					Minz_Log::warning(_t('feedback.sub.feed.over_max',
 					Minz_Log::warning(_t('feedback.sub.feed.over_max',
-					                  $limits['max_feeds']));
-					$is_error = true;
+									  $limits['max_feeds']));
+					$ok = false;
 					continue;
 					continue;
 				}
 				}
 
 
-				$is_error = $this->addFeedOpml($elt, $parent_cat);
-				if (!$is_error) {
-					$nb_feeds += 1;
+				if ($this->addFeedOpml($elt, $parent_cat)) {
+					$nb_feeds++;
+				} else {
+					$ok = false;
 				}
 				}
 			} else {
 			} else {
 				// No xmlUrl? It should be a category!
 				// No xmlUrl? It should be a category!
 				$limit_reached = ($nb_cats >= $limits['max_categories']);
 				$limit_reached = ($nb_cats >= $limits['max_categories']);
-				if ($limit_reached) {
+				if (!FreshRSS_Context::$isCli && $limit_reached) {
 					Minz_Log::warning(_t('feedback.sub.category.over_max',
 					Minz_Log::warning(_t('feedback.sub.category.over_max',
-					                  $limits['max_categories']));
+									  $limits['max_categories']));
+					$ok = false;
+					continue;
 				}
 				}
 
 
-				$is_error = $this->addCategoryOpml($elt, $parent_cat, $limit_reached);
-				if (!$is_error) {
-					$nb_cats += 1;
+				if ($this->addCategoryOpml($elt, $parent_cat, $limit_reached)) {
+					$nb_cats++;
+				} else {
+					$ok = false;
 				}
 				}
 			}
 			}
-
-			if (!$error && $is_error) {
-				// oops: there is at least one error!
-				$error = $is_error;
-			}
 		}
 		}
 
 
-		return $error;
+		return $ok;
 	}
 	}
 
 
 	/**
 	/**
@@ -219,21 +258,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	 *
 	 *
 	 * @param array $feed_elt an OPML element (must be a feed element).
 	 * @param array $feed_elt an OPML element (must be a feed element).
 	 * @param string $parent_cat the name of the parent category.
 	 * @param string $parent_cat the name of the parent category.
-	 * @return boolean true if an error occured, false else.
+	 * @return boolean false if an error occured, true otherwise.
 	 */
 	 */
 	private function addFeedOpml($feed_elt, $parent_cat) {
 	private function addFeedOpml($feed_elt, $parent_cat) {
-		$default_cat = $this->catDAO->getDefault();
-		if (is_null($parent_cat)) {
+		if ($parent_cat == null) {
 			// This feed has no parent category so we get the default one
 			// This feed has no parent category so we get the default one
+			$this->catDAO->checkDefault();
+			$default_cat = $this->catDAO->getDefault();
 			$parent_cat = $default_cat->name();
 			$parent_cat = $default_cat->name();
 		}
 		}
 
 
 		$cat = $this->catDAO->searchByName($parent_cat);
 		$cat = $this->catDAO->searchByName($parent_cat);
-		if (is_null($cat)) {
+		if ($cat == null) {
 			// If there is not $cat, it means parent category does not exist in
 			// If there is not $cat, it means parent category does not exist in
 			// database.
 			// database.
 			// If it happens, take the default category.
 			// If it happens, take the default category.
-			$cat = $default_cat;
+			$this->catDAO->checkDefault();
+			$cat = $this->catDAO->getDefault();
 		}
 		}
 
 
 		// We get different useful information
 		// We get different useful information
@@ -259,7 +300,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 
 			// Call the extension hook
 			// Call the extension hook
 			$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
 			$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
-			if (!is_null($feed)) {
+			if ($feed != null) {
 				// addFeedObject checks if feed is already in DB so nothing else to
 				// addFeedObject checks if feed is already in DB so nothing else to
 				// check here
 				// check here
 				$id = $this->feedDAO->addFeedObject($feed);
 				$id = $this->feedDAO->addFeedObject($feed);
@@ -268,11 +309,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 				$error = true;
 				$error = true;
 			}
 			}
 		} catch (FreshRSS_Feed_Exception $e) {
 		} catch (FreshRSS_Feed_Exception $e) {
-			Minz_Log::warning($e->getMessage());
+			if (FreshRSS_Context::$isCli) {
+				fwrite(STDERR, 'FreshRSS error during OPML feed import: ' . $e->getMessage() . "\n");
+			} else {
+				Minz_Log::warning($e->getMessage());
+			}
 			$error = true;
 			$error = true;
 		}
 		}
 
 
-		return $error;
+		if ($error) {
+			if (FreshRSS_Context::$isCli) {
+				fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id() . "\n");
+			} else {
+				Minz_Log::warning('Error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id());
+			}
+		}
+
+		return !$error;
 	}
 	}
 
 
 	/**
 	/**
@@ -282,29 +335,34 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	 * @param string $parent_cat the name of the parent category.
 	 * @param string $parent_cat the name of the parent category.
 	 * @param boolean $cat_limit_reached indicates if category limit has been reached.
 	 * @param boolean $cat_limit_reached indicates if category limit has been reached.
 	 *                if yes, category is not added (but we try for feeds!)
 	 *                if yes, category is not added (but we try for feeds!)
-	 * @return boolean true if an error occured, false else.
+	 * @return boolean false if an error occured, true otherwise.
 	 */
 	 */
 	private function addCategoryOpml($cat_elt, $parent_cat, $cat_limit_reached) {
 	private function addCategoryOpml($cat_elt, $parent_cat, $cat_limit_reached) {
 		// Create a new Category object
 		// Create a new Category object
-		$cat = new FreshRSS_Category(Minz_Helper::htmlspecialchars_utf8($cat_elt['text']));
+		$catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']);
+		$cat = new FreshRSS_Category($catName);
 
 
 		$error = true;
 		$error = true;
-		if (!$cat_limit_reached) {
+		if (FreshRSS_Context::$isCli || !$cat_limit_reached) {
 			$id = $this->catDAO->addCategoryObject($cat);
 			$id = $this->catDAO->addCategoryObject($cat);
 			$error = ($id === false);
 			$error = ($id === false);
 		}
 		}
+		if ($error) {
+			if (FreshRSS_Context::$isCli) {
+				fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n");
+			} else {
+				Minz_Log::warning('Error during OPML category import from URL: ' . $catName);
+			}
+		}
 
 
 		if (isset($cat_elt['@outlines'])) {
 		if (isset($cat_elt['@outlines'])) {
 			// Our cat_elt contains more categories or more feeds, so we
 			// Our cat_elt contains more categories or more feeds, so we
 			// add them recursively.
 			// add them recursively.
 			// Note: FreshRSS does not support yet category arborescence
 			// Note: FreshRSS does not support yet category arborescence
-			$res = $this->addOpmlElements($cat_elt['@outlines'], $cat->name());
-			if (!$error && $res) {
-				$error = true;
-			}
+			$error &= !$this->addOpmlElements($cat_elt['@outlines'], $catName);
 		}
 		}
 
 
-		return $error;
+		return !$error;
 	}
 	}
 
 
 	/**
 	/**
@@ -312,13 +370,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	 *
 	 *
 	 * @param string $article_file the JSON file content.
 	 * @param string $article_file the JSON file content.
 	 * @param boolean $starred true if articles from the file must be starred.
 	 * @param boolean $starred true if articles from the file must be starred.
-	 * @return boolean true if an error occured, false else.
+	 * @return boolean false if an error occured, true otherwise.
 	 */
 	 */
 	private function importJson($article_file, $starred = false) {
 	private function importJson($article_file, $starred = false) {
 		$article_object = json_decode($article_file, true);
 		$article_object = json_decode($article_file, true);
-		if (is_null($article_object)) {
-			Minz_Log::warning('Try to import a non-JSON file');
-			return true;
+		if ($article_object == null) {
+			if (FreshRSS_Context::$isCli) {
+				fwrite(STDERR, 'FreshRSS error trying to import a non-JSON file' . "\n");
+			} else {
+				Minz_Log::warning('Try to import a non-JSON file');
+			}
+			return false;
 		}
 		}
 
 
 		$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
 		$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
@@ -337,29 +399,36 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			$feed = new FreshRSS_Feed($item['origin'][$key]);
 			$feed = new FreshRSS_Feed($item['origin'][$key]);
 			$feed = $this->feedDAO->searchByUrl($feed->url());
 			$feed = $this->feedDAO->searchByUrl($feed->url());
 
 
-			if (is_null($feed)) {
+			if ($feed == null) {
 				// Feed does not exist in DB,we should to try to add it.
 				// Feed does not exist in DB,we should to try to add it.
-				if ($nb_feeds >= $limits['max_feeds']) {
+				if ((!FreshRSS_Context::$isCli) && ($nb_feeds >= $limits['max_feeds'])) {
 					// Oops, no more place!
 					// Oops, no more place!
 					Minz_Log::warning(_t('feedback.sub.feed.over_max', $limits['max_feeds']));
 					Minz_Log::warning(_t('feedback.sub.feed.over_max', $limits['max_feeds']));
 				} else {
 				} else {
 					$feed = $this->addFeedJson($item['origin'], $google_compliant);
 					$feed = $this->addFeedJson($item['origin'], $google_compliant);
 				}
 				}
 
 
-				if (is_null($feed)) {
+				if ($feed == null) {
 					// Still null? It means something went wrong.
 					// Still null? It means something went wrong.
 					$error = true;
 					$error = true;
 				} else {
 				} else {
-					// Nice! Increase the counter.
-					$nb_feeds += 1;
+					$nb_feeds++;
 				}
 				}
 			}
 			}
 
 
-			if (!is_null($feed)) {
+			if ($feed != null) {
 				$article_to_feed[$item['id']] = $feed->id();
 				$article_to_feed[$item['id']] = $feed->id();
 			}
 			}
 		}
 		}
 
 
+		$newGuids = array();
+		foreach ($article_object['items'] as $item) {
+			$newGuids[] = safe_ascii($item['id']);
+		}
+		// For this feed, check existing GUIDs already in database.
+		$existingHashForGuids = $this->entryDAO->listHashForFeedGuids($feed->id(), $newGuids);
+		unset($newGuids);
+
 		// Then, articles are imported.
 		// Then, articles are imported.
 		$this->entryDAO->beginTransaction();
 		$this->entryDAO->beginTransaction();
 		foreach ($article_object['items'] as $item) {
 		foreach ($article_object['items'] as $item) {
@@ -376,7 +445,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			if ($google_compliant) {
 			if ($google_compliant) {
 				// Remove tags containing "/state/com.google" which are useless.
 				// Remove tags containing "/state/com.google" which are useless.
 				$tags = array_filter($tags, function($var) {
 				$tags = array_filter($tags, function($var) {
-					return strpos($var, '/state/com.google') === false;
+					return strpos($var, '/state/com.google') !== false;
 				});
 				});
 			}
 			}
 
 
@@ -389,13 +458,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			$entry->_tags($tags);
 			$entry->_tags($tags);
 
 
 			$entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
 			$entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
-			if (is_null($entry)) {
+			if ($entry == null) {
 				// An extension has returned a null value, there is nothing to insert.
 				// An extension has returned a null value, there is nothing to insert.
 				continue;
 				continue;
 			}
 			}
 
 
 			$values = $entry->toArray();
 			$values = $entry->toArray();
-			$id = $this->entryDAO->addEntry($values);
+			if (isset($existingHashForGuids[$entry->guid()])) {
+				$id = $this->entryDAO->updateEntry($values);
+			} else {
+				$id = $this->entryDAO->addEntry($values);
+			}
 
 
 			if (!$error && ($id === false)) {
 			if (!$error && ($id === false)) {
 				$error = true;
 				$error = true;
@@ -403,7 +476,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		}
 		}
 		$this->entryDAO->commit();
 		$this->entryDAO->commit();
 
 
-		return $error;
+		return !$error;
 	}
 	}
 
 
 	/**
 	/**
@@ -415,8 +488,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	 *         else null.
 	 *         else null.
 	 */
 	 */
 	private function addFeedJson($origin, $google_compliant) {
 	private function addFeedJson($origin, $google_compliant) {
-		$default_cat = $this->catDAO->getDefault();
-
 		$return = null;
 		$return = null;
 		$key = $google_compliant ? 'htmlUrl' : 'feedUrl';
 		$key = $google_compliant ? 'htmlUrl' : 'feedUrl';
 		$url = $origin[$key];
 		$url = $origin[$key];
@@ -426,13 +497,13 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		try {
 		try {
 			// Create a Feed object and add it in database.
 			// Create a Feed object and add it in database.
 			$feed = new FreshRSS_Feed($url);
 			$feed = new FreshRSS_Feed($url);
-			$feed->_category($default_cat->id());
+			$feed->_category(FreshRSS_CategoryDAO::defaultCategoryId);
 			$feed->_name($name);
 			$feed->_name($name);
 			$feed->_website($website);
 			$feed->_website($website);
 
 
 			// Call the extension hook
 			// Call the extension hook
 			$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
 			$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
-			if (!is_null($feed)) {
+			if ($feed != null) {
 				// addFeedObject checks if feed is already in DB so nothing else to
 				// addFeedObject checks if feed is already in DB so nothing else to
 				// check here.
 				// check here.
 				$id = $this->feedDAO->addFeedObject($feed);
 				$id = $this->feedDAO->addFeedObject($feed);
@@ -443,67 +514,98 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 				}
 				}
 			}
 			}
 		} catch (FreshRSS_Feed_Exception $e) {
 		} catch (FreshRSS_Feed_Exception $e) {
-			Minz_Log::warning($e->getMessage());
+			if (FreshRSS_Context::$isCli) {
+				fwrite(STDERR, 'FreshRSS error during JSON feed import: ' . $e->getMessage() . "\n");
+			} else {
+				Minz_Log::warning($e->getMessage());
+			}
 		}
 		}
 
 
 		return $return;
 		return $return;
 	}
 	}
 
 
-	/**
-	 * This action handles export action.
-	 *
-	 * This action must be reached by a POST request.
-	 *
-	 * Parameters are:
-	 *   - export_opml (default: false)
-	 *   - export_starred (default: false)
-	 *   - export_feeds (default: array()) a list of feed ids
-	 */
-	public function exportAction() {
-		if (!Minz_Request::isPost()) {
-			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
-		}
+	public function exportFile($export_opml = true, $export_starred = false, $export_feeds = array(), $maxFeedEntries = 50, $username = null) {
+		require_once(LIB_PATH . '/lib_opml.php');
 
 
-		$this->view->_useLayout(false);
+		$this->catDAO = new FreshRSS_CategoryDAO($username);
+		$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
+		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
+
+		if ($export_feeds === true) {
+			//All feeds
+			$export_feeds = $this->feedDAO->listFeedsIds();
+		}
+		if (!is_array($export_feeds)) {
+			$export_feeds = array();
+		}
 
 
-		$export_opml = Minz_Request::param('export_opml', false);
-		$export_starred = Minz_Request::param('export_starred', false);
-		$export_feeds = Minz_Request::param('export_feeds', array());
+		$day = date('Y-m-d');
 
 
 		$export_files = array();
 		$export_files = array();
 		if ($export_opml) {
 		if ($export_opml) {
-			$export_files['feeds.opml'] = $this->generateOpml();
+			$export_files["feeds_${day}.opml.xml"] = $this->generateOpml();
 		}
 		}
 
 
 		if ($export_starred) {
 		if ($export_starred) {
-			$export_files['starred.json'] = $this->generateEntries('starred');
+			$export_files["starred_${day}.json"] = $this->generateEntries('starred');
 		}
 		}
 
 
 		foreach ($export_feeds as $feed_id) {
 		foreach ($export_feeds as $feed_id) {
 			$feed = $this->feedDAO->searchById($feed_id);
 			$feed = $this->feedDAO->searchById($feed_id);
 			if ($feed) {
 			if ($feed) {
-				$filename = 'feed_' . $feed->category() . '_'
+				$filename = "feed_${day}_" . $feed->category() . '_'
 				          . $feed->id() . '.json';
 				          . $feed->id() . '.json';
-				$export_files[$filename] = $this->generateEntries('feed', $feed);
+				$export_files[$filename] = $this->generateEntries('feed', $feed, $maxFeedEntries);
 			}
 			}
 		}
 		}
 
 
 		$nb_files = count($export_files);
 		$nb_files = count($export_files);
 		if ($nb_files > 1) {
 		if ($nb_files > 1) {
-			// If there are more than 1 file to export, we need a zip archive.
+			// If there are more than 1 file to export, we need a ZIP archive.
 			try {
 			try {
-				$this->exportZip($export_files);
+				$this->sendZip($export_files);
 			} catch (Exception $e) {
 			} catch (Exception $e) {
-				# Oops, there is no Zip extension!
-				Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'),
-				                  array('c' => 'importExport', 'a' => 'index'));
+				throw new FreshRSS_ZipMissing_Exception($e);
 			}
 			}
 		} elseif ($nb_files === 1) {
 		} elseif ($nb_files === 1) {
 			// Only one file? Guess its type and export it.
 			// Only one file? Guess its type and export it.
 			$filename = key($export_files);
 			$filename = key($export_files);
-			$type = $this->guessFileType($filename);
-			$this->exportFile('freshrss_' . $filename, $export_files[$filename], $type);
-		} else {
+			$type = self::guessFileType($filename);
+			$this->sendFile('freshrss_' . $filename, $export_files[$filename], $type);
+		}
+		return $nb_files;
+	}
+
+	/**
+	 * This action handles export action.
+	 *
+	 * This action must be reached by a POST request.
+	 *
+	 * Parameters are:
+	 *   - export_opml (default: false)
+	 *   - export_starred (default: false)
+	 *   - export_feeds (default: array()) a list of feed ids
+	 */
+	public function exportAction() {
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+		}
+		$this->view->_useLayout(false);
+
+		$nb_files = 0;
+		try {
+			$nb_files = $this->exportFile(
+					Minz_Request::param('export_opml', false),
+					Minz_Request::param('export_starred', false),
+					Minz_Request::param('export_feeds', array())
+				);
+		} catch (FreshRSS_ZipMissing_Exception $zme) {
+			# Oops, there is no ZIP extension!
+			Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'),
+			                  array('c' => 'importExport', 'a' => 'index'));
+		}
+
+		if ($nb_files < 1) {
 			// Nothing to do...
 			// Nothing to do...
 			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
 			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
 		}
 		}
@@ -532,7 +634,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	 * @param FreshRSS_Feed $feed feed of which we want to get entries.
 	 * @param FreshRSS_Feed $feed feed of which we want to get entries.
 	 * @return string the JSON file content.
 	 * @return string the JSON file content.
 	 */
 	 */
-	private function generateEntries($type, $feed = NULL) {
+	private function generateEntries($type, $feed = NULL, $maxFeedEntries = 50) {
 		$this->view->categories = $this->catDAO->listCategories();
 		$this->view->categories = $this->catDAO->listCategories();
 
 
 		if ($type == 'starred') {
 		if ($type == 'starred') {
@@ -542,12 +644,12 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			$this->view->entries = $this->entryDAO->listWhere(
 			$this->view->entries = $this->entryDAO->listWhere(
 				's', '', FreshRSS_Entry::STATE_ALL, 'ASC', $unread_fav['all']
 				's', '', FreshRSS_Entry::STATE_ALL, 'ASC', $unread_fav['all']
 			);
 			);
-		} elseif ($type == 'feed' && !is_null($feed)) {
+		} elseif ($type === 'feed' && $feed != null) {
 			$this->view->list_title = _t('sub.import_export.feed_list', $feed->name());
 			$this->view->list_title = _t('sub.import_export.feed_list', $feed->name());
 			$this->view->type = 'feed/' . $feed->id();
 			$this->view->type = 'feed/' . $feed->id();
 			$this->view->entries = $this->entryDAO->listWhere(
 			$this->view->entries = $this->entryDAO->listWhere(
 				'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC',
 				'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC',
-				FreshRSS_Context::$user_conf->posts_per_page
+				$maxFeedEntries
 			);
 			);
 			$this->view->feed = $feed;
 			$this->view->feed = $feed;
 		}
 		}
@@ -561,7 +663,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	 * @param array $files list of files where key is filename and value the content.
 	 * @param array $files list of files where key is filename and value the content.
 	 * @throws Exception if Zip extension is not loaded.
 	 * @throws Exception if Zip extension is not loaded.
 	 */
 	 */
-	private function exportZip($files) {
+	private function sendZip($files) {
 		if (!extension_loaded('zip')) {
 		if (!extension_loaded('zip')) {
 			throw new Exception();
 			throw new Exception();
 		}
 		}
@@ -579,7 +681,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$zip->close();
 		$zip->close();
 		header('Content-Type: application/zip');
 		header('Content-Type: application/zip');
 		header('Content-Length: ' . filesize($zip_file));
 		header('Content-Length: ' . filesize($zip_file));
-		header('Content-Disposition: attachment; filename="freshrss_export.zip"');
+		$day = date('Y-m-d');
+		header('Content-Disposition: attachment; filename="freshrss_' . $day . '_export.zip"');
 		readfile($zip_file);
 		readfile($zip_file);
 		unlink($zip_file);
 		unlink($zip_file);
 	}
 	}
@@ -592,16 +695,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	 * @param string $type the file type (opml, json_feed or json_starred).
 	 * @param string $type the file type (opml, json_feed or json_starred).
 	 *                     If equals to unknown, nothing happens.
 	 *                     If equals to unknown, nothing happens.
 	 */
 	 */
-	private function exportFile($filename, $content, $type) {
+	private function sendFile($filename, $content, $type) {
 		if ($type === 'unknown') {
 		if ($type === 'unknown') {
 			return;
 			return;
 		}
 		}
 
 
 		$content_type = '';
 		$content_type = '';
 		if ($type === 'opml') {
 		if ($type === 'opml') {
-			$content_type = "text/opml";
+			$content_type = 'application/xml';
 		} elseif ($type === 'json_feed' || $type === 'json_starred') {
 		} elseif ($type === 'json_feed' || $type === 'json_starred') {
-			$content_type = "text/json";
+			$content_type = 'application/json';
 		}
 		}
 
 
 		header('Content-Type: ' . $content_type . '; charset=utf-8');
 		header('Content-Type: ' . $content_type . '; charset=utf-8');

+ 38 - 6
app/Controllers/indexController.php

@@ -34,7 +34,9 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 
 
 		$this->view->callbackBeforeContent = function($view) {
 		$this->view->callbackBeforeContent = function($view) {
 			try {
 			try {
+				FreshRSS_Context::$number++;	//+1 for pagination
 				$entries = FreshRSS_index_Controller::listEntriesByContext();
 				$entries = FreshRSS_index_Controller::listEntriesByContext();
+				FreshRSS_Context::$number--;
 
 
 				$nb_entries = count($entries);
 				$nb_entries = count($entries);
 				if ($nb_entries > FreshRSS_Context::$number) {
 				if ($nb_entries > FreshRSS_Context::$number) {
@@ -154,8 +156,14 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 	 *   - order (default: conf->sort_order)
 	 *   - order (default: conf->sort_order)
 	 *   - nb (default: conf->posts_per_page)
 	 *   - nb (default: conf->posts_per_page)
 	 *   - next (default: empty string)
 	 *   - next (default: empty string)
+	 *   - hours (default: 0)
 	 */
 	 */
 	private function updateContext() {
 	private function updateContext() {
+		if (empty(FreshRSS_Context::$categories)) {
+			$catDAO = new FreshRSS_CategoryDAO();
+			FreshRSS_Context::$categories = $catDAO->listCategories();
+		}
+
 		// Update number of read / unread variables.
 		// Update number of read / unread variables.
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 		FreshRSS_Context::$total_starred = $entryDAO->countUnreadReadFavorites();
 		FreshRSS_Context::$total_starred = $entryDAO->countUnreadReadFavorites();
@@ -180,10 +188,14 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		FreshRSS_Context::$order = Minz_Request::param(
 		FreshRSS_Context::$order = Minz_Request::param(
 			'order', FreshRSS_Context::$user_conf->sort_order
 			'order', FreshRSS_Context::$user_conf->sort_order
 		);
 		);
-		FreshRSS_Context::$number = Minz_Request::param(
-			'nb', FreshRSS_Context::$user_conf->posts_per_page
-		);
+		FreshRSS_Context::$number = intval(Minz_Request::param('nb', FreshRSS_Context::$user_conf->posts_per_page));
+		if (FreshRSS_Context::$number > FreshRSS_Context::$user_conf->max_posts_per_rss) {
+			FreshRSS_Context::$number = max(
+				FreshRSS_Context::$user_conf->max_posts_per_rss,
+				FreshRSS_Context::$user_conf->posts_per_page);
+		}
 		FreshRSS_Context::$first_id = Minz_Request::param('next', '');
 		FreshRSS_Context::$first_id = Minz_Request::param('next', '');
+		FreshRSS_Context::$sinceHours = intval(Minz_Request::param('hours', 0));
 	}
 	}
 
 
 	/**
 	/**
@@ -201,11 +213,31 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 			$id = '';
 			$id = '';
 		}
 		}
 
 
-		return $entryDAO->listWhere(
+		$limit = FreshRSS_Context::$number;
+
+		$date_min = 0;
+		if (FreshRSS_Context::$sinceHours) {
+			$date_min = time() - (FreshRSS_Context::$sinceHours * 3600);
+			$limit = FreshRSS_Context::$user_conf->max_posts_per_rss;
+		}
+
+		$entries = $entryDAO->listWhere(
 			$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
 			$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
-			FreshRSS_Context::$number + 1, FreshRSS_Context::$first_id,
-			FreshRSS_Context::$search
+			$limit, FreshRSS_Context::$first_id,
+			FreshRSS_Context::$search, $date_min
 		);
 		);
+
+		if (FreshRSS_Context::$sinceHours && (count($entries) < FreshRSS_Context::$user_conf->min_posts_per_rss)) {
+			$date_min = 0;
+			$limit = FreshRSS_Context::$user_conf->min_posts_per_rss;
+			$entries = $entryDAO->listWhere(
+				$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
+				$limit, FreshRSS_Context::$first_id,
+				FreshRSS_Context::$search, $date_min
+			);
+		}
+
+		return $entries;
 	}
 	}
 
 
 	/**
 	/**

+ 29 - 7
app/Controllers/statsController.php

@@ -18,6 +18,27 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
 		Minz_View::prependTitle(_t('admin.stats.title') . ' · ');
 		Minz_View::prependTitle(_t('admin.stats.title') . ' · ');
 	}
 	}
 
 
+	private function convertToSerie($data) {
+		$serie = array();
+
+		foreach ($data as $key => $value) {
+			$serie[] = array($key, $value);
+		}
+
+		return $serie;
+	}
+
+	private function convertToPieSerie($data) {
+		$serie = array();
+
+		foreach ($data as $value) {
+			$value['data'] = array(array(0, (int) $value['data']));
+			$serie[] = $value;
+		}
+
+		return $serie;
+	}
+
 	/**
 	/**
 	 * This action handles the statistic main page.
 	 * This action handles the statistic main page.
 	 *
 	 *
@@ -33,10 +54,11 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
 		$statsDAO = FreshRSS_Factory::createStatsDAO();
 		$statsDAO = FreshRSS_Factory::createStatsDAO();
 		Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
 		Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
 		$this->view->repartition = $statsDAO->calculateEntryRepartition();
 		$this->view->repartition = $statsDAO->calculateEntryRepartition();
-		$this->view->count = $statsDAO->calculateEntryCount();
-		$this->view->average = $statsDAO->calculateEntryAverage();
-		$this->view->feedByCategory = $statsDAO->calculateFeedByCategory();
-		$this->view->entryByCategory = $statsDAO->calculateEntryByCategory();
+		$entryCount = $statsDAO->calculateEntryCount();
+		$this->view->count = $this->convertToSerie($entryCount);
+		$this->view->average = round(array_sum(array_values($entryCount)) / count($entryCount), 2);
+		$this->view->feedByCategory = $this->convertToPieSerie($statsDAO->calculateFeedByCategory());
+		$this->view->entryByCategory = $this->convertToPieSerie($statsDAO->calculateEntryByCategory());
 		$this->view->topFeed = $statsDAO->calculateTopFeed();
 		$this->view->topFeed = $statsDAO->calculateTopFeed();
 	}
 	}
 
 
@@ -118,11 +140,11 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
 		$this->view->days = $statsDAO->getDays();
 		$this->view->days = $statsDAO->getDays();
 		$this->view->months = $statsDAO->getMonths();
 		$this->view->months = $statsDAO->getMonths();
 		$this->view->repartition = $statsDAO->calculateEntryRepartitionPerFeed($id);
 		$this->view->repartition = $statsDAO->calculateEntryRepartitionPerFeed($id);
-		$this->view->repartitionHour = $statsDAO->calculateEntryRepartitionPerFeedPerHour($id);
+		$this->view->repartitionHour = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerHour($id));
 		$this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id);
 		$this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id);
-		$this->view->repartitionDayOfWeek = $statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id);
+		$this->view->repartitionDayOfWeek = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id));
 		$this->view->averageDayOfWeek = $statsDAO->calculateEntryAveragePerFeedPerDayOfWeek($id);
 		$this->view->averageDayOfWeek = $statsDAO->calculateEntryAveragePerFeedPerDayOfWeek($id);
-		$this->view->repartitionMonth = $statsDAO->calculateEntryRepartitionPerFeedPerMonth($id);
+		$this->view->repartitionMonth = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerMonth($id));
 		$this->view->averageMonth = $statsDAO->calculateEntryAveragePerFeedPerMonth($id);
 		$this->view->averageMonth = $statsDAO->calculateEntryAveragePerFeedPerMonth($id);
 	}
 	}
 }
 }

+ 84 - 77
app/Controllers/userController.php

@@ -24,6 +24,16 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		}
 		}
 	}
 	}
 
 
+	public static function hashPassword($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
+		return $passwordHash == '' ? '' : $passwordHash;
+	}
+
 	/**
 	/**
 	 * This action displays the user profile page.
 	 * This action displays the user profile page.
 	 */
 	 */
@@ -41,12 +51,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			if ($passwordPlain != '') {
 			if ($passwordPlain != '') {
 				Minz_Request::_param('newPasswordPlain');	//Discard plain-text password ASAP
 				Minz_Request::_param('newPasswordPlain');	//Discard plain-text password ASAP
 				$_POST['newPasswordPlain'] = '';
 				$_POST['newPasswordPlain'] = '';
-				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
+				$passwordHash = self::hashPassword($passwordPlain);
 				$ok &= ($passwordHash != '');
 				$ok &= ($passwordHash != '');
 				FreshRSS_Context::$user_conf->passwordHash = $passwordHash;
 				FreshRSS_Context::$user_conf->passwordHash = $passwordHash;
 			}
 			}
@@ -54,12 +59,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 
 
 			$passwordPlain = Minz_Request::param('apiPasswordPlain', '', true);
 			$passwordPlain = Minz_Request::param('apiPasswordPlain', '', true);
 			if ($passwordPlain != '') {
 			if ($passwordPlain != '') {
-				if (!function_exists('password_hash')) {
-					include_once(LIB_PATH . '/password_compat.php');
-				}
-				$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
-				$passwordPlain = '';
-				$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);	//Compatibility with bcrypt.js
+				$passwordHash = self::hashPassword($passwordPlain);
 				$ok &= ($passwordHash != '');
 				$ok &= ($passwordHash != '');
 				FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash;
 				FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash;
 			}
 			}
@@ -99,6 +99,50 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		$this->view->size_user = $entryDAO->size();
 		$this->view->size_user = $entryDAO->size();
 	}
 	}
 
 
+	public static function createUser($new_user_name, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) {
+		if (!is_array($userConfig)) {
+			$userConfig = array();
+		}
+
+		$ok = ($new_user_name != '') && ctype_alnum($new_user_name);
+
+		if ($ok) {
+			$languages = Minz_Translate::availableLanguages();
+			if (empty($userConfig['language']) || !in_array($userConfig['language'], $languages)) {
+				$userConfig['language'] = 'en';
+			}
+
+			$ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers()));	//Not an existing user, case-insensitive
+
+			$configPath = join_path(DATA_PATH, 'users', $new_user_name, 'config.php');
+			$ok &= !file_exists($configPath);
+		}
+		if ($ok) {
+			$passwordHash = '';
+			if ($passwordPlain != '') {
+				$passwordHash = self::hashPassword($passwordPlain);
+				$ok &= ($passwordHash != '');
+			}
+
+			$apiPasswordHash = '';
+			if ($apiPasswordPlain != '') {
+				$apiPasswordHash = self::hashPassword($apiPasswordPlain);
+				$ok &= ($apiPasswordHash != '');
+			}
+		}
+		if ($ok) {
+			mkdir(join_path(DATA_PATH, 'users', $new_user_name));
+			$userConfig['passwordHash'] = $passwordHash;
+			$userConfig['apiPasswordHash'] = $apiPasswordHash;
+			$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
+		}
+		if ($ok) {
+			$userDAO = new FreshRSS_UserDAO();
+			$ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds);
+		}
+		return $ok;
+	}
+
 	/**
 	/**
 	 * This action creates a new user.
 	 * This action creates a new user.
 	 *
 	 *
@@ -116,57 +160,13 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 				FreshRSS_Auth::hasAccess('admin') ||
 				FreshRSS_Auth::hasAccess('admin') ||
 				!max_registrations_reached()
 				!max_registrations_reached()
 		)) {
 		)) {
-			$db = FreshRSS_Context::$system_conf->db;
-			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
-
-			$new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language);
-			$languages = Minz_Translate::availableLanguages();
-			if (!isset($languages[$new_user_language])) {
-				$new_user_language = FreshRSS_Context::$user_conf->language;
-			}
-
 			$new_user_name = Minz_Request::param('new_user_name');
 			$new_user_name = Minz_Request::param('new_user_name');
-			$ok = ($new_user_name != '') && ctype_alnum($new_user_name);
-
-			if ($ok) {
-				$default_user = FreshRSS_Context::$system_conf->default_user;
-				$ok &= (strcasecmp($new_user_name, $default_user) !== 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
+			$passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
+			$new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language);
 
 
-				$configPath = join_path(DATA_PATH, 'users', $new_user_name, 'config.php');
-				$ok &= !file_exists($configPath);
-			}
-			if ($ok) {
-				$passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
-				$passwordHash = '';
-				if ($passwordPlain != '') {
-					Minz_Request::_param('new_user_passwordPlain');	//Discard plain-text password ASAP
-					$_POST['new_user_passwordPlain'] = '';
-					if (!function_exists('password_hash')) {
-						include_once(LIB_PATH . '/password_compat.php');
-					}
-					$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
-					$passwordPlain = '';
-					$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);	//Compatibility with bcrypt.js
-					$ok &= ($passwordHash != '');
-				}
-				if (empty($passwordHash)) {
-					$passwordHash = '';
-				}
-			}
-			if ($ok) {
-				mkdir(join_path(DATA_PATH, 'users', $new_user_name));
-				$config_array = array(
-					'language' => $new_user_language,
-					'passwordHash' => $passwordHash,
-				);
-				$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);
-			}
+			$ok = self::createUser($new_user_name, $passwordPlain, '', array('language' => $new_user_language));
+			Minz_Request::_param('new_user_passwordPlain');	//Discard plain-text password ASAP
+			$_POST['new_user_passwordPlain'] = '';
 			invalidateHttpCache();
 			invalidateHttpCache();
 
 
 			$notif = array(
 			$notif = array(
@@ -183,6 +183,27 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		Minz_Request::forward($redirect_url, true);
 		Minz_Request::forward($redirect_url, true);
 	}
 	}
 
 
+	public static function deleteUser($username) {
+		$db = FreshRSS_Context::$system_conf->db;
+		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+
+		$ok = ctype_alnum($username);
+		if ($ok) {
+			$default_user = FreshRSS_Context::$system_conf->default_user;
+			$ok &= (strcasecmp($username, $default_user) !== 0);	//It is forbidden to delete the default user
+		}
+		$user_data = join_path(DATA_PATH, 'users', $username);
+		if ($ok) {
+			$ok &= is_dir($user_data);
+		}
+		if ($ok) {
+			$userDAO = new FreshRSS_UserDAO();
+			$ok &= $userDAO->deleteUser($username);
+			$ok &= recursive_unlink($user_data);
+		}
+		return $ok;
+	}
+
 	/**
 	/**
 	 * This action delete an existing user.
 	 * This action delete an existing user.
 	 *
 	 *
@@ -204,16 +225,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 				FreshRSS_Auth::hasAccess('admin') ||
 				FreshRSS_Auth::hasAccess('admin') ||
 				$self_deletion
 				$self_deletion
 		)) {
 		)) {
-			$db = FreshRSS_Context::$system_conf->db;
-			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
-
-			$ok = ctype_alnum($username);
-			$user_data = join_path(DATA_PATH, 'users', $username);
-
-			if ($ok) {
-				$default_user = FreshRSS_Context::$system_conf->default_user;
-				$ok &= (strcasecmp($username, $default_user) !== 0);	//It is forbidden to delete the default user
-			}
+			$ok = true;
 			if ($ok && $self_deletion) {
 			if ($ok && $self_deletion) {
 				// We check the password if it's a self-destruction
 				// We check the password if it's a self-destruction
 				$nonce = Minz_Session::param('nonce');
 				$nonce = Minz_Session::param('nonce');
@@ -225,12 +237,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 				);
 				);
 			}
 			}
 			if ($ok) {
 			if ($ok) {
-				$ok &= is_dir($user_data);
-			}
-			if ($ok) {
-				$userDAO = new FreshRSS_UserDAO();
-				$ok &= $userDAO->deleteUser($username);
-				$ok &= recursive_unlink($user_data);
+				$ok &= self::deleteUser($username);
 			}
 			}
 			if ($ok && $self_deletion) {
 			if ($ok && $self_deletion) {
 				FreshRSS_Auth::removeAccess();
 				FreshRSS_Auth::removeAccess();

+ 14 - 0
app/Exceptions/AlreadySubscribedException.php

@@ -0,0 +1,14 @@
+<?php
+
+class FreshRSS_AlreadySubscribed_Exception extends Exception {
+	private $feedName = '';
+
+	public function __construct($url, $feedName) {
+		parent::__construct('Already subscribed! ' . $url, 2135);
+		$this->feedName = $feedName;
+	}
+
+	public function feedName() {
+		return $this->feedName;
+	}
+}

+ 14 - 0
app/Exceptions/FeedNotAddedException.php

@@ -0,0 +1,14 @@
+<?php
+
+class FreshRSS_FeedNotAdded_Exception extends Exception {
+	private $feedName = '';
+
+	public function __construct($url, $feedName) {
+		parent::__construct('Feed not added! ' . $url, 2147);
+		$this->feedName = $feedName;
+	}
+
+	public function feedName() {
+		return $this->feedName;
+	}
+}

+ 14 - 0
app/Exceptions/ZipException.php

@@ -0,0 +1,14 @@
+<?php
+
+class FreshRSS_Zip_Exception extends Exception {
+	private $zipErrorCode = 0;
+	
+	public function __construct($zipErrorCode) {
+		parent::__construct('ZIP error! ' . $url, 2141);
+		$this->zipErrorCode = $zipErrorCode;
+	}
+
+	public function zipErrorCode() {
+		return $this->zipErrorCode;
+	}
+}

+ 4 - 0
app/Exceptions/ZipMissingException.php

@@ -0,0 +1,4 @@
+<?php
+
+class FreshRSS_ZipMissing_Exception extends Exception {
+}

+ 1 - 1
app/FreshRSS.php

@@ -49,7 +49,7 @@ class FreshRSS extends Minz_FrontController {
 		self::initI18n();
 		self::initI18n();
 		self::loadNotifications();
 		self::loadNotifications();
 		// Enable extensions for the current (logged) user.
 		// Enable extensions for the current (logged) user.
-		if (FreshRSS_Auth::hasAccess()) {
+		if (FreshRSS_Auth::hasAccess() || $system_conf->allow_anonymous) {
 			$ext_list = FreshRSS_Context::$user_conf->extensions_enabled;
 			$ext_list = FreshRSS_Context::$user_conf->extensions_enabled;
 			Minz_ExtensionManager::enableByList($ext_list);
 			Minz_ExtensionManager::enableByList($ext_list);
 		}
 		}

+ 14 - 7
app/Models/CategoryDAO.php

@@ -1,6 +1,9 @@
 <?php
 <?php
 
 
 class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
+
+	const defaultCategoryId = 1;
+
 	public function addCategory($valuesTmp) {
 	public function addCategory($valuesTmp) {
 		$sql = 'INSERT INTO `' . $this->prefix . 'category`(name) VALUES(?)';
 		$sql = 'INSERT INTO `' . $this->prefix . 'category`(name) VALUES(?)';
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
@@ -10,7 +13,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		);
 		);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
-			return $this->bd->lastInsertId();
+			return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"');
 		} else {
 		} else {
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			Minz_Log::error('SQL error addCategory: ' . $info[2]);
 			Minz_Log::error('SQL error addCategory: ' . $info[2]);
@@ -50,6 +53,9 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 	}
 	}
 
 
 	public function deleteCategory($id) {
 	public function deleteCategory($id) {
+		if ($id <= self::defaultCategoryId) {
+			return false;
+		}
 		$sql = 'DELETE FROM `' . $this->prefix . 'category` WHERE id=?';
 		$sql = 'DELETE FROM `' . $this->prefix . 'category` WHERE id=?';
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 
 
@@ -100,7 +106,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 	public function listCategories($prePopulateFeeds = true, $details = false) {
 	public function listCategories($prePopulateFeeds = true, $details = false) {
 		if ($prePopulateFeeds) {
 		if ($prePopulateFeeds) {
 			$sql = 'SELECT c.id AS c_id, c.name AS c_name, '
 			$sql = 'SELECT c.id AS c_id, c.name AS c_name, '
-			     . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.cache_nbEntries, f.cache_nbUnreads ')
+			     . ($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 '
 			     . 'FROM `' . $this->prefix . 'category` c '
 			     . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id '
 			     . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id '
 			     . 'GROUP BY f.id, c_id '
 			     . 'GROUP BY f.id, c_id '
@@ -117,7 +123,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 	}
 	}
 
 
 	public function getDefault() {
 	public function getDefault() {
-		$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=1';
+		$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=' . self::defaultCategoryId;
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 
 
 		$stm->execute();
 		$stm->execute();
@@ -131,11 +137,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		}
 		}
 	}
 	}
 	public function checkDefault() {
 	public function checkDefault() {
-		$def_cat = $this->searchById(1);
+		$def_cat = $this->searchById(self::defaultCategoryId);
 
 
 		if ($def_cat == null) {
 		if ($def_cat == null) {
 			$cat = new FreshRSS_Category(_t('gen.short.default_category'));
 			$cat = new FreshRSS_Category(_t('gen.short.default_category'));
-			$cat->_id(1);
+			$cat->_id(self::defaultCategoryId);
 
 
 			$values = array(
 			$values = array(
 				'id' => $cat->id(),
 				'id' => $cat->id(),
@@ -207,12 +213,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 
 
 		$previousLine = null;
 		$previousLine = null;
 		$feedsDao = array();
 		$feedsDao = array();
+		$feedDao = FreshRSS_Factory::createFeedDAO();
 		foreach ($listDAO as $line) {
 		foreach ($listDAO as $line) {
 			if ($previousLine['c_id'] != null && $line['c_id'] !== $previousLine['c_id']) {
 			if ($previousLine['c_id'] != null && $line['c_id'] !== $previousLine['c_id']) {
 				// End of the current category, we add it to the $list
 				// End of the current category, we add it to the $list
 				$cat = new FreshRSS_Category(
 				$cat = new FreshRSS_Category(
 					$previousLine['c_name'],
 					$previousLine['c_name'],
-					FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id'])
+					$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
 				);
 				);
 				$cat->_id($previousLine['c_id']);
 				$cat->_id($previousLine['c_id']);
 				$list[$previousLine['c_id']] = $cat;
 				$list[$previousLine['c_id']] = $cat;
@@ -228,7 +235,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		if ($previousLine != null) {
 		if ($previousLine != null) {
 			$cat = new FreshRSS_Category(
 			$cat = new FreshRSS_Category(
 				$previousLine['c_name'],
 				$previousLine['c_name'],
-				FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id'])
+				$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
 			);
 			);
 			$cat->_id($previousLine['c_id']);
 			$cat->_id($previousLine['c_id']);
 			$list[$previousLine['c_id']] = $cat;
 			$list[$previousLine['c_id']] = $cat;

+ 1 - 0
app/Models/ConfigurationSetter.php

@@ -282,6 +282,7 @@ class FreshRSS_ConfigurationSetter {
 
 
 		switch ($value['type']) {
 		switch ($value['type']) {
 		case 'mysql':
 		case 'mysql':
+		case 'pgsql':
 			if (empty($value['host']) ||
 			if (empty($value['host']) ||
 					empty($value['user']) ||
 					empty($value['user']) ||
 					empty($value['base']) ||
 					empty($value['base']) ||

+ 16 - 4
app/Models/Context.php

@@ -35,6 +35,9 @@ class FreshRSS_Context {
 	public static $first_id = '';
 	public static $first_id = '';
 	public static $next_id = '';
 	public static $next_id = '';
 	public static $id_max = '';
 	public static $id_max = '';
+	public static $sinceHours = 0;
+
+	public static $isCli = false;
 
 
 	/**
 	/**
 	 * Initialize the context.
 	 * Initialize the context.
@@ -45,9 +48,6 @@ class FreshRSS_Context {
 		// Init configuration.
 		// Init configuration.
 		self::$system_conf = Minz_Configuration::get('system');
 		self::$system_conf = Minz_Configuration::get('system');
 		self::$user_conf = Minz_Configuration::get('user');
 		self::$user_conf = Minz_Configuration::get('user');
-
-		$catDAO = new FreshRSS_CategoryDAO();
-		self::$categories = $catDAO->listCategories();
 	}
 	}
 
 
 	/**
 	/**
@@ -139,15 +139,22 @@ class FreshRSS_Context {
 		$id = substr($get, 2);
 		$id = substr($get, 2);
 		$nb_unread = 0;
 		$nb_unread = 0;
 
 
+		if (empty(self::$categories)) {
+			$catDAO = new FreshRSS_CategoryDAO();
+			self::$categories = $catDAO->listCategories();
+		}
+
 		switch($type) {
 		switch($type) {
 		case 'a':
 		case 'a':
 			self::$current_get['all'] = true;
 			self::$current_get['all'] = true;
 			self::$name = _t('index.feed.title');
 			self::$name = _t('index.feed.title');
+			self::$description = self::$system_conf->meta_description;
 			self::$get_unread = self::$total_unread;
 			self::$get_unread = self::$total_unread;
 			break;
 			break;
 		case 's':
 		case 's':
 			self::$current_get['starred'] = true;
 			self::$current_get['starred'] = true;
 			self::$name = _t('index.feed.title_fav');
 			self::$name = _t('index.feed.title_fav');
+			self::$description = self::$system_conf->meta_description;
 			self::$get_unread = self::$total_starred['unread'];
 			self::$get_unread = self::$total_starred['unread'];
 
 
 			// Update state if favorite is not yet enabled.
 			// Update state if favorite is not yet enabled.
@@ -198,11 +205,16 @@ class FreshRSS_Context {
 	/**
 	/**
 	 * Set the value of $next_get attribute.
 	 * Set the value of $next_get attribute.
 	 */
 	 */
-	public static function _nextGet() {
+	private static function _nextGet() {
 		$get = self::currentGet();
 		$get = self::currentGet();
 		// By default, $next_get == $get
 		// By default, $next_get == $get
 		self::$next_get = $get;
 		self::$next_get = $get;
 
 
+		if (empty(self::$categories)) {
+			$catDAO = new FreshRSS_CategoryDAO();
+			self::$categories = $catDAO->listCategories();
+		}
+
 		if (self::$user_conf->onread_jump_next && strlen($get) > 2) {
 		if (self::$user_conf->onread_jump_next && strlen($get) > 2) {
 			$another_unread_id = '';
 			$another_unread_id = '';
 			$found_current_get = false;
 			$found_current_get = false;

+ 43 - 0
app/Models/DatabaseDAOPGSQL.php

@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * This class is used to test database is well-constructed.
+ */
+class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO {
+	public function tablesAreCorrect() {
+		$db = FreshRSS_Context::$system_conf->db;
+		$dbowner = $db['user'];
+		$sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=?';
+		$stm = $this->bd->prepare($sql);
+		$values = array($dbowner);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		
+		$tables = array(
+			$this->prefix . 'category' => false,
+			$this->prefix . 'feed' => false,
+			$this->prefix . 'entry' => false,
+		);
+		foreach ($res as $value) {
+			$tables[array_pop($value)] = true;
+		}
+
+		return count(array_keys($tables, true, true)) == count($tables);
+	}
+
+	public function getSchema($table) {
+		$sql = 'select column_name as field, data_type as type, column_default as default, is_nullable as null from INFORMATION_SCHEMA.COLUMNS where table_name = ?';
+		$stm = $this->bd->prepare($sql);
+		$stm->execute(array($this->prefix . $table));
+		return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
+	}
+
+	public function daoToSchema($dao) {
+		return array(
+			'name' => $dao['field'],
+			'type' => strtolower($dao['type']),
+			'notnull' => (bool)$dao['null'],
+			'default' => $dao['default'],
+		);
+	}
+}

+ 103 - 65
app/Models/EntryDAO.php

@@ -3,13 +3,21 @@
 class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 	public function isCompressed() {
 	public function isCompressed() {
-		return parent::$sharedDbType !== 'sqlite';
+		return parent::$sharedDbType === 'mysql';
 	}
 	}
 
 
 	public function hasNativeHex() {
 	public function hasNativeHex() {
 		return parent::$sharedDbType !== 'sqlite';
 		return parent::$sharedDbType !== 'sqlite';
 	}
 	}
 
 
+	public function sqlHexDecode($x) {
+		return 'unhex(' . $x . ')';
+	}
+
+	public function sqlHexEncode($x) {
+		return 'hex(' . $x . ')';
+	}
+
 	protected function addColumn($name) {
 	protected function addColumn($name) {
 		Minz_Log::warning('FreshRSS_EntryDAO::addColumn: ' . $name);
 		Minz_Log::warning('FreshRSS_EntryDAO::addColumn: ' . $name);
 		$hasTransaction = false;
 		$hasTransaction = false;
@@ -20,7 +28,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 					$this->bd->beginTransaction();
 					$this->bd->beginTransaction();
 					$hasTransaction = true;
 					$hasTransaction = true;
 				}
 				}
-				$stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN lastSeen INT(11) DEFAULT 0');
+				$stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN `lastSeen` INT(11) DEFAULT 0');
 				if ($stm && $stm->execute()) {
 				if ($stm && $stm->execute()) {
 					$stm = $this->bd->prepare('CREATE INDEX entry_lastSeen_index ON `' . $this->prefix . 'entry`(`lastSeen`);');	//"IF NOT EXISTS" does not exist in MySQL 5.7
 					$stm = $this->bd->prepare('CREATE INDEX entry_lastSeen_index ON `' . $this->prefix . 'entry`(`lastSeen`);');	//"IF NOT EXISTS" does not exist in MySQL 5.7
 					if ($stm && $stm->execute()) {
 					if ($stm && $stm->execute()) {
@@ -105,32 +113,45 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if ($this->addEntryPrepared === null) {
 		if ($this->addEntryPrepared === null) {
 			$sql = 'INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, '
 			$sql = 'INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, '
 			     . ($this->isCompressed() ? 'content_bin' : 'content')
 			     . ($this->isCompressed() ? 'content_bin' : 'content')
-			     . ', link, date, lastSeen, hash, is_read, is_favorite, id_feed, tags) '
-			     . 'VALUES(?, ?, ?, ?, '
-			     . ($this->isCompressed() ? 'COMPRESS(?)' : '?')
-			     . ', ?, ?, ?, '
-			     . ($this->hasNativeHex() ? 'X?' : '?')
-			     . ', ?, ?, ?, ?)';
+			     . ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) '
+			     . 'VALUES(:id, :guid, :title, :author, '
+			     . ($this->isCompressed() ? 'COMPRESS(:content)' : ':content')
+			     . ', :link, :date, :last_seen, '
+			     . $this->sqlHexDecode(':hash')
+			     . ', :is_read, :is_favorite, :id_feed, :tags)';
 			$this->addEntryPrepared = $this->bd->prepare($sql);
 			$this->addEntryPrepared = $this->bd->prepare($sql);
 		}
 		}
+		$this->addEntryPrepared->bindParam(':id', $valuesTmp['id']);
+		$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
+		$valuesTmp['guid'] = safe_ascii($valuesTmp['guid']);
+		$this->addEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
+		$valuesTmp['title'] = substr($valuesTmp['title'], 0, 255);
+		$this->addEntryPrepared->bindParam(':title', $valuesTmp['title']);
+		$valuesTmp['author'] = substr($valuesTmp['author'], 0, 255);
+		$this->addEntryPrepared->bindParam(':author', $valuesTmp['author']);
+		$this->addEntryPrepared->bindParam(':content', $valuesTmp['content']);
+		$valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
+		$valuesTmp['link'] = safe_ascii($valuesTmp['link']);
+		$this->addEntryPrepared->bindParam(':link', $valuesTmp['link']);
+		$this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
+		$valuesTmp['lastSeen'] = time();
+		$this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
+		$valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0;
+		$this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT);
+		$valuesTmp['is_favorite'] = $valuesTmp['is_favorite'] ? 1 : 0;
+		$this->addEntryPrepared->bindParam(':is_favorite', $valuesTmp['is_favorite'], PDO::PARAM_INT);
+		$this->addEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
+		$valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023);
+		$this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
+
+		if ($this->hasNativeHex()) {
+			$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
+		} else {
+			$valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']);	//hex2bin() is PHP5.4+
+			$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
+		}
 
 
-		$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'],
-			time(),
-			$this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']),	// X'09AF' hexadecimal literals do not work with SQLite/PDO	//hex2bin() is PHP5.4+
-			$valuesTmp['is_read'] ? 1 : 0,
-			$valuesTmp['is_favorite'] ? 1 : 0,
-			$valuesTmp['id_feed'],
-			substr($valuesTmp['tags'], 0, 1023),
-		);
-
-		if ($this->addEntryPrepared && $this->addEntryPrepared->execute($values)) {
+		if ($this->addEntryPrepared && $this->addEntryPrepared->execute()) {
 			return $this->bd->lastInsertId();
 			return $this->bd->lastInsertId();
 		} else {
 		} else {
 			$info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo();
 			$info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo();
@@ -153,35 +174,44 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 		if ($this->updateEntryPrepared === null) {
 		if ($this->updateEntryPrepared === null) {
 			$sql = 'UPDATE `' . $this->prefix . 'entry` '
 			$sql = 'UPDATE `' . $this->prefix . 'entry` '
-			     . 'SET title=?, author=?, '
-			     . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?')
-			     . ', link=?, date=?, lastSeen=?, hash='
-			     . ($this->hasNativeHex() ? 'X?' : '?')
-			     . ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=?, ')
-			     . 'tags=? '
-			     . 'WHERE id_feed=? AND guid=?';
+			     . 'SET title=:title, author=:author, '
+			     . ($this->isCompressed() ? 'content_bin=COMPRESS(:content)' : 'content=:content')
+			     . ', link=:link, date=:date, `lastSeen`=:last_seen, '
+			     . 'hash=' . $this->sqlHexDecode(':hash')
+			     . ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=:is_read, ')
+			     . 'tags=:tags '
+			     . 'WHERE id_feed=:id_feed AND guid=:guid';
 			$this->updateEntryPrepared = $this->bd->prepare($sql);
 			$this->updateEntryPrepared = $this->bd->prepare($sql);
 		}
 		}
 
 
-		$values = array(
-			substr($valuesTmp['title'], 0, 255),
-			substr($valuesTmp['author'], 0, 255),
-			$valuesTmp['content'],
-			substr($valuesTmp['link'], 0, 1023),
-			$valuesTmp['date'],
-			time(),
-			$this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']),
-		);
+		$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
+		$this->updateEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
+		$valuesTmp['title'] = substr($valuesTmp['title'], 0, 255);
+		$this->updateEntryPrepared->bindParam(':title', $valuesTmp['title']);
+		$valuesTmp['author'] = substr($valuesTmp['author'], 0, 255);
+		$this->updateEntryPrepared->bindParam(':author', $valuesTmp['author']);
+		$this->updateEntryPrepared->bindParam(':content', $valuesTmp['content']);
+		$valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
+		$valuesTmp['link'] = safe_ascii($valuesTmp['link']);
+		$this->updateEntryPrepared->bindParam(':link', $valuesTmp['link']);
+		$this->updateEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
+		$valuesTmp['lastSeen'] = time();
+		$this->updateEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
 		if ($valuesTmp['is_read'] !== null) {
 		if ($valuesTmp['is_read'] !== null) {
-			$values[] = $valuesTmp['is_read'] ? 1 : 0;
+			$this->updateEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'] ? 1 : 0, PDO::PARAM_INT);
+		}
+		$this->updateEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
+		$valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023);
+		$this->updateEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
+
+		if ($this->hasNativeHex()) {
+			$this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
+		} else {
+			$valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']);	//hex2bin() is PHP5.4+
+			$this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
 		}
 		}
-		$values = array_merge($values, array(
-			substr($valuesTmp['tags'], 0, 1023),
-			$valuesTmp['id_feed'],
-			substr($valuesTmp['guid'], 0, 760),
-		));
 
 
-		if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute($values)) {
+		if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute()) {
 			return $this->bd->lastInsertId();
 			return $this->bd->lastInsertId();
 		} else {
 		} else {
 			$info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo();
 			$info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo();
@@ -246,15 +276,19 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		 .	'WHERE e.is_read=0 '
 		 .	'WHERE e.is_read=0 '
 		 .	'GROUP BY e.id_feed'
 		 .	'GROUP BY e.id_feed'
 		 . ') x ON x.id_feed=f.id '
 		 . ') x ON x.id_feed=f.id '
-		 . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) '
-		 . 'WHERE 1';
+		 . 'SET f.`cache_nbUnreads`=COALESCE(x.nbUnreads, 0)';
+		$hasWhere = false;
 		$values = array();
 		$values = array();
 		if ($feedId !== false) {
 		if ($feedId !== false) {
-			$sql .= ' AND f.id=?';
+			$sql .= $hasWhere ? ' AND' : ' WHERE';
+			$hasWhere = true;
+			$sql .= ' f.id=?';
 			$values[] = $id;
 			$values[] = $id;
 		}
 		}
 		if ($catId !== false) {
 		if ($catId !== false) {
-			$sql .= ' AND f.category=?';
+			$sql .= $hasWhere ? ' AND' : ' WHERE';
+			$hasWhere = true;
+			$sql .= ' f.category=?';
 			$values[] = $catId;
 			$values[] = $catId;
 		}
 		}
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
@@ -309,7 +343,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		} else {
 		} else {
 			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
 			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
 				 . 'SET e.is_read=?,'
 				 . 'SET e.is_read=?,'
-				 . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
+				 . 'f.`cache_nbUnreads`=f.`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
 				 . 'WHERE e.id=? AND e.is_read=?';
 				 . 'WHERE e.id=? AND e.is_read=?';
 			$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
 			$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
 			$stm = $this->bd->prepare($sql);
 			$stm = $this->bd->prepare($sql);
@@ -430,17 +464,17 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		}
 		$this->bd->beginTransaction();
 		$this->bd->beginTransaction();
 
 
-		$sql = 'UPDATE `' . $this->prefix . 'entry` e '
-			 . 'SET e.is_read=1 '
-			 . 'WHERE e.id_feed=? AND e.is_read=0 AND e.id <= ?';
+		$sql = 'UPDATE `' . $this->prefix . 'entry` '
+			 . 'SET is_read=1 '
+			 . 'WHERE id_feed=? AND is_read=0 AND id <= ?';
 		$values = array($id_feed, $idMax);
 		$values = array($id_feed, $idMax);
 
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
 
 
 		$stm = $this->bd->prepare($sql . $search);
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::error('SQL error markReadFeed: ' . $info[2]);
+			Minz_Log::error('SQL error markReadFeed: ' . $info[2] . ' with SQL: ' . $sql . $search);
 			$this->bd->rollBack();
 			$this->bd->rollBack();
 			return false;
 			return false;
 		}
 		}
@@ -448,13 +482,13 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 		if ($affected > 0) {
 		if ($affected > 0) {
 			$sql = 'UPDATE `' . $this->prefix . 'feed` '
 			$sql = 'UPDATE `' . $this->prefix . 'feed` '
-				 . 'SET cache_nbUnreads=cache_nbUnreads-' . $affected
+				 . 'SET `cache_nbUnreads`=`cache_nbUnreads`-' . $affected
 				 . ' WHERE id=?';
 				 . ' WHERE id=?';
 			$values = array($id_feed);
 			$values = array($id_feed);
 			$stm = $this->bd->prepare($sql);
 			$stm = $this->bd->prepare($sql);
 			if (!($stm && $stm->execute($values))) {
 			if (!($stm && $stm->execute($values))) {
 				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-				Minz_Log::error('SQL error markReadFeed: ' . $info[2]);
+				Minz_Log::error('SQL error markReadFeed cache: ' . $info[2]);
 				$this->bd->rollBack();
 				$this->bd->rollBack();
 				return false;
 				return false;
 			}
 			}
@@ -658,7 +692,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if (count($guids) < 1) {
 		if (count($guids) < 1) {
 			return array();
 			return array();
 		}
 		}
-		$sql = 'SELECT guid, hex(hash) AS hexHash FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
+		$guids = array_unique($guids);
+		$sql = 'SELECT guid, ' . $this->sqlHexEncode('hash') . ' AS hex_hash FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 		$values = array($id_feed);
 		$values = array($id_feed);
 		$values = array_merge($values, $guids);
 		$values = array_merge($values, $guids);
@@ -666,7 +701,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$result = array();
 			$result = array();
 			$rows = $stm->fetchAll(PDO::FETCH_ASSOC);
 			$rows = $stm->fetchAll(PDO::FETCH_ASSOC);
 			foreach ($rows as $row) {
 			foreach ($rows as $row) {
-				$result[$row['guid']] = $row['hexHash'];
+				$result[$row['guid']] = $row['hex_hash'];
 			}
 			}
 			return $result;
 			return $result;
 		} else {
 		} else {
@@ -680,13 +715,16 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		}
 	}
 	}
 
 
-	public function updateLastSeen($id_feed, $guids) {
+	public function updateLastSeen($id_feed, $guids, $mtime = 0) {
 		if (count($guids) < 1) {
 		if (count($guids) < 1) {
 			return 0;
 			return 0;
 		}
 		}
-		$sql = 'UPDATE `' . $this->prefix . 'entry` SET lastSeen=? WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
+		$sql = 'UPDATE `' . $this->prefix . 'entry` SET `lastSeen`=? WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
-		$values = array(time(), $id_feed);
+		if ($mtime <= 0) {
+			$mtime = time();
+		}
+		$values = array($mtime, $id_feed);
 		$values = array_merge($values, $guids);
 		$values = array_merge($values, $guids);
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 			return $stm->rowCount();

+ 31 - 0
app/Models/EntryDAOPGSQL.php

@@ -0,0 +1,31 @@
+<?php
+
+class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
+
+	public function sqlHexDecode($x) {
+		return 'decode(' . $x . ", 'hex')";
+	}
+
+	public function sqlHexEncode($x) {
+		return 'encode(' . $x . ", 'hex')";
+	}
+
+	protected function autoUpdateDb($errorInfo) {
+		return false;
+	}
+
+	protected function addColumn($name) {
+		return false;
+	}
+
+	public function size($all = true) {
+		$db = FreshRSS_Context::$system_conf->db;
+		$sql = 'SELECT pg_size_pretty(pg_database_size(?))';
+		$values = array($db['base']);
+		$stm = $this->bd->prepare($sql);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+		return $res[0];
+	}
+
+}

+ 14 - 6
app/Models/EntryDAOSQLite.php

@@ -2,6 +2,10 @@
 
 
 class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 
 
+	public function sqlHexDecode($x) {
+		return $x;
+	}
+
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
 		if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') {	//ER_BAD_FIELD_ERROR
 		if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') {	//ER_BAD_FIELD_ERROR
 			//autoAddColumn
 			//autoAddColumn
@@ -24,17 +28,21 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 
 
 	protected function updateCacheUnreads($catId = false, $feedId = false) {
 	protected function updateCacheUnreads($catId = false, $feedId = false) {
 		$sql = 'UPDATE `' . $this->prefix . 'feed` '
 		$sql = 'UPDATE `' . $this->prefix . 'feed` '
-		 . 'SET cache_nbUnreads=('
+		 . 'SET `cache_nbUnreads`=('
 		 .	'SELECT COUNT(*) AS nbUnreads FROM `' . $this->prefix . 'entry` e '
 		 .	'SELECT COUNT(*) AS nbUnreads FROM `' . $this->prefix . 'entry` e '
-		 .	'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0) '
-		 . 'WHERE 1';
+		 .	'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0)';
+		$hasWhere = false;
 		$values = array();
 		$values = array();
 		if ($feedId !== false) {
 		if ($feedId !== false) {
-			$sql .= ' AND id=?';
+			$sql .= $hasWhere ? ' AND' : ' WHERE';
+			$hasWhere = true;
+			$sql .= ' id=?';
 			$values[] = $feedId;
 			$values[] = $feedId;
 		}
 		}
 		if ($catId !== false) {
 		if ($catId !== false) {
-			$sql .= ' AND category=?';
+			$sql .= $hasWhere ? ' AND' : ' WHERE';
+			$hasWhere = true;
+			$sql .= ' category=?';
 			$values[] = $catId;
 			$values[] = $catId;
 		}
 		}
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
@@ -82,7 +90,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 			}
 			}
 			$affected = $stm->rowCount();
 			$affected = $stm->rowCount();
 			if ($affected > 0) {
 			if ($affected > 0) {
-				$sql = 'UPDATE `' . $this->prefix . 'feed` SET cache_nbUnreads=cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
+				$sql = 'UPDATE `' . $this->prefix . 'feed` SET `cache_nbUnreads`=`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
 				 . 'WHERE id=(SELECT e.id_feed FROM `' . $this->prefix . 'entry` e WHERE e.id=?)';
 				 . 'WHERE id=(SELECT e.id_feed FROM `' . $this->prefix . 'entry` e WHERE e.id=?)';
 				$values = array($ids);
 				$values = array($ids);
 				$stm = $this->bd->prepare($sql);
 				$stm = $this->bd->prepare($sql);

+ 26 - 16
app/Models/Factory.php

@@ -4,37 +4,47 @@ class FreshRSS_Factory {
 
 
 	public static function createFeedDao($username = null) {
 	public static function createFeedDao($username = null) {
 		$conf = Minz_Configuration::get('system');
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_FeedDAOSQLite($username);
-		} else {
-			return new FreshRSS_FeedDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_FeedDAOSQLite($username);
+			default:
+				return new FreshRSS_FeedDAO($username);
 		}
 		}
 	}
 	}
 
 
 	public static function createEntryDao($username = null) {
 	public static function createEntryDao($username = null) {
 		$conf = Minz_Configuration::get('system');
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_EntryDAOSQLite($username);
-		} else {
-			return new FreshRSS_EntryDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_EntryDAOSQLite($username);
+			case 'pgsql':
+				return new FreshRSS_EntryDAOPGSQL($username);
+			default:
+				return new FreshRSS_EntryDAO($username);
 		}
 		}
 	}
 	}
 
 
 	public static function createStatsDAO($username = null) {
 	public static function createStatsDAO($username = null) {
 		$conf = Minz_Configuration::get('system');
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_StatsDAOSQLite($username);
-		} else {
-			return new FreshRSS_StatsDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_StatsDAOSQLite($username);
+			case 'pgsql':
+				return new FreshRSS_StatsDAOPGSQL($username);
+			default:
+				return new FreshRSS_StatsDAO($username);
 		}
 		}
 	}
 	}
 
 
 	public static function createDatabaseDAO($username = null) {
 	public static function createDatabaseDAO($username = null) {
 		$conf = Minz_Configuration::get('system');
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_DatabaseDAOSQLite($username);
-		} else {
-			return new FreshRSS_DatabaseDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_DatabaseDAOSQLite($username);
+			case 'pgsql':
+				return new FreshRSS_DatabaseDAOPGSQL($username);
+			default:
+				return new FreshRSS_DatabaseDAO($username);
 		}
 		}
 	}
 	}
 
 

+ 29 - 12
app/Models/Feed.php

@@ -131,13 +131,26 @@ class FreshRSS_Feed extends Minz_Model {
 		return $this->nbNotRead;
 		return $this->nbNotRead;
 	}
 	}
 	public function faviconPrepare() {
 	public function faviconPrepare() {
-		$file = DATA_PATH . '/favicons/' . $this->hash() . '.txt';
-		if (!file_exists($file)) {
-			$t = $this->website;
-			if ($t == '') {
-				$t = $this->url;
+		global $favicons_dir;
+		require_once(LIB_PATH . '/favicons.php');
+		$url = $this->website;
+		if ($url == '') {
+			$url = $this->url;
+		}
+		$txt = $favicons_dir . $this->hash() . '.txt';
+		if (!file_exists($txt)) {
+			file_put_contents($txt, $url);
+		}
+		if (FreshRSS_Context::$isCli) {
+			$ico = $favicons_dir . $this->hash() . '.ico';
+			$ico_mtime = @filemtime($ico);
+			$txt_mtime = @filemtime($txt);
+			if ($txt_mtime != false &&
+				($ico_mtime == false || $ico_mtime < $txt_mtime || ($ico_mtime < time() - (14 * 86400)))) {
+				// no ico file or we should download a new one.
+				$url = file_get_contents($txt);
+				download_favicon($url, $ico) || touch($ico);
 			}
 			}
-			file_put_contents($file, $t);
 		}
 		}
 	}
 	}
 	public static function faviconDelete($hash) {
 	public static function faviconDelete($hash) {
@@ -216,7 +229,7 @@ class FreshRSS_Feed extends Minz_Model {
 		$this->nbEntries = intval($value);
 		$this->nbEntries = intval($value);
 	}
 	}
 
 
-	public function load($loadDetails = false) {
+	public function load($loadDetails = false, $noCache = false) {
 		if ($this->url !== null) {
 		if ($this->url !== null) {
 			if (CACHE_PATH === false) {
 			if (CACHE_PATH === false) {
 				throw new Minz_FileNotExistException(
 				throw new Minz_FileNotExistException(
@@ -268,7 +281,7 @@ class FreshRSS_Feed extends Minz_Model {
 					$this->_url($clean_url);
 					$this->_url($clean_url);
 				}
 				}
 
 
-				if (($mtime === true) || ($mtime > $this->lastUpdate)) {
+				if (($mtime === true) || ($mtime > $this->lastUpdate) || $noCache) {
 					//Minz_Log::debug('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url);
 					//Minz_Log::debug('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url);
 					$this->loadEntries($feed);	// et on charge les articles du flux
 					$this->loadEntries($feed);	// et on charge les articles du flux
 				} else {
 				} else {
@@ -309,11 +322,11 @@ class FreshRSS_Feed extends Minz_Model {
 					$elinks[$elink] = '1';
 					$elinks[$elink] = '1';
 					$mime = strtolower($enclosure->get_type());
 					$mime = strtolower($enclosure->get_type());
 					if (strpos($mime, 'image/') === 0) {
 					if (strpos($mime, 'image/') === 0) {
-						$content .= '<br /><img lazyload="" postpone="" src="' . $elink . '" alt="" />';
+						$content .= '<p class="enclosure"><img src="' . $elink . '" alt="" /></p>';
 					} elseif (strpos($mime, 'audio/') === 0) {
 					} elseif (strpos($mime, 'audio/') === 0) {
-						$content .= '<br /><audio lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />';
+						$content .= '<p class="enclosure"><audio preload="none" src="' . $elink . '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
 					} elseif (strpos($mime, 'video/') === 0) {
 					} elseif (strpos($mime, 'video/') === 0) {
-						$content .= '<br /><video lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />';
+						$content .= '<p class="enclosure"><video preload="none" src="' . $elink . '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
 					} else {
 					} else {
 						unset($elinks[$elink]);
 						unset($elinks[$elink]);
 					}
 					}
@@ -340,6 +353,10 @@ class FreshRSS_Feed extends Minz_Model {
 		$this->entries = $entries;
 		$this->entries = $entries;
 	}
 	}
 
 
+	function cacheModifiedTime() {
+		return @filemtime(CACHE_PATH . '/' . md5($this->url) . '.spc');
+	}
+
 	function lock() {
 	function lock() {
 		$this->lockPath = TMP_PATH . '/' . $this->hash() . '.freshrss.lock';
 		$this->lockPath = TMP_PATH . '/' . $this->hash() . '.freshrss.lock';
 		if (file_exists($this->lockPath) && ((time() - @filemtime($this->lockPath)) > 3600)) {
 		if (file_exists($this->lockPath) && ((time() - @filemtime($this->lockPath)) > 3600)) {
@@ -460,7 +477,7 @@ class FreshRSS_Feed extends Minz_Model {
 				CURLOPT_URL => $this->hubUrl,
 				CURLOPT_URL => $this->hubUrl,
 				CURLOPT_FOLLOWLOCATION => true,
 				CURLOPT_FOLLOWLOCATION => true,
 				CURLOPT_RETURNTRANSFER => true,
 				CURLOPT_RETURNTRANSFER => true,
-				CURLOPT_USERAGENT => _t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
+				CURLOPT_USERAGENT => 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
 				CURLOPT_POSTFIELDS => 'hub.verify=sync'
 				CURLOPT_POSTFIELDS => 'hub.verify=sync'
 					. '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe')
 					. '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe')
 					. '&hub.topic=' . urlencode($this->selfUrl)
 					. '&hub.topic=' . urlencode($this->selfUrl)

+ 39 - 18
app/Models/FeedDAO.php

@@ -2,9 +2,12 @@
 
 
 class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	public function addFeed($valuesTmp) {
 	public function addFeed($valuesTmp) {
-		$sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)';
+		$sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, `lastUpdate`, priority, `httpAuth`, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)';
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 
 
+		$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
+		$valuesTmp['website'] = safe_ascii($valuesTmp['website']);
+
 		$values = array(
 		$values = array(
 			substr($valuesTmp['url'], 0, 511),
 			substr($valuesTmp['url'], 0, 511),
 			$valuesTmp['category'],
 			$valuesTmp['category'],
@@ -16,7 +19,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		);
 		);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
-			return $this->bd->lastInsertId();
+			return $this->bd->lastInsertId('"' . $this->prefix . 'feed_id_seq"');
 		} else {
 		} else {
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			Minz_Log::error('SQL error addFeed: ' . $info[2]);
 			Minz_Log::error('SQL error addFeed: ' . $info[2]);
@@ -55,6 +58,13 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function updateFeed($id, $valuesTmp) {
 	public function updateFeed($id, $valuesTmp) {
+		if (isset($valuesTmp['url'])) {
+			$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
+		}
+		if (isset($valuesTmp['website'])) {
+			$valuesTmp['website'] = safe_ascii($valuesTmp['website']);
+		}
+
 		$set = '';
 		$set = '';
 		foreach ($valuesTmp as $key => $v) {
 		foreach ($valuesTmp as $key => $v) {
 			$set .= $key . '=?, ';
 			$set .= $key . '=?, ';
@@ -82,22 +92,26 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		}
 	}
 	}
 
 
-	public function updateLastUpdate($id, $inError = 0, $updateCache = true) {
+	public function updateLastUpdate($id, $inError = false, $updateCache = true, $mtime = 0) {
 		if ($updateCache) {
 		if ($updateCache) {
 			$sql = 'UPDATE `' . $this->prefix . 'feed` '	//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
 			$sql = 'UPDATE `' . $this->prefix . 'feed` '	//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
-			     . 'SET cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
-			     . 'cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
-			     . 'lastUpdate=?, error=? '
+			     . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+			     . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
+			     . '`lastUpdate`=?, error=? '
 			     . 'WHERE id=?';
 			     . 'WHERE id=?';
 		} else {
 		} else {
 			$sql = 'UPDATE `' . $this->prefix . 'feed` '
 			$sql = 'UPDATE `' . $this->prefix . 'feed` '
-			     . 'SET lastUpdate=?, error=? '
+			     . 'SET `lastUpdate`=?, error=? '
 			     . 'WHERE id=?';
 			     . 'WHERE id=?';
 		}
 		}
 
 
+		if ($mtime <= 0) {
+			$mtime = time();
+		}
+
 		$values = array(
 		$values = array(
-			time(),
-			$inError,
+			$mtime,
+			$inError ? 1 : 0,
 			$id,
 			$id,
 		);
 		);
 
 
@@ -198,6 +212,13 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		}
 	}
 	}
 
 
+	public function listFeedsIds() {
+		$sql = 'SELECT id FROM `' . $this->prefix . 'feed`';
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+	}
+
 	public function listFeeds() {
 	public function listFeeds() {
 		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` ORDER BY name';
 		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` ORDER BY name';
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
@@ -222,14 +243,14 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $feedCategoryNames;
 		return $feedCategoryNames;
 	}
 	}
 
 
+	/**
+	 * Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL.
+	 */
 	public function listFeedsOrderUpdate($defaultCacheDuration = 3600) {
 	public function listFeedsOrderUpdate($defaultCacheDuration = 3600) {
-		if ($defaultCacheDuration < 0) {
-			$defaultCacheDuration = 2147483647;
-		}
-		$sql = 'SELECT id, url, name, website, lastUpdate, pathEntries, httpAuth, keep_history, ttl '
+		$sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl '
 		     . 'FROM `' . $this->prefix . 'feed` '
 		     . 'FROM `' . $this->prefix . 'feed` '
-		     . 'WHERE ttl <> -1 AND lastUpdate < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) '
-		     . 'ORDER BY lastUpdate';
+		     . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl <> -1 AND `lastUpdate` < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ')
+		     . 'ORDER BY `lastUpdate`';
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 		if (!($stm && $stm->execute())) {
 		if (!($stm && $stm->execute())) {
 			$sql2 = 'ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN ttl INT NOT NULL DEFAULT -2';	//v0.7.3
 			$sql2 = 'ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN ttl INT NOT NULL DEFAULT -2';	//v0.7.3
@@ -282,7 +303,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		     .	'FROM `' . $this->prefix . 'entry` e '
 		     .	'FROM `' . $this->prefix . 'entry` e '
 		     .	'GROUP BY e.id_feed'
 		     .	'GROUP BY e.id_feed'
 		     . ') x ON x.id_feed=f.id '
 		     . ') x ON x.id_feed=f.id '
-		     . 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads';
+		     . 'SET f.`cache_nbEntries`=x.nbEntries, f.`cache_nbUnreads`=x.nbUnreads';
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 
 
 		if ($stm && $stm->execute()) {
 		if ($stm && $stm->execute()) {
@@ -308,7 +329,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$affected = $stm->rowCount();
 		$affected = $stm->rowCount();
 
 
 		$sql = 'UPDATE `' . $this->prefix . 'feed` '
 		$sql = 'UPDATE `' . $this->prefix . 'feed` '
-			 . 'SET cache_nbEntries=0, cache_nbUnreads=0 WHERE id=?';
+			 . 'SET `cache_nbEntries`=0, `cache_nbUnreads`=0 WHERE id=?';
 		$values = array($id);
 		$values = array($id);
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 		if (!($stm && $stm->execute($values))) {
 		if (!($stm && $stm->execute($values))) {
@@ -326,7 +347,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
 		$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
 		     . 'WHERE id_feed=:id_feed AND id<=:id_max '
 		     . 'WHERE id_feed=:id_feed AND id<=:id_max '
 		     . 'AND is_favorite=0 '	//Do not remove favourites
 		     . 'AND is_favorite=0 '	//Do not remove favourites
-		     . 'AND lastSeen < (SELECT maxLastSeen FROM (SELECT (MAX(e3.lastSeen)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) '	//Do not remove the most newly seen articles, plus a few seconds of tolerance
+		     . 'AND `lastSeen` < (SELECT maxLastSeen FROM (SELECT (MAX(e3.`lastSeen`)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) '	//Do not remove the most newly seen articles, plus a few seconds of tolerance
 		     . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)';	//Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery'
 		     . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)';	//Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery'
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 
 

+ 2 - 2
app/Models/FeedDAOSQLite.php

@@ -4,8 +4,8 @@ class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
 
 
 	public function updateCachedValues() {	//For one single feed, call updateLastUpdate($id)
 	public function updateCachedValues() {	//For one single feed, call updateLastUpdate($id)
 		$sql = 'UPDATE `' . $this->prefix . 'feed` '
 		$sql = 'UPDATE `' . $this->prefix . 'feed` '
-		     . 'SET cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
-		     . 'cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
+		     . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+		     . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 		if ($stm && $stm->execute()) {
 		if ($stm && $stm->execute()) {
 			return $stm->rowCount();
 			return $stm->rowCount();

+ 33 - 69
app/Models/StatsDAO.php

@@ -4,6 +4,10 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo {
 
 
 	const ENTRY_COUNT_PERIOD = 30;
 	const ENTRY_COUNT_PERIOD = 30;
 
 
+	protected function sqlFloor($s) {
+		return "FLOOR($s)";
+	}
+
 	/**
 	/**
 	 * Calculates entry repartition for all feeds and for main stream.
 	 * Calculates entry repartition for all feeds and for main stream.
 	 *
 	 *
@@ -37,12 +41,12 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo {
 			$filter .= "AND e.id_feed = {$feed}";
 			$filter .= "AND e.id_feed = {$feed}";
 		}
 		}
 		$sql = <<<SQL
 		$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
+SELECT COUNT(1) AS total,
+COUNT(1) - SUM(e.is_read) AS count_unreads,
+SUM(e.is_read) AS count_reads,
+SUM(e.is_favorite) AS count_favorites
+FROM `{$this->prefix}entry` AS e
+, `{$this->prefix}feed` AS f
 WHERE e.id_feed = f.id
 WHERE e.id_feed = f.id
 {$filter}
 {$filter}
 SQL;
 SQL;
@@ -61,14 +65,16 @@ SQL;
 	 */
 	 */
 	public function calculateEntryCount() {
 	public function calculateEntryCount() {
 		$count = $this->initEntryCountArray();
 		$count = $this->initEntryCountArray();
-		$period = self::ENTRY_COUNT_PERIOD;
+		$midnight = mktime(0, 0, 0);
+		$oldest = $midnight - (self::ENTRY_COUNT_PERIOD * 86400);
 
 
 		// Get stats per day for the last 30 days
 		// Get stats per day for the last 30 days
+		$sqlDay = $this->sqlFloor("(date - $midnight) / 86400");
 		$sql = <<<SQL
 		$sql = <<<SQL
-SELECT DATEDIFF(FROM_UNIXTIME(e.date), NOW()) AS day,
-COUNT(1) AS count
-FROM {$this->prefix}entry AS e
-WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -{$period} DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d')
+SELECT {$sqlDay} AS day,
+COUNT(*) as count
+FROM `{$this->prefix}entry`
+WHERE date >= {$oldest} AND date < {$midnight}
 GROUP BY day
 GROUP BY day
 ORDER BY day ASC
 ORDER BY day ASC
 SQL;
 SQL;
@@ -80,28 +86,7 @@ SQL;
 			$count[$value['day']] = (int) $value['count'];
 			$count[$value['day']] = (int) $value['count'];
 		}
 		}
 
 
-		return $this->convertToSerie($count);
-	}
-
-	/**
-	 * Calculates entry average per day on a 30 days period.
-	 *
-	 * @return integer
-	 */
-	public function calculateEntryAverage() {
-		$period = self::ENTRY_COUNT_PERIOD;
-
-		// Get stats per day for the last 30 days
-		$sql = <<<SQL
-SELECT COUNT(1) / {$period} AS average
-FROM {$this->prefix}entry AS e
-WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -{$period} DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d')
-SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
-		$res = $stm->fetch(PDO::FETCH_NAMED);
-
-		return round($res['average'], 2);
+		return $count;
 	}
 	}
 
 
 	/**
 	/**
@@ -158,7 +143,7 @@ SQL;
 		$sql = <<<SQL
 		$sql = <<<SQL
 SELECT DATE_FORMAT(FROM_UNIXTIME(e.date), '{$period}') AS period
 SELECT DATE_FORMAT(FROM_UNIXTIME(e.date), '{$period}') AS period
 , COUNT(1) AS count
 , COUNT(1) AS count
-FROM {$this->prefix}entry AS e
+FROM `{$this->prefix}entry` AS e
 {$restrict}
 {$restrict}
 GROUP BY period
 GROUP BY period
 ORDER BY period ASC
 ORDER BY period ASC
@@ -173,7 +158,7 @@ SQL;
 			$repartition[(int) $value['period']] = (int) $value['count'];
 			$repartition[(int) $value['period']] = (int) $value['count'];
 		}
 		}
 
 
-		return $this->convertToSerie($repartition);
+		return $repartition;
 	}
 	}
 
 
 	/**
 	/**
@@ -222,7 +207,7 @@ SQL;
 SELECT COUNT(1) AS count
 SELECT COUNT(1) AS count
 , MIN(date) AS date_min
 , MIN(date) AS date_min
 , MAX(date) AS date_max
 , MAX(date) AS date_max
-FROM {$this->prefix}entry AS e
+FROM `{$this->prefix}entry` AS e
 {$restrict}
 {$restrict}
 SQL;
 SQL;
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
@@ -266,8 +251,8 @@ SQL;
 		$sql = <<<SQL
 		$sql = <<<SQL
 SELECT c.name AS label
 SELECT c.name AS label
 , COUNT(f.id) AS data
 , COUNT(f.id) AS data
-FROM {$this->prefix}category AS c,
-{$this->prefix}feed AS f
+FROM `{$this->prefix}category` AS c,
+`{$this->prefix}feed` AS f
 WHERE c.id = f.category
 WHERE c.id = f.category
 GROUP BY label
 GROUP BY label
 ORDER BY data DESC
 ORDER BY data DESC
@@ -276,7 +261,7 @@ SQL;
 		$stm->execute();
 		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 
-		return $this->convertToPieSerie($res);
+		return $res;
 	}
 	}
 
 
 	/**
 	/**
@@ -289,9 +274,9 @@ SQL;
 		$sql = <<<SQL
 		$sql = <<<SQL
 SELECT c.name AS label
 SELECT c.name AS label
 , COUNT(e.id) AS data
 , COUNT(e.id) AS data
-FROM {$this->prefix}category AS c,
-{$this->prefix}feed AS f,
-{$this->prefix}entry AS e
+FROM `{$this->prefix}category` AS c,
+`{$this->prefix}feed` AS f,
+`{$this->prefix}entry` AS e
 WHERE c.id = f.category
 WHERE c.id = f.category
 AND f.id = e.id_feed
 AND f.id = e.id_feed
 GROUP BY label
 GROUP BY label
@@ -301,7 +286,7 @@ SQL;
 		$stm->execute();
 		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 
-		return $this->convertToPieSerie($res);
+		return $res;
 	}
 	}
 
 
 	/**
 	/**
@@ -315,9 +300,9 @@ SELECT f.id AS id
 , MAX(f.name) AS name
 , MAX(f.name) AS name
 , MAX(c.name) AS category
 , MAX(c.name) AS category
 , COUNT(e.id) AS count
 , COUNT(e.id) AS count
-FROM {$this->prefix}category AS c,
-{$this->prefix}feed AS f,
-{$this->prefix}entry AS e
+FROM `{$this->prefix}category` AS c,
+`{$this->prefix}feed` AS f,
+`{$this->prefix}entry` AS e
 WHERE c.id = f.category
 WHERE c.id = f.category
 AND f.id = e.id_feed
 AND f.id = e.id_feed
 GROUP BY f.id
 GROUP BY f.id
@@ -340,8 +325,8 @@ SELECT MAX(f.id) as id
 , MAX(f.name) AS name
 , MAX(f.name) AS name
 , MAX(date) AS last_date
 , MAX(date) AS last_date
 , COUNT(*) AS nb_articles
 , COUNT(*) AS nb_articles
-FROM {$this->prefix}feed AS f,
-{$this->prefix}entry AS e
+FROM `{$this->prefix}feed` AS f,
+`{$this->prefix}entry` AS e
 WHERE f.id = e.id_feed
 WHERE f.id = e.id_feed
 GROUP BY f.id
 GROUP BY f.id
 ORDER BY name
 ORDER BY name
@@ -351,27 +336,6 @@ SQL;
 		return $stm->fetchAll(PDO::FETCH_ASSOC);
 		return $stm->fetchAll(PDO::FETCH_ASSOC);
 	}
 	}
 
 
-	protected function convertToSerie($data) {
-		$serie = array();
-
-		foreach ($data as $key => $value) {
-			$serie[] = array($key, $value);
-		}
-
-		return $serie;
-	}
-
-	protected function convertToPieSerie($data) {
-		$serie = array();
-
-		foreach ($data as $value) {
-			$value['data'] = array(array(0, (int) $value['data']));
-			$serie[] = $value;
-		}
-
-		return $serie;
-	}
-
 	/**
 	/**
 	 * Gets days ready for graphs
 	 * Gets days ready for graphs
 	 *
 	 *

+ 67 - 0
app/Models/StatsDAOPGSQL.php

@@ -0,0 +1,67 @@
+<?php
+
+class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
+
+	/**
+	 * Calculates the number of article per hour of the day per feed
+	 *
+	 * @param integer $feed id
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerHour($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('hour', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per day of week per feed
+	 *
+	 * @param integer $feed id
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerDayOfWeek($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('day', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per month per feed
+	 *
+	 * @param integer $feed
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerMonth($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('month', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per period per feed
+	 *
+	 * @param string $period format string to use for grouping
+	 * @param integer $feed id
+	 * @return string
+	 */
+	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
+		$restrict = '';
+		if ($feed) {
+			$restrict = "WHERE e.id_feed = {$feed}";
+		}
+		$sql = <<<SQL
+SELECT extract( {$period} from to_timestamp(e.date)) AS period
+, COUNT(1) AS count
+FROM "{$this->prefix}entry" AS e
+{$restrict}
+GROUP BY period
+ORDER BY period ASC
+SQL;
+
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_NAMED);
+
+		foreach ($res as $value) {
+			$repartition[(int) $value['period']] = (int) $value['count'];
+		}
+
+		return $repartition;
+	}
+
+}

+ 4 - 55
app/Models/StatsDAOSQLite.php

@@ -2,59 +2,8 @@
 
 
 class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
 class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
 
 
-	/**
-	 * Calculates entry count per day on a 30 days period.
-	 * Returns the result as a JSON object.
-	 *
-	 * @return JSON object
-	 */
-	public function calculateEntryCount() {
-		$count = $this->initEntryCountArray();
-		$period = parent::ENTRY_COUNT_PERIOD;
-
-		// Get stats per day for the last 30 days
-		$sql = <<<SQL
-SELECT round(julianday(e.date, 'unixepoch') - julianday('now')) AS day,
-COUNT(1) AS count
-FROM {$this->prefix}entry AS e
-WHERE strftime('%Y%m%d', e.date, 'unixepoch')
-	BETWEEN strftime('%Y%m%d', 'now', '-{$period} days')
-	AND strftime('%Y%m%d', 'now', '-1 day')
-GROUP BY day
-ORDER BY day ASC
-SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-
-		foreach ($res as $value) {
-			$count[(int) $value['day']] = (int) $value['count'];
-		}
-
-		return $this->convertToSerie($count);
-	}
-
-	/**
-	 * Calculates entry average per day on a 30 days period.
-	 *
-	 * @return integer
-	 */
-	public function calculateEntryAverage() {
-		$period = self::ENTRY_COUNT_PERIOD;
-
-		// Get stats per day for the last 30 days
-		$sql = <<<SQL
-SELECT COUNT(1) / {$period} AS average
-FROM {$this->prefix}entry AS e
-WHERE strftime('%Y%m%d', e.date, 'unixepoch')
-	BETWEEN strftime('%Y%m%d', 'now', '-{$period} days')
-	AND strftime('%Y%m%d', 'now', '-1 day')
-SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
-		$res = $stm->fetch(PDO::FETCH_NAMED);
-
-		return round($res['average'], 2);
+	protected function sqlFloor($s) {
+		return "CAST(($s) AS INT)";
 	}
 	}
 
 
 	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
 	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
@@ -66,7 +15,7 @@ SQL;
 		$sql = <<<SQL
 		$sql = <<<SQL
 SELECT strftime('{$period}', e.date, 'unixepoch') AS period
 SELECT strftime('{$period}', e.date, 'unixepoch') AS period
 , COUNT(1) AS count
 , COUNT(1) AS count
-FROM {$this->prefix}entry AS e
+FROM `{$this->prefix}entry` AS e
 {$restrict}
 {$restrict}
 GROUP BY period
 GROUP BY period
 ORDER BY period ASC
 ORDER BY period ASC
@@ -81,7 +30,7 @@ SQL;
 			$repartition[(int) $value['period']] = (int) $value['count'];
 			$repartition[(int) $value['period']] = (int) $value['count'];
 		}
 		}
 
 
-		return $this->convertToSerie($repartition);
+		return $repartition;
 	}
 	}
 
 
 }
 }

+ 40 - 14
app/Models/UserDAO.php

@@ -1,34 +1,60 @@
 <?php
 <?php
 
 
 class FreshRSS_UserDAO extends Minz_ModelPdo {
 class FreshRSS_UserDAO extends Minz_ModelPdo {
-	public function createUser($username) {
+	public function createUser($username, $new_user_language, $insertDefaultFeeds = true) {
 		$db = FreshRSS_Context::$system_conf->db;
 		$db = FreshRSS_Context::$system_conf->db;
 		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
 
 		$userPDO = new Minz_ModelPdo($username);
 		$userPDO = new Minz_ModelPdo($username);
 
 
-		$ok = false;
-		if (defined('SQL_CREATE_TABLES')) {	//E.g. MySQL
-			$sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_', _t('gen.short.default_category'));
-			$stm = $userPDO->bd->prepare($sql);
-			$ok = $stm && $stm->execute();
-		} else {	//E.g. SQLite
-			global $SQL_CREATE_TABLES;
-			if (is_array($SQL_CREATE_TABLES)) {
-				$ok = true;
-				foreach ($SQL_CREATE_TABLES as $instruction) {
-					$sql = sprintf($instruction, '', _t('gen.short.default_category'));
+		$currentLanguage = Minz_Translate::language();
+
+		try {
+			Minz_Translate::reset($new_user_language);
+			$ok = false;
+			$bd_prefix_user = $db['prefix'] . $username . '_';
+			if (defined('SQL_CREATE_TABLES')) {	//E.g. MySQL
+				$sql = sprintf(SQL_CREATE_TABLES, $bd_prefix_user, _t('gen.short.default_category'));
+				$stm = $userPDO->bd->prepare($sql);
+				$ok = $stm && $stm->execute();
+			} else {	//E.g. SQLite
+				global $SQL_CREATE_TABLES;
+				if (is_array($SQL_CREATE_TABLES)) {
+					$ok = true;
+					foreach ($SQL_CREATE_TABLES as $instruction) {
+						$sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category'));
+						$stm = $userPDO->bd->prepare($sql);
+						$ok &= ($stm && $stm->execute());
+					}
+				}
+			}
+			if ($insertDefaultFeeds) {
+				if (defined('SQL_INSERT_FEEDS')) {	//E.g. MySQL
+					$sql = sprintf(SQL_INSERT_FEEDS, $bd_prefix_user);
 					$stm = $userPDO->bd->prepare($sql);
 					$stm = $userPDO->bd->prepare($sql);
-					$ok &= ($stm && $stm->execute());
+					$ok &= $stm && $stm->execute();
+				} else {	//E.g. SQLite
+					global $SQL_INSERT_FEEDS;
+					if (is_array($SQL_INSERT_FEEDS)) {
+						foreach ($SQL_INSERT_FEEDS as $instruction) {
+							$sql = sprintf($instruction, $bd_prefix_user);
+							$stm = $userPDO->bd->prepare($sql);
+							$ok &= ($stm && $stm->execute());
+						}
+					}
 				}
 				}
 			}
 			}
+		} catch (Exception $e) {
+			Minz_Log::error('Error while creating user: ' . $e->getMessage());
 		}
 		}
 
 
+		Minz_Translate::reset($currentLanguage);
+
 		if ($ok) {
 		if ($ok) {
 			return true;
 			return true;
 		} else {
 		} else {
 			$info = empty($stm) ? array(2 => 'syntax error') : $stm->errorInfo();
 			$info = empty($stm) ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::error('SQL error : ' . $info[2]);
+			Minz_Log::error('SQL error: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
 	}
 	}

+ 6 - 1
app/SQL/install.sql.mysql.php

@@ -1,4 +1,6 @@
 <?php
 <?php
+define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+
 define('SQL_CREATE_TABLES', '
 define('SQL_CREATE_TABLES', '
 CREATE TABLE IF NOT EXISTS `%1$scategory` (
 CREATE TABLE IF NOT EXISTS `%1$scategory` (
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
@@ -57,11 +59,14 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` (
 ENGINE = INNODB;
 ENGINE = INNODB;
 
 
 INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
 INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
+');
+
+define('SQL_INSERT_FEEDS', '
 INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);
 INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);
 INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS @ GitHub", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);
 INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS @ GitHub", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);
 ');
 ');
 
 
-define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `%1$sentry`, `%1$sfeed`, `%1$scategory`');
 
 
 define('SQL_UPDATE_UTF8MB4', '
 define('SQL_UPDATE_UTF8MB4', '
 ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
 ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

+ 63 - 0
app/SQL/install.sql.pgsql.php

@@ -0,0 +1,63 @@
+<?php
+define('SQL_CREATE_DB', 'CREATE DATABASE %1$s ENCODING \'UTF8\';');
+
+global $SQL_CREATE_TABLES;
+$SQL_CREATE_TABLES = array(
+'CREATE TABLE IF NOT EXISTS "%1$scategory" (
+	"id" SERIAL PRIMARY KEY,
+	"name" VARCHAR(255) UNIQUE NOT NULL
+);',
+
+'CREATE TABLE IF NOT EXISTS "%1$sfeed" (
+	"id" SERIAL PRIMARY KEY,
+	"url" varchar(511) UNIQUE NOT NULL,
+	"category" SMALLINT DEFAULT 0,
+	"name" VARCHAR(255) NOT NULL,
+	"website" VARCHAR(255),
+	"description" text,
+	"lastUpdate" INT DEFAULT 0,
+	"priority" SMALLINT NOT NULL DEFAULT 10,
+	"pathEntries" VARCHAR(511) DEFAULT NULL,
+	"httpAuth" VARCHAR(511) DEFAULT NULL,
+	"error" smallint DEFAULT 0,
+	"keep_history" INT NOT NULL DEFAULT -2,
+	"ttl" INT NOT NULL DEFAULT -2,
+	"cache_nbEntries" INT DEFAULT 0,
+	"cache_nbUnreads" INT DEFAULT 0,
+	FOREIGN KEY ("category") REFERENCES "%1$scategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);',
+'CREATE INDEX %1$sname_index ON "%1$sfeed" ("name");',
+'CREATE INDEX %1$spriority_index ON "%1$sfeed" ("priority");',
+'CREATE INDEX %1$skeep_history_index ON "%1$sfeed" ("keep_history");',
+
+'CREATE TABLE IF NOT EXISTS "%1$sentry" (
+	"id" BIGINT NOT NULL PRIMARY KEY,
+	"guid" VARCHAR(760) UNIQUE NOT NULL,
+	"title" VARCHAR(255) NOT NULL,
+	"author" VARCHAR(255),
+	"content" TEXT,
+	"link" VARCHAR(1023) NOT NULL,
+	"date" INT,
+	"lastSeen" INT DEFAULT 0,
+	"hash" BYTEA,
+	"is_read" SMALLINT NOT NULL DEFAULT 0,
+	"is_favorite" SMALLINT NOT NULL DEFAULT 0,
+	"id_feed" SMALLINT,
+	"tags" VARCHAR(1023),
+	FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+	UNIQUE ("id_feed","guid")
+);',
+'CREATE INDEX %1$sis_favorite_index ON "%1$sentry" ("is_favorite");',
+'CREATE INDEX %1$sis_read_index ON "%1$sentry" ("is_read");',
+'CREATE INDEX %1$sentry_lastSeen_index ON "%1$sentry" ("lastSeen");',
+
+'INSERT INTO "%1$scategory" (name) SELECT \'%2$s\' WHERE NOT EXISTS (SELECT id FROM "%1$scategory" WHERE id = 1);',
+);
+
+global $SQL_INSERT_FEEDS;
+$SQL_INSERT_FEEDS = array(
+'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'http://freshrss.org/feeds/all.atom.xml\', 1, \'FreshRSS.org\', \'http://freshrss.org/\', \'FreshRSS, a free, self-hostable aggregator…\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'http://freshrss.org/feeds/all.atom.xml\');',
+'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'https://github.com/FreshRSS/FreshRSS/releases.atom\', 1, \'FreshRSS @ GitHub\', \'https://github.com/FreshRSS/FreshRSS/\', \'FreshRSS releases @ GitHub\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'https://github.com/FreshRSS/FreshRSS/releases.atom\');',
+);
+
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentry", "%1$sfeed", "%1$scategory"');

+ 20 - 16
app/SQL/install.sql.sqlite.php

@@ -1,16 +1,16 @@
 <?php
 <?php
 global $SQL_CREATE_TABLES;
 global $SQL_CREATE_TABLES;
 $SQL_CREATE_TABLES = array(
 $SQL_CREATE_TABLES = array(
-'CREATE TABLE IF NOT EXISTS `%1$scategory` (
+'CREATE TABLE IF NOT EXISTS `category` (
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 	`name` varchar(255) NOT NULL,
 	`name` varchar(255) NOT NULL,
 	UNIQUE (`name`)
 	UNIQUE (`name`)
 );',
 );',
 
 
-'CREATE TABLE IF NOT EXISTS `%1$sfeed` (
+'CREATE TABLE IF NOT EXISTS `feed` (
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 	`url` varchar(511) NOT NULL,
 	`url` varchar(511) NOT NULL,
-	`%1$scategory` SMALLINT DEFAULT 0,
+	`category` SMALLINT DEFAULT 0,
 	`name` varchar(255) NOT NULL,
 	`name` varchar(255) NOT NULL,
 	`website` varchar(255),
 	`website` varchar(255),
 	`description` text,
 	`description` text,
@@ -23,15 +23,15 @@ $SQL_CREATE_TABLES = array(
 	`ttl` INT NOT NULL DEFAULT -2,
 	`ttl` INT NOT NULL DEFAULT -2,
 	`cache_nbEntries` int DEFAULT 0,
 	`cache_nbEntries` int DEFAULT 0,
 	`cache_nbUnreads` int DEFAULT 0,
 	`cache_nbUnreads` int DEFAULT 0,
-	FOREIGN KEY (`%1$scategory`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
+	FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
 	UNIQUE (`url`)
 	UNIQUE (`url`)
 );',
 );',
 
 
-'CREATE INDEX IF NOT EXISTS feed_name_index ON `%1$sfeed`(`name`);',
-'CREATE INDEX IF NOT EXISTS feed_priority_index ON `%1$sfeed`(`priority`);',
-'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `%1$sfeed`(`keep_history`);',
+'CREATE INDEX IF NOT EXISTS feed_name_index ON `feed`(`name`);',
+'CREATE INDEX IF NOT EXISTS feed_priority_index ON `feed`(`priority`);',
+'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `feed`(`keep_history`);',
 
 
-'CREATE TABLE IF NOT EXISTS `%1$sentry` (
+'CREATE TABLE IF NOT EXISTS `entry` (
 	`id` bigint NOT NULL,
 	`id` bigint NOT NULL,
 	`guid` varchar(760) NOT NULL,
 	`guid` varchar(760) NOT NULL,
 	`title` varchar(255) NOT NULL,
 	`title` varchar(255) NOT NULL,
@@ -46,17 +46,21 @@ $SQL_CREATE_TABLES = array(
 	`id_feed` SMALLINT,
 	`id_feed` SMALLINT,
 	`tags` varchar(1023),
 	`tags` varchar(1023),
 	PRIMARY KEY (`id`),
 	PRIMARY KEY (`id`),
-	FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+	FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	UNIQUE (`id_feed`,`guid`)
 	UNIQUE (`id_feed`,`guid`)
 );',
 );',
 
 
-'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `%1$sentry`(`is_favorite`);',
-'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `%1$sentry`(`is_read`);',
-'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `%1$sentry`(`lastSeen`);',	//v1.1.1
+'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `entry`(`is_favorite`);',
+'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `entry`(`is_read`);',
+'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `entry`(`lastSeen`);',	//v1.1.1
 
 
-'INSERT OR IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");',
-'INSERT OR IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);',
-'INSERT OR IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS releases", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);',
+'INSERT OR IGNORE INTO `category` (id, name) VALUES(1, "%2$s");',
 );
 );
 
 
-define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
+global $SQL_INSERT_FEEDS;
+$SQL_INSERT_FEEDS = array(
+'INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);',
+'INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS releases", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);',
+);
+
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS entry, feed, category');

+ 2 - 1
app/actualize_script.php

@@ -28,12 +28,13 @@ $app = new FreshRSS();
 
 
 $system_conf = Minz_Configuration::get('system');
 $system_conf = Minz_Configuration::get('system');
 $system_conf->auth_type = 'none';  // avoid necessity to be logged in (not saved!)
 $system_conf->auth_type = 'none';  // avoid necessity to be logged in (not saved!)
+FreshRSS_Context::$isCli = true;
 
 
 // Create the list of users to actualize.
 // Create the list of users to actualize.
 // Users are processed in a random order but always start with admin
 // Users are processed in a random order but always start with admin
 $users = listUsers();
 $users = listUsers();
 shuffle($users);
 shuffle($users);
-if ($system_conf->default_user !== ''){
+if ($system_conf->default_user !== '') {
 	array_unshift($users, $system_conf->default_user);
 	array_unshift($users, $system_conf->default_user);
 	$users = array_unique($users);
 	$users = array_unique($users);
 }
 }

+ 4 - 4
app/i18n/cz/admin.php

@@ -33,7 +33,7 @@ return array(
 			'ok' => 'Máte požadovanou knihovnu pro ověřování znaků (ctype).',
 			'ok' => 'Máte požadovanou knihovnu pro ověřování znaků (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'Nemáte cURL (balíček php5-curl).',
+			'nok' => 'Nemáte cURL (balíček php-curl).',
 			'ok' => 'Máte rozšíření cURL.',
 			'ok' => 'Máte rozšíření cURL.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -71,8 +71,8 @@ return array(
 			'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).',
 			'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'_' => 'PHP instalace',
 			'_' => 'PHP instalace',
@@ -93,7 +93,7 @@ return array(
 			'ok' => 'Oprávnění adresáře users jsou v pořádku.',
 			'ok' => 'Oprávnění adresáře users jsou v pořádku.',
 		),
 		),
 		'zip' => array(
 		'zip' => array(
-			'nok' => 'Nemáte rozšíření ZIP (balíček php5-zip).',
+			'nok' => 'Nemáte rozšíření ZIP (balíček php-zip).',
 			'ok' => 'Máte rozšíření ZIP.',
 			'ok' => 'Máte rozšíření ZIP.',
 		),
 		),
 	),
 	),

+ 3 - 3
app/i18n/cz/feedback.php

@@ -43,12 +43,12 @@ return array(
 		'not_found' => '%s neexistuje',
 		'not_found' => '%s neexistuje',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
-		'export_no_zip_extension' => 'Na serveru není naistalována podpora zip. Zkuste prosím exportovat soubory jeden po druhém.',
+		'export_no_zip_extension' => 'Na serveru není naistalována podpora ZIP. Zkuste prosím exportovat soubory jeden po druhém.',
 		'feeds_imported' => 'Vaše kanály byly naimportovány a nyní budou aktualizovány',
 		'feeds_imported' => 'Vaše kanály byly naimportovány a nyní budou aktualizovány',
 		'feeds_imported_with_errors' => 'Vaše kanály byly naimportovány, došlo ale k nějakým chybám',
 		'feeds_imported_with_errors' => 'Vaše kanály byly naimportovány, došlo ale k nějakým chybám',
 		'file_cannot_be_uploaded' => 'Soubor nelze nahrát!',
 		'file_cannot_be_uploaded' => 'Soubor nelze nahrát!',
-		'no_zip_extension' => 'Na serveru není naistalována podpora zip.',
-		'zip_error' => 'Během importu zip souboru došlo k chybě.',
+		'no_zip_extension' => 'Na serveru není naistalována podpora ZIP.',
+		'zip_error' => 'Během importu ZIP souboru došlo k chybě.',
 	),
 	),
 	'sub' => array(
 	'sub' => array(
 		'actualize' => 'Aktualizovat',
 		'actualize' => 'Aktualizovat',

+ 3 - 3
app/i18n/cz/install.php

@@ -41,7 +41,7 @@ return array(
 			'ok' => 'Je nainstalována požadovaná knihovna pro ověřování znaků (ctype).',
 			'ok' => 'Je nainstalována požadovaná knihovna pro ověřování znaků (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'Nemáte cURL (balíček php5-curl).',
+			'nok' => 'Nemáte cURL (balíček php-curl).',
 			'ok' => 'Máte rozšíření cURL.',
 			'ok' => 'Máte rozšíření cURL.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -73,8 +73,8 @@ return array(
 			'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).',
 			'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'nok' => 'Vaše verze PHP je %s, ale FreshRSS vyžaduje alespoň verzi %s.',
 			'nok' => 'Vaše verze PHP je %s, ale FreshRSS vyžaduje alespoň verzi %s.',

+ 2 - 2
app/i18n/cz/sub.php

@@ -44,8 +44,8 @@ return array(
 		'export_opml' => 'Exportovat seznam kanálů (OPML)',
 		'export_opml' => 'Exportovat seznam kanálů (OPML)',
 		'export_starred' => 'Exportovat oblíbené',
 		'export_starred' => 'Exportovat oblíbené',
 		'feed_list' => 'Seznam %s článků',
 		'feed_list' => 'Seznam %s článků',
-		'file_to_import' => 'Soubor k importu<br />(OPML, Json nebo Zip)',
-		'file_to_import_no_zip' => 'Soubor k importu<br />(OPML nebo Json)',
+		'file_to_import' => 'Soubor k importu<br />(OPML, JSON nebo ZIP)',
+		'file_to_import_no_zip' => 'Soubor k importu<br />(OPML nebo JSON)',
 		'import' => 'Import',
 		'import' => 'Import',
 		'starred_list' => 'Seznam oblíbených článků',
 		'starred_list' => 'Seznam oblíbených článků',
 		'title' => 'Import / export',
 		'title' => 'Import / export',

+ 4 - 4
app/i18n/de/admin.php

@@ -33,7 +33,7 @@ return array(
 			'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).',
 			'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'Ihnen fehlt cURL (Paket php5-curl).',
+			'nok' => 'Ihnen fehlt cURL (Paket php-curl).',
 			'ok' => 'Sie haben die cURL-Erweiterung.',
 			'ok' => 'Sie haben die cURL-Erweiterung.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -71,8 +71,8 @@ return array(
 			'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).',
 			'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'_' => 'PHP-Installation',
 			'_' => 'PHP-Installation',
@@ -93,7 +93,7 @@ return array(
 			'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/users</em> sind in Ordnung.',
 			'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/users</em> sind in Ordnung.',
 		),
 		),
 		'zip' => array(
 		'zip' => array(
-			'nok' => 'Ihnen fehlt die ZIP-Erweiterung (Paket php5-zip).',
+			'nok' => 'Ihnen fehlt die ZIP-Erweiterung (Paket php-zip).',
 			'ok' => 'Sie haben die ZIP-Erweiterung.',
 			'ok' => 'Sie haben die ZIP-Erweiterung.',
 		),
 		),
 	),
 	),

+ 3 - 3
app/i18n/de/feedback.php

@@ -43,12 +43,12 @@ return array(
 		'not_found' => '%s existiert nicht',
 		'not_found' => '%s existiert nicht',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
-		'export_no_zip_extension' => 'Die Zip-Erweiterung fehlt auf Ihrem Server. Bitte versuchen Sie die Dateien eine nach der anderen zu exportieren.',
+		'export_no_zip_extension' => 'Die ZIP-Erweiterung fehlt auf Ihrem Server. Bitte versuchen Sie die Dateien eine nach der anderen zu exportieren.',
 		'feeds_imported' => 'Ihre Feeds sind importiert worden und werden jetzt aktualisiert',
 		'feeds_imported' => 'Ihre Feeds sind importiert worden und werden jetzt aktualisiert',
 		'feeds_imported_with_errors' => 'Ihre Feeds sind importiert worden, aber es traten einige Fehler auf',
 		'feeds_imported_with_errors' => 'Ihre Feeds sind importiert worden, aber es traten einige Fehler auf',
 		'file_cannot_be_uploaded' => 'Die Datei kann nicht hochgeladen werden!',
 		'file_cannot_be_uploaded' => 'Die Datei kann nicht hochgeladen werden!',
-		'no_zip_extension' => 'Die Zip-Erweiterung ist auf Ihrem Server nicht vorhanden.',
-		'zip_error' => 'Ein Fehler trat während des Zip-Imports auf.',
+		'no_zip_extension' => 'Die ZIP-Erweiterung ist auf Ihrem Server nicht vorhanden.',
+		'zip_error' => 'Ein Fehler trat während des ZIP-Imports auf.',
 	),
 	),
 	'sub' => array(
 	'sub' => array(
 		'actualize' => 'Aktualisieren',
 		'actualize' => 'Aktualisieren',

+ 4 - 4
app/i18n/de/install.php

@@ -4,7 +4,7 @@ return array(
 	'action' => array(
 	'action' => array(
 		'finish' => 'Installation fertigstellen',
 		'finish' => 'Installation fertigstellen',
 		'fix_errors_before' => 'Bitte Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.',
 		'fix_errors_before' => 'Bitte Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.',
-		'keep_install' => 'Vorherige Installation beibehalten (Daten)',
+		'keep_install' => 'Vorherige Konfiguration beibehalten',
 		'next_step' => 'Zum nächsten Schritt springen',
 		'next_step' => 'Zum nächsten Schritt springen',
 		'reinstall' => 'Neuinstallation von FreshRSS',
 		'reinstall' => 'Neuinstallation von FreshRSS',
 	),
 	),
@@ -41,7 +41,7 @@ return array(
 			'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).',
 			'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'Ihnen fehlt cURL (Paket php5-curl).',
+			'nok' => 'Ihnen fehlt cURL (Paket php-curl).',
 			'ok' => 'Sie haben die cURL-Erweiterung.',
 			'ok' => 'Sie haben die cURL-Erweiterung.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -73,8 +73,8 @@ return array(
 			'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).',
 			'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'nok' => 'Ihre PHP-Version ist %s aber FreshRSS benötigt mindestens Version %s.',
 			'nok' => 'Ihre PHP-Version ist %s aber FreshRSS benötigt mindestens Version %s.',

+ 1 - 1
app/i18n/de/sub.php

@@ -44,7 +44,7 @@ return array(
 		'export_opml' => 'Liste der Feeds exportieren (OPML)',
 		'export_opml' => 'Liste der Feeds exportieren (OPML)',
 		'export_starred' => 'Ihre Favoriten exportieren',
 		'export_starred' => 'Ihre Favoriten exportieren',
 		'feed_list' => 'Liste von %s Artikeln',
 		'feed_list' => 'Liste von %s Artikeln',
-		'file_to_import' => 'Zu importierende Datei<br />(OPML, JSON oder Zip)',
+		'file_to_import' => 'Zu importierende Datei<br />(OPML, JSON oder ZIP)',
 		'file_to_import_no_zip' => 'Zu importierende Datei<br />(OPML oder JSON)',
 		'file_to_import_no_zip' => 'Zu importierende Datei<br />(OPML oder JSON)',
 		'import' => 'Importieren',
 		'import' => 'Importieren',
 		'starred_list' => 'Liste der Lieblingsartikel',
 		'starred_list' => 'Liste der Lieblingsartikel',

+ 10 - 10
app/i18n/en/admin.php

@@ -29,12 +29,12 @@ return array(
 			'ok' => 'Connection to the database is ok.',
 			'ok' => 'Connection to the database is ok.',
 		),
 		),
 		'ctype' => array(
 		'ctype' => array(
-			'nok' => 'You lack a required library for character type checking (php-ctype).',
+			'nok' => 'Cannot find a required library for character type checking (php-ctype).',
 			'ok' => 'You have the required library for character type checking (ctype).',
 			'ok' => 'You have the required library for character type checking (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'You lack cURL (php5-curl package).',
-			'ok' => 'You have cURL extension.',
+			'nok' => 'Cannot find the cURL library (php-curl package).',
+			'ok' => 'You have the cURL library.',
 		),
 		),
 		'data' => array(
 		'data' => array(
 			'nok' => 'Check permissions on <em>./data</em> directory. HTTP server must have rights to write into',
 			'nok' => 'Check permissions on <em>./data</em> directory. HTTP server must have rights to write into',
@@ -42,7 +42,7 @@ return array(
 		),
 		),
 		'database' => 'Database installation',
 		'database' => 'Database installation',
 		'dom' => array(
 		'dom' => array(
-			'nok' => 'You lack a required library to browse the DOM (php-xml package).',
+			'nok' => 'Cannot find a required library to browse the DOM (php-xml package).',
 			'ok' => 'You have the required library to browse the DOM.',
 			'ok' => 'You have the required library to browse the DOM.',
 		),
 		),
 		'entries' => array(
 		'entries' => array(
@@ -59,20 +59,20 @@ return array(
 		),
 		),
 		'files' => 'File installation',
 		'files' => 'File installation',
 		'json' => array(
 		'json' => array(
-			'nok' => 'You lack JSON (php5-json package).',
+			'nok' => 'Cannot find JSON (php5-json package).',
 			'ok' => 'You have JSON extension.',
 			'ok' => 'You have JSON extension.',
 		),
 		),
 		'minz' => array(
 		'minz' => array(
-			'nok' => 'You lack the Minz framework.',
+			'nok' => 'Cannot find the Minz framework.',
 			'ok' => 'You have the Minz framework.',
 			'ok' => 'You have the Minz framework.',
 		),
 		),
 		'pcre' => array(
 		'pcre' => array(
-			'nok' => 'You lack a required library for regular expressions (php-pcre).',
+			'nok' => 'Cannot find a required library for regular expressions (php-pcre).',
 			'ok' => 'You have the required library for regular expressions (PCRE).',
 			'ok' => 'You have the required library for regular expressions (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite).',
-			'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Cannot find PDO or one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'_' => 'PHP installation',
 			'_' => 'PHP installation',
@@ -93,7 +93,7 @@ return array(
 			'ok' => 'Permissions on users directory are good.',
 			'ok' => 'Permissions on users directory are good.',
 		),
 		),
 		'zip' => array(
 		'zip' => array(
-			'nok' => 'You lack ZIP extension (php5-zip package).',
+			'nok' => 'Cannot find ZIP extension (php-zip package).',
 			'ok' => 'You have ZIP extension.',
 			'ok' => 'You have ZIP extension.',
 		),
 		),
 	),
 	),

+ 3 - 3
app/i18n/en/feedback.php

@@ -43,12 +43,12 @@ return array(
 		'not_found' => '%s does not exist',
 		'not_found' => '%s does not exist',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
-		'export_no_zip_extension' => 'Zip extension is not present on your server. Please try to export files one by one.',
+		'export_no_zip_extension' => 'ZIP extension is not present on your server. Please try to export files one by one.',
 		'feeds_imported' => 'Your feeds have been imported and will now be updated',
 		'feeds_imported' => 'Your feeds have been imported and will now be updated',
 		'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred',
 		'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred',
 		'file_cannot_be_uploaded' => 'File cannot be uploaded!',
 		'file_cannot_be_uploaded' => 'File cannot be uploaded!',
-		'no_zip_extension' => 'Zip extension is not present on your server.',
-		'zip_error' => 'An error occured during Zip import.',
+		'no_zip_extension' => 'ZIP extension is not present on your server.',
+		'zip_error' => 'An error occured during ZIP import.',
 	),
 	),
 	'sub' => array(
 	'sub' => array(
 		'actualize' => 'Actualise',
 		'actualize' => 'Actualise',

+ 13 - 13
app/i18n/en/install.php

@@ -4,7 +4,7 @@ return array(
 	'action' => array(
 	'action' => array(
 		'finish' => 'Complete installation',
 		'finish' => 'Complete installation',
 		'fix_errors_before' => 'Please fix errors before skipping to the next step.',
 		'fix_errors_before' => 'Please fix errors before skipping to the next step.',
-		'keep_install' => 'Keep previous installation',
+		'keep_install' => 'Keep previous configuration',
 		'next_step' => 'Go to the next step',
 		'next_step' => 'Go to the next step',
 		'reinstall' => 'Reinstall FreshRSS',
 		'reinstall' => 'Reinstall FreshRSS',
 	),
 	),
@@ -25,9 +25,9 @@ return array(
 		),
 		),
 		'host' => 'Host',
 		'host' => 'Host',
 		'prefix' => 'Table prefix',
 		'prefix' => 'Table prefix',
-		'password' => 'HTTP password',
+		'password' => 'Database password',
 		'type' => 'Type of database',
 		'type' => 'Type of database',
-		'username' => 'HTTP username',
+		'username' => 'Database username',
 	),
 	),
 	'check' => array(
 	'check' => array(
 		'_' => 'Checks',
 		'_' => 'Checks',
@@ -37,19 +37,19 @@ return array(
 			'ok' => 'Permissions on cache directory are good.',
 			'ok' => 'Permissions on cache directory are good.',
 		),
 		),
 		'ctype' => array(
 		'ctype' => array(
-			'nok' => 'You lack a required library for character type checking (php-ctype).',
+			'nok' => 'Cannot find a required library for character type checking (php-ctype).',
 			'ok' => 'You have the required library for character type checking (ctype).',
 			'ok' => 'You have the required library for character type checking (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'You lack cURL (php5-curl package).',
-			'ok' => 'You have cURL extension.',
+			'nok' => 'Cannot find the cURL library (php-curl package).',
+			'ok' => 'You have the cURL library.',
 		),
 		),
 		'data' => array(
 		'data' => array(
 			'nok' => 'Check permissions on <em>./data</em> directory. HTTP server must have rights to write into',
 			'nok' => 'Check permissions on <em>./data</em> directory. HTTP server must have rights to write into',
 			'ok' => 'Permissions on data directory are good.',
 			'ok' => 'Permissions on data directory are good.',
 		),
 		),
 		'dom' => array(
 		'dom' => array(
-			'nok' => 'You lack a required library to browse the DOM.',
+			'nok' => 'Cannot find a required library to browse the DOM.',
 			'ok' => 'You have the required library to browse the DOM.',
 			'ok' => 'You have the required library to browse the DOM.',
 		),
 		),
 		'favicons' => array(
 		'favicons' => array(
@@ -61,20 +61,20 @@ return array(
 			'ok' => 'Your HTTP REFERER is known and corresponds to your server.',
 			'ok' => 'Your HTTP REFERER is known and corresponds to your server.',
 		),
 		),
 		'json' => array(
 		'json' => array(
-			'nok' => 'You lack a recommended library to parse JSON.',
+			'nok' => 'Cannot find a recommended library to parse JSON.',
 			'ok' => 'You have a recommended library to parse JSON.',
 			'ok' => 'You have a recommended library to parse JSON.',
 		),
 		),
 		'minz' => array(
 		'minz' => array(
-			'nok' => 'You lack the Minz framework.',
+			'nok' => 'Cannot find the Minz framework.',
 			'ok' => 'You have the Minz framework.',
 			'ok' => 'You have the Minz framework.',
 		),
 		),
 		'pcre' => array(
 		'pcre' => array(
-			'nok' => 'You lack a required library for regular expressions (php-pcre).',
+			'nok' => 'Cannot find a required library for regular expressions (php-pcre).',
 			'ok' => 'You have the required library for regular expressions (PCRE).',
 			'ok' => 'You have the required library for regular expressions (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite).',
-			'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Cannot find PDO or one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'nok' => 'Your PHP version is %s but FreshRSS requires at least version %s.',
 			'nok' => 'Your PHP version is %s but FreshRSS requires at least version %s.',
@@ -85,7 +85,7 @@ return array(
 			'ok' => 'Permissions on users directory are good.',
 			'ok' => 'Permissions on users directory are good.',
 		),
 		),
 		'xml' => array(
 		'xml' => array(
-			'nok' => 'You lack the required library to parse XML.',
+			'nok' => 'Cannot find the required library to parse XML.',
 			'ok' => 'You have the required library to parse XML.',
 			'ok' => 'You have the required library to parse XML.',
 		),
 		),
 	),
 	),

+ 2 - 2
app/i18n/en/sub.php

@@ -44,8 +44,8 @@ return array(
 		'export_opml' => 'Export list of feeds (OPML)',
 		'export_opml' => 'Export list of feeds (OPML)',
 		'export_starred' => 'Export your favourites',
 		'export_starred' => 'Export your favourites',
 		'feed_list' => 'List of %s articles',
 		'feed_list' => 'List of %s articles',
-		'file_to_import' => 'File to import<br />(OPML, Json or Zip)',
-		'file_to_import_no_zip' => 'File to import<br />(OPML or Json)',
+		'file_to_import' => 'File to import<br />(OPML, JSON or ZIP)',
+		'file_to_import_no_zip' => 'File to import<br />(OPML or JSON)',
 		'import' => 'Import',
 		'import' => 'Import',
 		'starred_list' => 'List of favourite articles',
 		'starred_list' => 'List of favourite articles',
 		'title' => 'Import / export',
 		'title' => 'Import / export',

+ 14 - 14
app/i18n/fr/admin.php

@@ -29,12 +29,12 @@ return array(
 			'ok' => 'La connexion à la base de données est bonne.',
 			'ok' => 'La connexion à la base de données est bonne.',
 		),
 		),
 		'ctype' => array(
 		'ctype' => array(
-			'nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype).',
-			'ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype).',
+			'nok' => 'Impossible de trouver une librairie pour la vérification des types de caractères (php-ctype).',
+			'ok' => 'Vous disposez de la librairie pour la vérification des types de caractères (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'Vous ne disposez pas de cURL (paquet php5-curl).',
-			'ok' => 'Vous disposez de cURL.',
+			'nok' => 'Impossible de trouver la librairie cURL (paquet php-curl).',
+			'ok' => 'Vous disposez de la librairie cURL.',
 		),
 		),
 		'data' => array(
 		'data' => array(
 			'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data</em>. Le serveur HTTP doit être capable d’écrire dedans',
 			'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data</em>. Le serveur HTTP doit être capable d’écrire dedans',
@@ -42,8 +42,8 @@ return array(
 		),
 		),
 		'database' => 'Installation de la base de données',
 		'database' => 'Installation de la base de données',
 		'dom' => array(
 		'dom' => array(
-			'nok' => 'Il manque une librairie pour parcourir le DOM (paquet php-xml).',
-			'ok' => 'Vous disposez du nécessaire pour parcourir le DOM.',
+			'nok' => 'Impossible de trouver une librairie pour parcourir le DOM (paquet php-xml).',
+			'ok' => 'Vous disposez de la librairie pour parcourir le DOM.',
 		),
 		),
 		'entries' => array(
 		'entries' => array(
 			'nok' => 'La table entry est mal configurée.',
 			'nok' => 'La table entry est mal configurée.',
@@ -60,19 +60,19 @@ return array(
 		'files' => 'Installation des fichiers',
 		'files' => 'Installation des fichiers',
 		'json' => array(
 		'json' => array(
 			'nok' => 'Vous ne disposez pas de JSON (paquet php5-json).',
 			'nok' => 'Vous ne disposez pas de JSON (paquet php5-json).',
-			'ok' => 'Vous disposez de l\'extension JSON.',
+			'ok' => 'Vous disposez de lextension JSON.',
 		),
 		),
 		'minz' => array(
 		'minz' => array(
 			'nok' => 'Vous ne disposez pas de la librairie Minz.',
 			'nok' => 'Vous ne disposez pas de la librairie Minz.',
 			'ok' => 'Vous disposez du framework Minz',
 			'ok' => 'Vous disposez du framework Minz',
 		),
 		),
 		'pcre' => array(
 		'pcre' => array(
-			'nok' => 'Il manque une librairie pour les expressions régulières (php-pcre).',
-			'ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE).',
+			'nok' => 'Impossible de trouver une librairie pour les expressions régulières (php-pcre).',
+			'ok' => 'Vous disposez de la librairie pour les expressions régulières (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'_' => 'Installation de PHP',
 			'_' => 'Installation de PHP',
@@ -80,7 +80,7 @@ return array(
 			'ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS.',
 			'ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS.',
 		),
 		),
 		'tables' => array(
 		'tables' => array(
-			'nok' => 'Il manque une ou plusieurs tables en base de données.',
+			'nok' => 'Impossible de trouver une ou plusieurs tables en base de données.',
 			'ok' => 'Les tables sont bien présentes en base de données.',
 			'ok' => 'Les tables sont bien présentes en base de données.',
 		),
 		),
 		'title' => 'Vérification de l’installation',
 		'title' => 'Vérification de l’installation',
@@ -93,8 +93,8 @@ return array(
 			'ok' => 'Les droits sur le répertoire des utilisateurs sont bons.',
 			'ok' => 'Les droits sur le répertoire des utilisateurs sont bons.',
 		),
 		),
 		'zip' => array(
 		'zip' => array(
-			'nok' => 'Vous ne disposez pas de l\'extension ZIP (paquet php5-zip).',
-			'ok' => 'Vous disposez de l\'extension ZIP.',
+			'nok' => 'Vous ne disposez pas de l’extension ZIP (paquet php-zip).',
+			'ok' => 'Vous disposez de lextension ZIP.',
 		),
 		),
 	),
 	),
 	'extensions' => array(
 	'extensions' => array(

+ 3 - 3
app/i18n/fr/feedback.php

@@ -43,12 +43,12 @@ return array(
 		'not_found' => '%s n’existe pas',
 		'not_found' => '%s n’existe pas',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
-		'export_no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur. Veuillez essayer d’exporter les fichiers un par un.',
+		'export_no_zip_extension' => 'L’extension ZIP n’est pas présente sur votre serveur. Veuillez essayer d’exporter les fichiers un par un.',
 		'feeds_imported' => 'Vos flux ont été importés et vont maintenant être actualisés.',
 		'feeds_imported' => 'Vos flux ont été importés et vont maintenant être actualisés.',
 		'feeds_imported_with_errors' => 'Vos flux ont été importés mais des erreurs sont survenues.',
 		'feeds_imported_with_errors' => 'Vos flux ont été importés mais des erreurs sont survenues.',
 		'file_cannot_be_uploaded' => 'Le fichier ne peut pas être téléchargé !',
 		'file_cannot_be_uploaded' => 'Le fichier ne peut pas être téléchargé !',
-		'no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur.',
-		'zip_error' => 'Une erreur est survenue durant l’import du fichier Zip.',
+		'no_zip_extension' => 'L’extension ZIP n’est pas présente sur votre serveur.',
+		'zip_error' => 'Une erreur est survenue durant l’import du fichier ZIP.',
 	),
 	),
 	'sub' => array(
 	'sub' => array(
 		'actualize' => 'Actualiser',
 		'actualize' => 'Actualiser',

+ 13 - 13
app/i18n/fr/install.php

@@ -24,10 +24,10 @@ return array(
 			'ok' => 'La configuration de la base de données a été enregistrée.',
 			'ok' => 'La configuration de la base de données a été enregistrée.',
 		),
 		),
 		'host' => 'Hôte',
 		'host' => 'Hôte',
-		'password' => 'Mot de passe',
+		'password' => 'Mot de passe pour base de données',
 		'prefix' => 'Préfixe des tables',
 		'prefix' => 'Préfixe des tables',
 		'type' => 'Type de base de données',
 		'type' => 'Type de base de données',
-		'username' => 'Nom d’utilisateur',
+		'username' => 'Nom d’utilisateur pour base de données',
 	),
 	),
 	'check' => array(
 	'check' => array(
 		'_' => 'Vérifications',
 		'_' => 'Vérifications',
@@ -37,11 +37,11 @@ return array(
 			'ok' => 'Les droits sur le répertoire de cache sont bons.',
 			'ok' => 'Les droits sur le répertoire de cache sont bons.',
 		),
 		),
 		'ctype' => array(
 		'ctype' => array(
-			'nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype).',
-			'ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype).',
+			'nok' => 'Impossible de trouver une librairie pour la vérification des types de caractères (php-ctype).',
+			'ok' => 'Vous disposez de la librairie pour la vérification des types de caractères (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'Vous ne disposez pas de cURL (paquet php5-curl).',
+			'nok' => 'Vous ne disposez pas de cURL (paquet php-curl).',
 			'ok' => 'Vous disposez de cURL.',
 			'ok' => 'Vous disposez de cURL.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -49,8 +49,8 @@ return array(
 			'ok' => 'Les droits sur le répertoire de data sont bons.',
 			'ok' => 'Les droits sur le répertoire de data sont bons.',
 		),
 		),
 		'dom' => array(
 		'dom' => array(
-			'nok' => 'Il manque une librairie pour parcourir le DOM.',
-			'ok' => 'Vous disposez du nécessaire pour parcourir le DOM.',
+			'nok' => 'Impossible de trouver une librairie pour parcourir le DOM.',
+			'ok' => 'Vous disposez de la librairie pour parcourir le DOM.',
 		),
 		),
 		'favicons' => array(
 		'favicons' => array(
 			'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/favicons</em>. Le serveur HTTP doit être capable d’écrire dedans',
 			'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/favicons</em>. Le serveur HTTP doit être capable d’écrire dedans',
@@ -61,7 +61,7 @@ return array(
 			'ok' => 'Le HTTP REFERER est connu et semble correspondre à votre serveur.',
 			'ok' => 'Le HTTP REFERER est connu et semble correspondre à votre serveur.',
 		),
 		),
 		'json' => array(
 		'json' => array(
-			'nok' => 'Il manque une librairie recommandée pour JSON.',
+			'nok' => 'Impossible de trouver une librairie recommandée pour JSON.',
 			'ok' => 'Vouz disposez de la librairie recommandée pour JSON.',
 			'ok' => 'Vouz disposez de la librairie recommandée pour JSON.',
 		),
 		),
 		'minz' => array(
 		'minz' => array(
@@ -69,12 +69,12 @@ return array(
 			'ok' => 'Vous disposez du framework Minz',
 			'ok' => 'Vous disposez du framework Minz',
 		),
 		),
 		'pcre' => array(
 		'pcre' => array(
-			'nok' => 'Il manque une librairie pour les expressions régulières (php-pcre).',
-			'ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE).',
+			'nok' => 'Impossible de trouver une librairie pour les expressions régulières (php-pcre).',
+			'ok' => 'Vous disposez de la librairie pour les expressions régulières (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s.',
 			'nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s.',
@@ -85,7 +85,7 @@ return array(
 			'ok' => 'Les droits sur le répertoire des utilisateurs sont bons.',
 			'ok' => 'Les droits sur le répertoire des utilisateurs sont bons.',
 		),
 		),
 		'xml' => array(
 		'xml' => array(
-			'nok' => 'Il manque une librairie requise pour XML.',
+			'nok' => 'Impossible de trouver une librairie requise pour XML.',
 			'ok' => 'Vouz disposez de la librairie requise pour XML.',
 			'ok' => 'Vouz disposez de la librairie requise pour XML.',
 		),
 		),
 	),
 	),

+ 2 - 2
app/i18n/fr/sub.php

@@ -44,8 +44,8 @@ return array(
 		'export_opml' => 'Exporter la liste des flux (OPML)',
 		'export_opml' => 'Exporter la liste des flux (OPML)',
 		'export_starred' => 'Exporter les favoris',
 		'export_starred' => 'Exporter les favoris',
 		'feed_list' => 'Liste des articles de %s',
 		'feed_list' => 'Liste des articles de %s',
-		'file_to_import' => 'Fichier à importer<br />(OPML, Json ou Zip)',
-		'file_to_import_no_zip' => 'Fichier à importer<br />(OPML ou Json)',
+		'file_to_import' => 'Fichier à importer<br />(OPML, JSON ou ZIP)',
+		'file_to_import_no_zip' => 'Fichier à importer<br />(OPML ou JSON)',
 		'import' => 'Importer',
 		'import' => 'Importer',
 		'starred_list' => 'Liste des articles favoris',
 		'starred_list' => 'Liste des articles favoris',
 		'title' => 'Importer / exporter',
 		'title' => 'Importer / exporter',

+ 4 - 4
app/i18n/it/admin.php

@@ -33,7 +33,7 @@ return array(
 			'ok' => 'Libreria richiesta per il controllo dei caratteri presente (ctype).',
 			'ok' => 'Libreria richiesta per il controllo dei caratteri presente (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'Manca il supporto per cURL (pacchetto php5-curl).',
+			'nok' => 'Manca il supporto per cURL (pacchetto php-curl).',
 			'ok' => 'Estensione cURL presente.',
 			'ok' => 'Estensione cURL presente.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -71,8 +71,8 @@ return array(
 			'ok' => 'Libreria richiesta per le regular expressions presente (PCRE).',
 			'ok' => 'Libreria richiesta per le regular expressions presente (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite).',
-			'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'_' => 'Installazione PHP',
 			'_' => 'Installazione PHP',
@@ -93,7 +93,7 @@ return array(
 			'ok' => 'I permessi sulla cartella users sono corretti.',
 			'ok' => 'I permessi sulla cartella users sono corretti.',
 		),
 		),
 		'zip' => array(
 		'zip' => array(
-			'nok' => 'Manca estensione ZIP (pacchetto php5-zip).',
+			'nok' => 'Manca estensione ZIP (pacchetto php-zip).',
 			'ok' => 'Estensione ZIP presente.',
 			'ok' => 'Estensione ZIP presente.',
 		),
 		),
 	),
 	),

+ 3 - 3
app/i18n/it/feedback.php

@@ -43,12 +43,12 @@ return array(
 		'not_found' => '%s non disponibile',
 		'not_found' => '%s non disponibile',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
-		'export_no_zip_extension' => 'Estensione Zip non presente sul server. Per favore esporta i files singolarmente.',
+		'export_no_zip_extension' => 'Estensione ZIP non presente sul server. Per favore esporta i files singolarmente.',
 		'feeds_imported' => 'I tuoi feed sono stati importati e saranno aggiornati',
 		'feeds_imported' => 'I tuoi feed sono stati importati e saranno aggiornati',
 		'feeds_imported_with_errors' => 'I tuoi feeds sono stati importati ma si sono verificati alcuni errori',
 		'feeds_imported_with_errors' => 'I tuoi feeds sono stati importati ma si sono verificati alcuni errori',
 		'file_cannot_be_uploaded' => 'Il file non può essere caricato!',
 		'file_cannot_be_uploaded' => 'Il file non può essere caricato!',
-		'no_zip_extension' => 'Estensione Zip non presente sul server.',
-		'zip_error' => 'Si è verificato un errore importando il file Zip',
+		'no_zip_extension' => 'Estensione ZIP non presente sul server.',
+		'zip_error' => 'Si è verificato un errore importando il file ZIP',
 	),
 	),
 	'sub' => array(
 	'sub' => array(
 		'actualize' => 'Aggiorna',
 		'actualize' => 'Aggiorna',

+ 6 - 6
app/i18n/it/install.php

@@ -4,7 +4,7 @@ return array(
 	'action' => array(
 	'action' => array(
 		'finish' => 'Installazione completata',
 		'finish' => 'Installazione completata',
 		'fix_errors_before' => 'Per favore correggi gli errori prima di passare al passaggio successivo.',
 		'fix_errors_before' => 'Per favore correggi gli errori prima di passare al passaggio successivo.',
-		'keep_install' => 'Mantieni installazione precedente',
+		'keep_install' => 'Mantieni configurazione precedente',
 		'next_step' => 'Vai al prossimo passaggio',
 		'next_step' => 'Vai al prossimo passaggio',
 		'reinstall' => 'Reinstalla FreshRSS',
 		'reinstall' => 'Reinstalla FreshRSS',
 	),
 	),
@@ -25,9 +25,9 @@ return array(
 		),
 		),
 		'host' => 'Host',
 		'host' => 'Host',
 		'prefix' => 'Prefisso tabella',
 		'prefix' => 'Prefisso tabella',
-		'password' => 'HTTP password',
+		'password' => 'Password del database',
 		'type' => 'Tipo di database',
 		'type' => 'Tipo di database',
-		'username' => 'HTTP username',
+		'username' => 'Nome utente del database',
 	),
 	),
 	'check' => array(
 	'check' => array(
 		'_' => 'Controlli',
 		'_' => 'Controlli',
@@ -41,7 +41,7 @@ return array(
 			'ok' => 'Libreria richiesta per il controllo dei caratteri presente (ctype).',
 			'ok' => 'Libreria richiesta per il controllo dei caratteri presente (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'Manca il supporto per cURL (pacchetto php5-curl).',
+			'nok' => 'Manca il supporto per cURL (pacchetto php-curl).',
 			'ok' => 'Estensione cURL presente.',
 			'ok' => 'Estensione cURL presente.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -73,8 +73,8 @@ return array(
 			'ok' => 'Libreria richiesta per le regular expressions presente (PCRE).',
 			'ok' => 'Libreria richiesta per le regular expressions presente (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite).',
-			'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'_' => 'Installazione PHP',
 			'_' => 'Installazione PHP',

+ 2 - 2
app/i18n/it/sub.php

@@ -44,8 +44,8 @@ return array(
 		'export_opml' => 'Esporta tutta la lista dei feed (OPML)',
 		'export_opml' => 'Esporta tutta la lista dei feed (OPML)',
 		'export_starred' => 'Esporta i tuoi preferiti',
 		'export_starred' => 'Esporta i tuoi preferiti',
 		'feed_list' => 'Elenco di %s articoli',
 		'feed_list' => 'Elenco di %s articoli',
-		'file_to_import' => 'File da importare<br />(OPML, Json o Zip)',
-		'file_to_import_no_zip' => 'File da importare<br />(OPML o Json)',
+		'file_to_import' => 'File da importare<br />(OPML, JSON o ZIP)',
+		'file_to_import_no_zip' => 'File da importare<br />(OPML o JSON)',
 		'import' => 'Importa',
 		'import' => 'Importa',
 		'starred_list' => 'Elenco articoli preferiti',
 		'starred_list' => 'Elenco articoli preferiti',
 		'title' => 'Importa / esporta',
 		'title' => 'Importa / esporta',

+ 4 - 4
app/i18n/nl/admin.php

@@ -33,7 +33,7 @@ return array(
 			'ok' => 'U hebt de benodigde bibliotheek voor character type checking (ctype).',
 			'ok' => 'U hebt de benodigde bibliotheek voor character type checking (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'U mist de cURL (php5-curl package).',
+			'nok' => 'U mist de cURL (php-curl package).',
 			'ok' => 'U hebt de cURL uitbreiding.',
 			'ok' => 'U hebt de cURL uitbreiding.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -71,8 +71,8 @@ return array(
 			'ok' => 'U hebt de benodigde bibliotheek voor regular expressions (PCRE).',
 			'ok' => 'U hebt de benodigde bibliotheek voor regular expressions (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'U mist PDO of een van de ondersteunde drivers (pdo_mysql, pdo_sqlite).',
-			'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite).',
+			'nok' => 'U mist PDO of een van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'_' => 'PHP installatie',
 			'_' => 'PHP installatie',
@@ -93,7 +93,7 @@ return array(
 			'ok' => 'Permissies op de users map zijn goed.',
 			'ok' => 'Permissies op de users map zijn goed.',
 		),
 		),
 		'zip' => array(
 		'zip' => array(
-			'nok' => 'U mist ZIP uitbreiding (php5-zip package).',
+			'nok' => 'U mist ZIP uitbreiding (php-zip package).',
 			'ok' => 'U hebt ZIP uitbreiding.',
 			'ok' => 'U hebt ZIP uitbreiding.',
 		),
 		),
 	),
 	),

+ 3 - 3
app/i18n/nl/feedback.php

@@ -43,12 +43,12 @@ return array(
 		'not_found' => '%s bestaat niet',
 		'not_found' => '%s bestaat niet',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
-		'export_no_zip_extension' => 'Zip uitbreiding is niet aanwezig op uw server. Exporteer a.u.b. uw bestanden één voor één.',
+		'export_no_zip_extension' => 'ZIP uitbreiding is niet aanwezig op uw server. Exporteer a.u.b. uw bestanden één voor één.',
 		'feeds_imported' => 'Uw feeds zijn geimporteerd en worden nu vernieuwd',
 		'feeds_imported' => 'Uw feeds zijn geimporteerd en worden nu vernieuwd',
 		'feeds_imported_with_errors' => 'Uw feeds zijn geimporteerd maar er zijn enige fouten opgetreden',
 		'feeds_imported_with_errors' => 'Uw feeds zijn geimporteerd maar er zijn enige fouten opgetreden',
 		'file_cannot_be_uploaded' => 'Bestand kan niet worden verzonden!',
 		'file_cannot_be_uploaded' => 'Bestand kan niet worden verzonden!',
-		'no_zip_extension' => 'Zip uitbreiding is niet aanwezig op uw server.',
-		'zip_error' => 'Er is een fout opgetreden tijdens het imporeren van het Zip bestand.',
+		'no_zip_extension' => 'ZIP uitbreiding is niet aanwezig op uw server.',
+		'zip_error' => 'Er is een fout opgetreden tijdens het imporeren van het ZIP bestand.',
 	),
 	),
 	'sub' => array(
 	'sub' => array(
 		'actualize' => 'Actualiseren',
 		'actualize' => 'Actualiseren',

+ 5 - 5
app/i18n/nl/install.php

@@ -25,9 +25,9 @@ return array(
 		),
 		),
 		'host' => 'Host',
 		'host' => 'Host',
 		'prefix' => 'Tabel voorvoegsel',
 		'prefix' => 'Tabel voorvoegsel',
-		'password' => 'HTTP wachtwoord',
+		'password' => 'Database wachtwoord',
 		'type' => 'Type database',
 		'type' => 'Type database',
-		'username' => 'HTTP gebruikersnaam',
+		'username' => 'Database gebruikersnaam',
 	),
 	),
 	'check' => array(
 	'check' => array(
 		'_' => 'Controles',
 		'_' => 'Controles',
@@ -41,7 +41,7 @@ return array(
 			'ok' => 'U hebt de benodigde bibliotheek voor character type checking (ctype).',
 			'ok' => 'U hebt de benodigde bibliotheek voor character type checking (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'U mist cURL (php5-curl package).',
+			'nok' => 'U mist cURL (php-curl package).',
 			'ok' => 'U hebt de cURL uitbreiding.',
 			'ok' => 'U hebt de cURL uitbreiding.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -73,8 +73,8 @@ return array(
 			'ok' => 'U hebt de benodigde bibliotheek voor regular expressions (PCRE).',
 			'ok' => 'U hebt de benodigde bibliotheek voor regular expressions (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'U mist PDO of één van de ondersteunde (pdo_mysql, pdo_sqlite).',
-			'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite).',
+			'nok' => 'U mist PDO of één van de ondersteunde (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'nok' => 'Uw PHP versie is %s maar FreshRSS benodigd tenminste versie %s.',
 			'nok' => 'Uw PHP versie is %s maar FreshRSS benodigd tenminste versie %s.',

+ 2 - 2
app/i18n/nl/sub.php

@@ -44,8 +44,8 @@ return array(
 		'export_opml' => 'Exporteer lijst van feeds (OPML)',
 		'export_opml' => 'Exporteer lijst van feeds (OPML)',
 		'export_starred' => 'Exporteer je fovorieten',
 		'export_starred' => 'Exporteer je fovorieten',
 		'feed_list' => 'Lijst van %s artikelen',
 		'feed_list' => 'Lijst van %s artikelen',
-		'file_to_import' => 'Bestand om te importeren<br />(OPML, Json of Zip)',
-		'file_to_import_no_zip' => 'Bestand om te importeren<br />(OPML of Json)',
+		'file_to_import' => 'Bestand om te importeren<br />(OPML, JSON of ZIP)',
+		'file_to_import_no_zip' => 'Bestand om te importeren<br />(OPML of JSON)',
 		'import' => 'Importeer',
 		'import' => 'Importeer',
 		'starred_list' => 'Lijst van favoriete artikelen',
 		'starred_list' => 'Lijst van favoriete artikelen',
 		'title' => 'Importeren / exporteren',
 		'title' => 'Importeren / exporteren',

+ 4 - 4
app/i18n/ru/admin.php

@@ -33,7 +33,7 @@ return array(
 			'ok' => 'У вас не установлена библиотека для проверки типов символов (ctype).',
 			'ok' => 'У вас не установлена библиотека для проверки типов символов (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'У вас не установлено расширение cURL (пакет php5-curl).',
+			'nok' => 'У вас не установлено расширение cURL (пакет php-curl).',
 			'ok' => 'У вас установлено расширение cURL.',
 			'ok' => 'У вас установлено расширение cURL.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -71,8 +71,8 @@ return array(
 			'ok' => 'У вас установлена необходимая библиотека для работы с регулярными выражениями (PCRE).',
 			'ok' => 'У вас установлена необходимая библиотека для работы с регулярными выражениями (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite).',
-			'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite).',
+			'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'_' => 'PHP installation',
 			'_' => 'PHP installation',
@@ -93,7 +93,7 @@ return array(
 			'ok' => 'Права на папку users  в порядке.',
 			'ok' => 'Права на папку users  в порядке.',
 		),
 		),
 		'zip' => array(
 		'zip' => array(
-			'nok' => 'You lack ZIP extension (php5-zip package).',
+			'nok' => 'You lack ZIP extension (php-zip package).',
 			'ok' => 'You have ZIP extension.',
 			'ok' => 'You have ZIP extension.',
 		),
 		),
 	),
 	),

+ 3 - 3
app/i18n/ru/feedback.php

@@ -43,12 +43,12 @@ return array(
 		'not_found' => '%s does not exist',
 		'not_found' => '%s does not exist',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
-		'export_no_zip_extension' => 'Zip extension is not present on your server. Please try to export files one by one.',
+		'export_no_zip_extension' => 'ZIP extension is not present on your server. Please try to export files one by one.',
 		'feeds_imported' => 'Your feeds have been imported and will now be updated',
 		'feeds_imported' => 'Your feeds have been imported and will now be updated',
 		'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred',
 		'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred',
 		'file_cannot_be_uploaded' => 'File cannot be uploaded!',
 		'file_cannot_be_uploaded' => 'File cannot be uploaded!',
-		'no_zip_extension' => 'Zip extension is not present on your server.',
-		'zip_error' => 'An error occured during Zip import.',
+		'no_zip_extension' => 'ZIP extension is not present on your server.',
+		'zip_error' => 'An error occured during ZIP import.',
 	),
 	),
 	'sub' => array(
 	'sub' => array(
 		'actualize' => 'Actualise',
 		'actualize' => 'Actualise',

+ 5 - 5
app/i18n/ru/install.php

@@ -25,9 +25,9 @@ return array(
 		),
 		),
 		'host' => 'Хост',
 		'host' => 'Хост',
 		'prefix' => 'Префикс таблицы',
 		'prefix' => 'Префикс таблицы',
-		'password' => 'Пароль HTTP',
+		'password' => 'Пароль базы данных',
 		'type' => 'Тип базы данных',
 		'type' => 'Тип базы данных',
-		'username' => 'Имя пользователя HTTP',
+		'username' => 'Имя пользователя базы данных',
 	),
 	),
 	'check' => array(
 	'check' => array(
 		'_' => 'Проверки',
 		'_' => 'Проверки',
@@ -41,7 +41,7 @@ return array(
 			'ok' => 'У вас установлена необходимая библиотека для проверки типов символов (ctype).',
 			'ok' => 'У вас установлена необходимая библиотека для проверки типов символов (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'У вас нет расширения cURL (пакет php5-curl).',
+			'nok' => 'У вас нет расширения cURL (пакет php-curl).',
 			'ok' => 'У вас установлено расширение cURL.',
 			'ok' => 'У вас установлено расширение cURL.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -69,8 +69,8 @@ return array(
 			'ok' => 'У вас установлена необходимая библиотека для работы с регулярными выражениями (PCRE).',
 			'ok' => 'У вас установлена необходимая библиотека для работы с регулярными выражениями (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite).',
-			'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite).',
+			'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'nok' => 'У вас установлен PHP версии %s, но FreshRSS необходима версия не ниже %s.',
 			'nok' => 'У вас установлен PHP версии %s, но FreshRSS необходима версия не ниже %s.',

+ 2 - 2
app/i18n/ru/sub.php

@@ -44,8 +44,8 @@ return array(
 		'export_opml' => 'Export list of feeds (OPML)',
 		'export_opml' => 'Export list of feeds (OPML)',
 		'export_starred' => 'Export your favourites',
 		'export_starred' => 'Export your favourites',
 		'feed_list' => 'List of %s articles',
 		'feed_list' => 'List of %s articles',
-		'file_to_import' => 'File to import<br />(OPML, Json or Zip)',
-		'file_to_import_no_zip' => 'File to import<br />(OPML or Json)',
+		'file_to_import' => 'File to import<br />(OPML, JSON or ZIP)',
+		'file_to_import_no_zip' => 'File to import<br />(OPML or JSON)',
 		'import' => 'Import',
 		'import' => 'Import',
 		'starred_list' => 'List of favourite articles',
 		'starred_list' => 'List of favourite articles',
 		'title' => 'Import / export',
 		'title' => 'Import / export',

+ 4 - 4
app/i18n/tr/admin.php

@@ -33,7 +33,7 @@ return array(
 			'ok' => 'Karakter yazım kontrolü için kütüphane sorunsuz (ctype).',
 			'ok' => 'Karakter yazım kontrolü için kütüphane sorunsuz (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'cURL eksik (php5-curl package).',
+			'nok' => 'cURL eksik (php-curl package).',
 			'ok' => 'cURL eklentisi sorunsuz.',
 			'ok' => 'cURL eklentisi sorunsuz.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -71,8 +71,8 @@ return array(
 			'ok' => 'Düzenli ifadeler kütüphanesi sorunsuz (PCRE).',
 			'ok' => 'Düzenli ifadeler kütüphanesi sorunsuz (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite).',
-			'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite).',
+			'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'_' => 'PHP kurulumu',
 			'_' => 'PHP kurulumu',
@@ -93,7 +93,7 @@ return array(
 			'ok' => 'Kullanıcılar klasörü yetkileri sorunsuz.',
 			'ok' => 'Kullanıcılar klasörü yetkileri sorunsuz.',
 		),
 		),
 		'zip' => array(
 		'zip' => array(
-			'nok' => 'ZIP eklentisi eksik (php5-zip package).',
+			'nok' => 'ZIP eklentisi eksik (php-zip package).',
 			'ok' => 'ZIP eklentisi sorunsuz.',
 			'ok' => 'ZIP eklentisi sorunsuz.',
 		),
 		),
 	),
 	),

+ 3 - 3
app/i18n/tr/feedback.php

@@ -43,12 +43,12 @@ return array(
 		'not_found' => '%s bulunmamaktadır',
 		'not_found' => '%s bulunmamaktadır',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
-		'export_no_zip_extension' => 'Zip eklentisi mevcut sunucunuzda yer almıyor. Lütfen başka dosya formatında dışarı aktarmayı deneyin.',
+		'export_no_zip_extension' => 'ZIP eklentisi mevcut sunucunuzda yer almıyor. Lütfen başka dosya formatında dışarı aktarmayı deneyin.',
 		'feeds_imported' => 'Akışlarınız içe aktarıldı ve şimdi güncellenecek',
 		'feeds_imported' => 'Akışlarınız içe aktarıldı ve şimdi güncellenecek',
 		'feeds_imported_with_errors' => 'Akışlarınız içeri aktarıldı ama bazı hatalar meydana geldi',
 		'feeds_imported_with_errors' => 'Akışlarınız içeri aktarıldı ama bazı hatalar meydana geldi',
 		'file_cannot_be_uploaded' => 'Dosya yüklenemedi!',
 		'file_cannot_be_uploaded' => 'Dosya yüklenemedi!',
-		'no_zip_extension' => 'Zip eklentisi mevcut sunucunuzda yer almıyor.',
-		'zip_error' => 'Zip içe aktarımı sırasında hata meydana geldi.',
+		'no_zip_extension' => 'ZIP eklentisi mevcut sunucunuzda yer almıyor.',
+		'zip_error' => 'ZIP içe aktarımı sırasında hata meydana geldi.',
 	),
 	),
 	'sub' => array(
 	'sub' => array(
 		'actualize' => 'Güncelleme',
 		'actualize' => 'Güncelleme',

+ 5 - 5
app/i18n/tr/install.php

@@ -25,9 +25,9 @@ return array(
 		),
 		),
 		'host' => 'Sunucu',
 		'host' => 'Sunucu',
 		'prefix' => 'Tablo ön eki',
 		'prefix' => 'Tablo ön eki',
-		'password' => 'HTTP şifre',
+		'password' => 'Veritabanı şifresi',
 		'type' => 'Veritabanı türü',
 		'type' => 'Veritabanı türü',
-		'username' => 'HTTP kullanıcı adı',
+		'username' => 'Veritabanı kullanıcı adı',
 	),
 	),
 	'check' => array(
 	'check' => array(
 		'_' => 'Kontroller',
 		'_' => 'Kontroller',
@@ -41,7 +41,7 @@ return array(
 			'ok' => 'Karakter yazım kontrolü için kütüphane sorunsuz (ctype).',
 			'ok' => 'Karakter yazım kontrolü için kütüphane sorunsuz (ctype).',
 		),
 		),
 		'curl' => array(
 		'curl' => array(
-			'nok' => 'cURL eksik (php5-curl package).',
+			'nok' => 'cURL eksik (php-curl package).',
 			'ok' => 'cURL eklentisi sorunsuz.',
 			'ok' => 'cURL eklentisi sorunsuz.',
 		),
 		),
 		'data' => array(
 		'data' => array(
@@ -73,8 +73,8 @@ return array(
 			'ok' => 'Düzenli ifadeler kütüphanesi sorunsuz (PCRE).',
 			'ok' => 'Düzenli ifadeler kütüphanesi sorunsuz (PCRE).',
 		),
 		),
 		'pdo' => array(
 		'pdo' => array(
-			'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite).',
-			'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite).',
+			'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		),
 		'php' => array(
 		'php' => array(
 			'nok' => 'PHP versiyonunuz %s fakat FreshRSS için gerekli olan en düşük sürüm %s.',
 			'nok' => 'PHP versiyonunuz %s fakat FreshRSS için gerekli olan en düşük sürüm %s.',

+ 2 - 2
app/i18n/tr/sub.php

@@ -44,8 +44,8 @@ return array(
 		'export_opml' => 'Akış listesini dışarı aktar (OPML)',
 		'export_opml' => 'Akış listesini dışarı aktar (OPML)',
 		'export_starred' => 'Favorileri dışarı aktar',
 		'export_starred' => 'Favorileri dışarı aktar',
 		'feed_list' => '%s makalenin listesi',
 		'feed_list' => '%s makalenin listesi',
-		'file_to_import' => 'Dosyadan içe aktar<br />(OPML, Json or Zip)',
-		'file_to_import_no_zip' => 'Dosyadan içe aktar<br />(OPML or Json)',
+		'file_to_import' => 'Dosyadan içe aktar<br />(OPML, JSON or ZIP)',
+		'file_to_import_no_zip' => 'Dosyadan içe aktar<br />(OPML or JSON)',
 		'import' => 'İçe aktar',
 		'import' => 'İçe aktar',
 		'starred_list' => 'Favori makaleleirn listesi',
 		'starred_list' => 'Favori makaleleirn listesi',
 		'title' => 'İçe / dışa aktar',
 		'title' => 'İçe / dışa aktar',

+ 58 - 138
app/install.php

@@ -4,23 +4,18 @@ if (function_exists('opcache_reset')) {
 }
 }
 header("Content-Security-Policy: default-src 'self'");
 header("Content-Security-Policy: default-src 'self'");
 
 
-define('BCRYPT_COST', 9);
+require(LIB_PATH . '/lib_install.php');
 
 
 session_name('FreshRSS');
 session_name('FreshRSS');
 session_set_cookie_params(0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true);
 session_set_cookie_params(0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true);
 session_start();
 session_start();
 
 
-Minz_Configuration::register('default_system', join_path(DATA_PATH, 'config.default.php'));
-Minz_Configuration::register('default_user', join_path(USERS_PATH, '_', 'config.default.php'));
-
 if (isset($_GET['step'])) {
 if (isset($_GET['step'])) {
 	define('STEP',(int)$_GET['step']);
 	define('STEP',(int)$_GET['step']);
 } else {
 } else {
 	define('STEP', 0);
 	define('STEP', 0);
 }
 }
 
 
-define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
-
 if (STEP === 3 && isset($_POST['type'])) {
 if (STEP === 3 && isset($_POST['type'])) {
 	$_SESSION['bd_type'] = $_POST['type'];
 	$_SESSION['bd_type'] = $_POST['type'];
 }
 }
@@ -28,10 +23,13 @@ if (STEP === 3 && isset($_POST['type'])) {
 if (isset($_SESSION['bd_type'])) {
 if (isset($_SESSION['bd_type'])) {
 	switch ($_SESSION['bd_type']) {
 	switch ($_SESSION['bd_type']) {
 	case 'mysql':
 	case 'mysql':
-		include(APP_PATH . '/SQL/install.sql.mysql.php');
+		include_once(APP_PATH . '/SQL/install.sql.mysql.php');
 		break;
 		break;
 	case 'sqlite':
 	case 'sqlite':
-		include(APP_PATH . '/SQL/install.sql.sqlite.php');
+		include_once(APP_PATH . '/SQL/install.sql.sqlite.php');
+		break;
+	case 'pgsql':
+		include_once(APP_PATH . '/SQL/install.sql.pgsql.php');
 		break;
 		break;
 	}
 	}
 }
 }
@@ -130,12 +128,7 @@ function saveStep2() {
 
 
 		$password_plain = param('passwordPlain', false);
 		$password_plain = param('passwordPlain', false);
 		if ($password_plain !== false && cryptAvailable()) {
 		if ($password_plain !== false && cryptAvailable()) {
-			if (!function_exists('password_hash')) {
-				include_once(LIB_PATH . '/password_compat.php');
-			}
-			$passwordHash = password_hash($password_plain, PASSWORD_BCRYPT, array('cost' => BCRYPT_COST));
-			$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);	//Compatibility with bcrypt.js
-			$_SESSION['passwordHash'] = $passwordHash;
+			$_SESSION['passwordHash'] = FreshRSS_user_Controller::hashPassword($password_plain);
 		}
 		}
 
 
 		if (empty($_SESSION['old_entries']) ||
 		if (empty($_SESSION['old_entries']) ||
@@ -148,7 +141,7 @@ function saveStep2() {
 			return false;
 			return false;
 		}
 		}
 
 
-		$_SESSION['salt'] = sha1(uniqid(mt_rand(), true).implode('', stat(__FILE__)));
+		$_SESSION['salt'] = generateSalt();
 		if ((!ctype_digit($_SESSION['old_entries'])) ||($_SESSION['old_entries'] < 1)) {
 		if ((!ctype_digit($_SESSION['old_entries'])) ||($_SESSION['old_entries'] < 1)) {
 			$_SESSION['old_entries'] = $user_default_config->old_entries;
 			$_SESSION['old_entries'] = $user_default_config->old_entries;
 		}
 		}
@@ -170,7 +163,7 @@ function saveStep2() {
 
 
 		recursive_unlink($user_dir);
 		recursive_unlink($user_dir);
 		mkdir($user_dir);
 		mkdir($user_dir);
-		file_put_contents($user_config_path, "<?php\n return " . var_export($config_array, true) . ';');
+		file_put_contents($user_config_path, "<?php\n return " . var_export($config_array, true) . ";\n");
 
 
 		header('Location: index.php?step=3');
 		header('Location: index.php?step=3');
 	}
 	}
@@ -199,6 +192,9 @@ function saveStep3() {
 			$_SESSION['bd_prefix'] = substr($_POST['prefix'], 0, 16);
 			$_SESSION['bd_prefix'] = substr($_POST['prefix'], 0, 16);
 			$_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] . (empty($_SESSION['default_user']) ? '' : ($_SESSION['default_user'] . '_'));
 			$_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] . (empty($_SESSION['default_user']) ? '' : ($_SESSION['default_user'] . '_'));
 		}
 		}
+		if ($_SESSION['bd_type'] === 'pgsql') {
+			$_SESSION['bd_base'] = strtolower($_SESSION['bd_base']);
+		}
 
 
 		// We use dirname to remove the /i part
 		// We use dirname to remove the /i part
 		$base_url = dirname(Minz_Request::guessBaseUrl());
 		$base_url = dirname(Minz_Request::guessBaseUrl());
@@ -221,55 +217,30 @@ function saveStep3() {
 		);
 		);
 
 
 		@unlink(join_path(DATA_PATH, 'config.php'));	//To avoid access-rights problems
 		@unlink(join_path(DATA_PATH, 'config.php'));	//To avoid access-rights problems
-		file_put_contents(join_path(DATA_PATH, 'config.php'), "<?php\n return " . var_export($config_array, true) . ';');
+		file_put_contents(join_path(DATA_PATH, 'config.php'), "<?php\n return " . var_export($config_array, true) . ";\n");
 
 
-		$res = checkBD();
+		$config_array['db']['default_user'] = $config_array['default_user'];
+		$config_array['db']['prefix_user'] = $_SESSION['bd_prefix_user'];
+		$ok = checkDb($config_array['db']) && checkDbUser($config_array['db']);
+		if (!$ok) {
+			@unlink(join_path(DATA_PATH, 'config.php'));
+		}
 
 
-		if ($res) {
+		if ($ok) {
 			$_SESSION['bd_error'] = '';
 			$_SESSION['bd_error'] = '';
 			header('Location: index.php?step=4');
 			header('Location: index.php?step=4');
-		} elseif (empty($_SESSION['bd_error'])) {
-			$_SESSION['bd_error'] = 'Unknown error!';
+		} else {
+			$_SESSION['bd_error'] = empty($config_array['db']['bd_error']) ? 'Unknown error!' : $config_array['db']['bd_error'];
 		}
 		}
 	}
 	}
 	invalidateHttpCache();
 	invalidateHttpCache();
 }
 }
 
 
-function newPdo() {
-	switch ($_SESSION['bd_type']) {
-	case 'mysql':
-		$str = 'mysql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base'];
-		$driver_options = array(
-			PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
-		);
-		break;
-	case 'sqlite':
-		$str = 'sqlite:' . join_path(USERS_PATH, $_SESSION['default_user'], 'db.sqlite');
-		$driver_options = array(
-			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
-		);
-		break;
-	default:
-		return false;
-	}
-	return new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
-}
-
-function deleteInstall() {
-	$res = unlink(join_path(DATA_PATH, 'do-install.txt'));
-
-	if (!$res) {
-		return false;
-	}
-
-	header('Location: index.php');
-}
-
 
 
 /*** VÉRIFICATIONS ***/
 /*** VÉRIFICATIONS ***/
 function checkStep() {
 function checkStep() {
 	$s0 = checkStep0();
 	$s0 = checkStep0();
-	$s1 = checkStep1();
+	$s1 = checkRequirements();
 	$s2 = checkStep2();
 	$s2 = checkStep2();
 	$s3 = checkStep3();
 	$s3 = checkStep3();
 	if (STEP > 0 && $s0['all'] != 'ok') {
 	if (STEP > 0 && $s0['all'] != 'ok') {
@@ -295,47 +266,6 @@ function checkStep0() {
 	);
 	);
 }
 }
 
 
-function checkStep1() {
-	$php = version_compare(PHP_VERSION, '5.3.3') >= 0;
-	$minz = file_exists(join_path(LIB_PATH, 'Minz'));
-	$curl = extension_loaded('curl');
-	$pdo_mysql = extension_loaded('pdo_mysql');
-	$pdo_sqlite = extension_loaded('pdo_sqlite');
-	$pdo = $pdo_mysql || $pdo_sqlite;
-	$pcre = extension_loaded('pcre');
-	$ctype = extension_loaded('ctype');
-	$dom = class_exists('DOMDocument');
-	$xml = function_exists('xml_parser_create');
-	$json = function_exists('json_encode');
-	$data = DATA_PATH && is_writable(DATA_PATH);
-	$cache = CACHE_PATH && is_writable(CACHE_PATH);
-	$users = USERS_PATH && is_writable(USERS_PATH);
-	$favicons = is_writable(join_path(DATA_PATH, 'favicons'));
-	$http_referer = is_referer_from_same_domain();
-
-	return array(
-		'php' => $php ? 'ok' : 'ko',
-		'minz' => $minz ? 'ok' : 'ko',
-		'curl' => $curl ? 'ok' : 'ko',
-		'pdo-mysql' => $pdo_mysql ? 'ok' : 'ko',
-		'pdo-sqlite' => $pdo_sqlite ? 'ok' : 'ko',
-		'pdo' => $pdo ? 'ok' : 'ko',
-		'pcre' => $pcre ? 'ok' : 'ko',
-		'ctype' => $ctype ? 'ok' : 'ko',
-		'dom' => $dom ? 'ok' : 'ko',
-		'xml' => $xml ? 'ok' : 'ko',
-		'json' => $json ? 'ok' : 'ko',
-		'data' => $data ? 'ok' : 'ko',
-		'cache' => $cache ? 'ok' : 'ko',
-		'users' => $users ? 'ok' : 'ko',
-		'favicons' => $favicons ? 'ok' : 'ko',
-		'http_referer' => $http_referer ? 'ok' : 'ko',
-		'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom && $xml &&
-		         $data && $cache && $users && $favicons && $http_referer ?
-		         'ok' : 'ko'
-	);
-}
-
 function freshrss_already_installed() {
 function freshrss_already_installed() {
 	$conf_path = join_path(DATA_PATH, 'config.php');
 	$conf_path = join_path(DATA_PATH, 'config.php');
 	if (!file_exists($conf_path)) {
 	if (!file_exists($conf_path)) {
@@ -406,43 +336,15 @@ function checkStep3() {
 	);
 	);
 }
 }
 
 
-function checkBD() {
+function checkDbUser(&$dbOptions) {
 	$ok = false;
 	$ok = false;
-
+	$str = $dbOptions['dsn'];
+	$driver_options = $dbOptions['options'];
 	try {
 	try {
-		$str = '';
-		$driver_options = null;
-		switch ($_SESSION['bd_type']) {
-		case 'mysql':
-			$driver_options = array(
-				PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4'
-			);
-
-			try {	// on ouvre une connexion juste pour créer la base si elle n'existe pas
-				$str = 'mysql:host=' . $_SESSION['bd_host'] . ';';
-				$c = new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
-				$sql = sprintf(SQL_CREATE_DB, $_SESSION['bd_base']);
-				$res = $c->query($sql);
-			} catch (PDOException $e) {
-			}
-
-			// on écrase la précédente connexion en sélectionnant la nouvelle BDD
-			$str = 'mysql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base'];
-			break;
-		case 'sqlite':
-			$str = 'sqlite:' . join_path(USERS_PATH, $_SESSION['default_user'], 'db.sqlite');
-			$driver_options = array(
-				PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
-			);
-			break;
-		default:
-			return false;
-		}
-
-		$c = new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
+		$c = new PDO($str, $dbOptions['user'], $dbOptions['password'], $driver_options);
 
 
 		if (defined('SQL_CREATE_TABLES')) {
 		if (defined('SQL_CREATE_TABLES')) {
-			$sql = sprintf(SQL_CREATE_TABLES, $_SESSION['bd_prefix_user'], _t('gen.short.default_category'));
+			$sql = sprintf(SQL_CREATE_TABLES, $dbOptions['prefix_user'], _t('gen.short.default_category'));
 			$stm = $c->prepare($sql);
 			$stm = $c->prepare($sql);
 			$ok = $stm->execute();
 			$ok = $stm->execute();
 		} else {
 		} else {
@@ -450,7 +352,22 @@ function checkBD() {
 			if (is_array($SQL_CREATE_TABLES)) {
 			if (is_array($SQL_CREATE_TABLES)) {
 				$ok = true;
 				$ok = true;
 				foreach ($SQL_CREATE_TABLES as $instruction) {
 				foreach ($SQL_CREATE_TABLES as $instruction) {
-					$sql = sprintf($instruction, $_SESSION['bd_prefix_user'], _t('gen.short.default_category'));
+					$sql = sprintf($instruction, $dbOptions['prefix_user'], _t('gen.short.default_category'));
+					$stm = $c->prepare($sql);
+					$ok &= $stm->execute();
+				}
+			}
+		}
+
+		if (defined('SQL_INSERT_FEEDS')) {
+			$sql = sprintf(SQL_INSERT_FEEDS, $dbOptions['prefix_user']);
+			$stm = $c->prepare($sql);
+			$ok &= $stm->execute();
+		} else {
+			global $SQL_INSERT_FEEDS;
+			if (is_array($SQL_INSERT_FEEDS)) {
+				foreach ($SQL_INSERT_FEEDS as $instruction) {
+					$sql = sprintf($instruction, $dbOptions['prefix_user']);
 					$stm = $c->prepare($sql);
 					$stm = $c->prepare($sql);
 					$ok &= $stm->execute();
 					$ok &= $stm->execute();
 				}
 				}
@@ -458,13 +375,8 @@ function checkBD() {
 		}
 		}
 	} catch (PDOException $e) {
 	} catch (PDOException $e) {
 		$ok = false;
 		$ok = false;
-		$_SESSION['bd_error'] = $e->getMessage();
+		$dbOptions['bd_error'] = $e->getMessage();
 	}
 	}
-
-	if (!$ok) {
-		@unlink(join_path(DATA_PATH, 'config.php'));
-	}
-
 	return $ok;
 	return $ok;
 }
 }
 
 
@@ -507,7 +419,7 @@ function printStep0() {
 
 
 // @todo refactor this view with the check_install action
 // @todo refactor this view with the check_install action
 function printStep1() {
 function printStep1() {
-	$res = checkStep1();
+	$res = checkRequirements();
 ?>
 ?>
 	<noscript><p class="alert alert-warn"><span class="alert-head"><?php echo _t('gen.short.attention'); ?></span> <?php echo _t('install.javascript_is_better'); ?></p></noscript>
 	<noscript><p class="alert alert-warn"><span class="alert-head"><?php echo _t('gen.short.attention'); ?></span> <?php echo _t('install.javascript_is_better'); ?></p></noscript>
 
 
@@ -690,7 +602,7 @@ function printStep3() {
 	<p class="alert alert-error"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.bdd.conf.ko'),(empty($_SESSION['bd_error']) ? '' : ' : ' . $_SESSION['bd_error']); ?></p>
 	<p class="alert alert-error"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.bdd.conf.ko'),(empty($_SESSION['bd_error']) ? '' : ' : ' . $_SESSION['bd_error']); ?></p>
 	<?php } ?>
 	<?php } ?>
 
 
-	<form action="index.php?step=3" method="post">
+	<form action="index.php?step=3" method="post" autocomplete="off">
 		<legend><?php echo _t('install.bdd.conf'); ?></legend>
 		<legend><?php echo _t('install.bdd.conf'); ?></legend>
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="type"><?php echo _t('install.bdd.type'); ?></label>
 			<label class="group-name" for="type"><?php echo _t('install.bdd.type'); ?></label>
@@ -708,6 +620,12 @@ function printStep3() {
 					SQLite
 					SQLite
 				</option>
 				</option>
 				<?php }?>
 				<?php }?>
+				<?php if (extension_loaded('pdo_pgsql')) {?>
+				<option value="pgsql"
+					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'pgsql') ? 'selected="selected"' : ''; ?>>
+					PostgreSQL (⚠️ experimental)
+				</option>
+				<?php }?>
 				</select>
 				</select>
 			</div>
 			</div>
 		</div>
 		</div>
@@ -716,7 +634,7 @@ function printStep3() {
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="host"><?php echo _t('install.bdd.host'); ?></label>
 			<label class="group-name" for="host"><?php echo _t('install.bdd.host'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="text" id="host" name="host" pattern="[0-9A-Za-z_.-]{1,64}" value="<?php echo isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : $system_default_config->db['host']; ?>" tabindex="2" />
+				<input type="text" id="host" name="host" pattern="[0-9A-Za-z_.-]{1,64}(:[0-9]{2,5})?" value="<?php echo isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : $system_default_config->db['host']; ?>" tabindex="2" />
 			</div>
 			</div>
 		</div>
 		</div>
 
 
@@ -730,7 +648,7 @@ function printStep3() {
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="pass"><?php echo _t('install.bdd.password'); ?></label>
 			<label class="group-name" for="pass"><?php echo _t('install.bdd.password'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="password" id="pass" name="pass" value="<?php echo isset($_SESSION['bd_password']) ? $_SESSION['bd_password'] : ''; ?>" tabindex="4" />
+				<input type="password" id="pass" name="pass" value="<?php echo isset($_SESSION['bd_password']) ? $_SESSION['bd_password'] : ''; ?>" tabindex="4" autocomplete="off" />
 			</div>
 			</div>
 		</div>
 		</div>
 
 
@@ -796,7 +714,9 @@ case 3:
 case 4:
 case 4:
 	break;
 	break;
 case 5:
 case 5:
-	deleteInstall();
+	if (deleteInstall()) {
+		header('Location: index.php');
+	}
 	break;
 	break;
 }
 }
 ?>
 ?>

+ 2 - 2
app/layout/aside_feed.phtml

@@ -79,13 +79,13 @@
 		<?php if (FreshRSS_Auth::hasAccess()) { ?>
 		<?php if (FreshRSS_Auth::hasAccess()) { ?>
 		<li class="item"><a href="<?php echo _url('stats', 'repartition', 'id', '------'); ?>"><?php echo _t('index.menu.stats'); ?></a></li>
 		<li class="item"><a href="<?php echo _url('stats', 'repartition', 'id', '------'); ?>"><?php echo _t('index.menu.stats'); ?></a></li>
 		<?php } ?>
 		<?php } ?>
-		<li class="item"><a target="_blank" href="http://example.net/"><?php echo _t('gen.action.see_website'); ?></a></li>
+		<li class="item"><a target="_blank" rel="noreferrer" href="http://example.net/"><?php echo _t('gen.action.see_website'); ?></a></li>
 		<?php if (FreshRSS_Auth::hasAccess()) { ?>
 		<?php if (FreshRSS_Auth::hasAccess()) { ?>
 		<li class="separator"></li>
 		<li class="separator"></li>
 		<li class="item"><a href="<?php echo _url('subscription', 'index', 'id', '------'); ?>"><?php echo _t('gen.action.manage'); ?></a></li>
 		<li class="item"><a href="<?php echo _url('subscription', 'index', 'id', '------'); ?>"><?php echo _t('gen.action.manage'); ?></a></li>
 		<li class="item"><a href="<?php echo _url('feed', 'actualize', 'id', '------'); ?>"><?php echo _t('gen.action.actualize'); ?></a></li>
 		<li class="item"><a href="<?php echo _url('feed', 'actualize', 'id', '------'); ?>"><?php echo _t('gen.action.actualize'); ?></a></li>
 		<li class="item">
 		<li class="item">
-			<?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?>
+			<?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : ''; ?>
 			<button class="read_all as-link <?php echo $confirm; ?>"
 			<button class="read_all as-link <?php echo $confirm; ?>"
 			        form="mark-read-aside"
 			        form="mark-read-aside"
 			        formaction="<?php echo _url('entry', 'read', 'get', 'f_------'); ?>"
 			        formaction="<?php echo _url('entry', 'read', 'get', 'f_------'); ?>"

+ 1 - 1
app/layout/aside_subscription.phtml

@@ -10,7 +10,7 @@
 	</li>
 	</li>
 
 
 	<li class="item">
 	<li class="item">
-		<a class="bookmarkClick" href="javascript:(function(){var%20url%20=%20location.href;window.open('<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'add'), 'html', true); ?>&amp;url_rss='+encodeURIComponent(url), '_blank');})();">
+		<a class="bookmarkClick" href="javascript:(function(){var%20url%20=%20location.href;var%20otherWindow=window.open('about:blank','_blank');otherWindow.opener=null;otherWindow.location='<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'add'), 'html', true); ?>&amp;url_rss='+encodeURIComponent(url);})();">
 			<?php echo _t('sub.menu.bookmark'); ?>
 			<?php echo _t('sub.menu.bookmark'); ?>
 		</a>
 		</a>
 	</li>
 	</li>

+ 3 - 0
app/layout/layout.phtml

@@ -42,6 +42,9 @@
 	} if (isset($this->rss_title)) {
 	} if (isset($this->rss_title)) {
 		$url_rss = $url_base;
 		$url_rss = $url_base;
 		$url_rss['a'] = 'rss';
 		$url_rss['a'] = 'rss';
+		if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) {
+			$url_rss['params']['hours'] = FreshRSS_Context::$user_conf->since_hours_posts_per_rss;
+		}
 ?>
 ?>
 		<link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display($url_rss); ?>" />
 		<link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display($url_rss); ?>" />
 <?php } if (FreshRSS_Context::$system_conf->allow_robots) { ?>
 <?php } if (FreshRSS_Context::$system_conf->allow_robots) { ?>

+ 5 - 2
app/layout/nav_menu.phtml

@@ -83,7 +83,7 @@
 
 
 	<div class="stick" id="nav_menu_read_all">
 	<div class="stick" id="nav_menu_read_all">
 		<form id="mark-read-menu" method="post">
 		<form id="mark-read-menu" method="post">
-		<?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?>
+		<?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : ''; ?>
 		<button class="read_all btn <?php echo $confirm; ?>"
 		<button class="read_all btn <?php echo $confirm; ?>"
 		        form="mark-read-menu"
 		        form="mark-read-menu"
 		        formaction="<?php echo Minz_Url::display($mark_read_url); ?>"
 		        formaction="<?php echo Minz_Url::display($mark_read_url); ?>"
@@ -151,8 +151,11 @@
 			if (FreshRSS_Context::$user_conf->token) {
 			if (FreshRSS_Context::$user_conf->token) {
 				$url_output['params']['token'] = FreshRSS_Context::$user_conf->token;
 				$url_output['params']['token'] = FreshRSS_Context::$user_conf->token;
 			}
 			}
+			if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) {
+				$url_output['params']['hours'] = FreshRSS_Context::$user_conf->since_hours_posts_per_rss;
+			}
 		?>
 		?>
-		<a class="view_rss btn" target="_blank" title="<?php echo _t('index.menu.rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+		<a class="view_rss btn" target="_blank" rel="noreferrer" title="<?php echo _t('index.menu.rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
 			<?php echo _i('rss'); ?>
 			<?php echo _i('rss'); ?>
 		</a>
 		</a>
 	</div>
 	</div>

+ 1 - 1
app/views/auth/index.phtml

@@ -60,7 +60,7 @@
 				<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo _t('gen.short.blank_to_disable'); ?>"<?php
 				<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo _t('gen.short.blank_to_disable'); ?>"<?php
 					echo FreshRSS_Auth::accessNeedsAction() ? '' : ' disabled="disabled"'; ?> data-leave-validation="<?php echo $token; ?>"/>
 					echo FreshRSS_Auth::accessNeedsAction() ? '' : ' disabled="disabled"'; ?> data-leave-validation="<?php echo $token; ?>"/>
 				<?php echo _i('help'); ?> <?php echo _t('admin.auth.token_help'); ?>
 				<?php echo _i('help'); ?> <?php echo _t('admin.auth.token_help'); ?>
-				<kbd><?php echo Minz_Url::display(array('params' => array('output' => 'rss', 'token' => $token)), 'html', true); ?></kbd>
+				<kbd><?php echo Minz_Url::display(array('a' => 'rss', 'params' => array('token' => $token, 'hours' => FreshRSS_Context::$user_conf->since_hours_posts_per_rss)), 'html', true); ?></kbd>
 			</div>
 			</div>
 		</div>
 		</div>
 		<?php } ?>
 		<?php } ?>

+ 2 - 2
app/views/configure/sharing.phtml

@@ -13,7 +13,7 @@
 			<input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_name'); ?>" size="64" />
 			<input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_name'); ?>" size="64" />
 			<input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_url'); ?>" size="64" />
 			<input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_url'); ?>" size="64" />
 			<a href="#" class="remove btn btn-attention" data-remove="group-share-##key##"><?php echo _i('close'); ?></a></div>
 			<a href="#" class="remove btn btn-attention" data-remove="group-share-##key##"><?php echo _i('close'); ?></a></div>
-			<a target="_blank" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="##help##"><?php echo _i('help'); ?></a>
+			<a target="_blank" rel="noreferrer" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="##help##"><?php echo _i('help'); ?></a>
 			</div></div>'>
 			</div></div>'>
 		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
 		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
 		<legend><?php echo _t('conf.sharing'); ?></legend>
 		<legend><?php echo _t('conf.sharing'); ?></legend>
@@ -38,7 +38,7 @@
 						<a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>"><?php echo _i('close'); ?></a>
 						<a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>"><?php echo _i('close'); ?></a>
 					</div>
 					</div>
 					<?php if ($share->formType() === 'advanced') { ?>
 					<?php if ($share->formType() === 'advanced') { ?>
-						<a target="_blank" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="<?php echo $share->help(); ?>"><?php echo _i('help'); ?></a>
+						<a target="_blank" rel="noreferrer" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="<?php echo $share->help(); ?>"><?php echo _i('help'); ?></a>
 					<?php } ?>
 					<?php } ?>
 				</div>
 				</div>
 			</div>
 			</div>

+ 3 - 3
app/views/feed/add.phtml

@@ -30,7 +30,7 @@
 			<label class="group-name"><?php echo _t('sub.feed.website'); ?></label>
 			<label class="group-name"><?php echo _t('sub.feed.website'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
 				<?php echo $this->feed->website(); ?>
 				<?php echo $this->feed->website(); ?>
-				<a class="btn" target="_blank" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a>
+				<a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a>
 			</div>
 			</div>
 		</div>
 		</div>
 		<?php } ?>
 		<?php } ?>
@@ -40,9 +40,9 @@
 			<div class="group-controls">
 			<div class="group-controls">
 				<div class="stick">
 				<div class="stick">
 					<input type="text" name="url_rss" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" />
 					<input type="text" name="url_rss" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" />
-					<a class="btn" target="_blank" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a>
+					<a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a>
 				</div>
 				</div>
-				<a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->feed->url(); ?>"><?php echo _t('sub.feed.validator'); ?></a>
+				<a class="btn" target="_blank" rel="noreferrer" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->feed->url(); ?>"><?php echo _t('sub.feed.validator'); ?></a>
 			</div>
 			</div>
 		</div>
 		</div>
 		<div class="form-group">
 		<div class="form-group">

+ 3 - 3
app/views/helpers/feed/update.phtml

@@ -37,7 +37,7 @@
 			<div class="group-controls">
 			<div class="group-controls">
 				<div class="stick">
 				<div class="stick">
 					<input type="text" name="website" id="website" class="extend" value="<?php echo $this->feed->website(); ?>" />
 					<input type="text" name="website" id="website" class="extend" value="<?php echo $this->feed->website(); ?>" />
-					<a class="btn" target="_blank" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a>
+					<a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a>
 				</div>
 				</div>
 			</div>
 			</div>
 		</div>
 		</div>
@@ -46,10 +46,10 @@
 			<div class="group-controls">
 			<div class="group-controls">
 				<div class="stick">
 				<div class="stick">
 					<input type="text" name="url" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" />
 					<input type="text" name="url" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" />
-					<a class="btn" target="_blank" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a>
+					<a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a>
 				</div>
 				</div>
 
 
-				<a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo rawurlencode(htmlspecialchars_decode($this->feed->url(), ENT_QUOTES)); ?>"><?php echo _t('sub.feed.validator'); ?></a>
+				<a class="btn" target="_blank" rel="noreferrer" href="http://validator.w3.org/feed/check.cgi?url=<?php echo rawurlencode(htmlspecialchars_decode($this->feed->url(), ENT_QUOTES)); ?>"><?php echo _t('sub.feed.validator'); ?></a>
 			</div>
 			</div>
 		</div>
 		</div>
 		<div class="form-group">
 		<div class="form-group">

+ 2 - 2
app/views/helpers/index/normal/entry_bottom.phtml

@@ -52,7 +52,7 @@
 						$share_options['title'] = $title;
 						$share_options['title'] = $title;
 						$share->update($share_options);
 						$share->update($share_options);
 				?><li class="item share">
 				?><li class="item share">
-					<a target="_blank" href="<?php echo $share->url(); ?>"><?php echo $share->name(); ?></a>
+					<a target="_blank" rel="noreferrer" href="<?php echo $share->url(); ?>"><?php echo $share->name(); ?></a>
 				</li><?php
 				</li><?php
 					}
 					}
 			?></ul>
 			?></ul>
@@ -81,6 +81,6 @@
 		?><li class="item date"><?php echo $this->entry->date(); ?></li><?php
 		?><li class="item date"><?php echo $this->entry->date(); ?></li><?php
 	}
 	}
 	if ($bottomline_link) {
 	if ($bottomline_link) {
-		?><li class="item link"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php
+		?><li class="item link"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php
 	} ?>
 	} ?>
 </ul>
 </ul>

+ 2 - 2
app/views/helpers/index/normal/entry_header.phtml

@@ -27,7 +27,7 @@
 		}
 		}
 	}
 	}
 	?><li class="item website"><a href="<?php echo _url('index', 'index', 'get', 'f_' . $this->feed->id()); ?>"><img class="favicon" src="<?php echo $this->feed->favicon(); ?>" alt="✇" /> <span><?php echo $this->feed->name(); ?></span></a></li>
 	?><li class="item website"><a href="<?php echo _url('index', 'index', 'get', 'f_' . $this->feed->id()); ?>"><img class="favicon" src="<?php echo $this->feed->favicon(); ?>" alt="✇" /> <span><?php echo $this->feed->name(); ?></span></a></li>
-	<li class="item title"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></li>
+	<li class="item title"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></li>
 	<?php if ($topline_date) { ?><li class="item date"><?php echo $this->entry->date(); ?> </li><?php } ?>
 	<?php if ($topline_date) { ?><li class="item date"><?php echo $this->entry->date(); ?> </li><?php } ?>
-	<?php if ($topline_link) { ?><li class="item link"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php } ?>
+	<?php if ($topline_link) { ?><li class="item link"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php } ?>
 </ul>
 </ul>

+ 2 - 1
app/views/helpers/pagination.phtml

@@ -1,6 +1,7 @@
 <?php
 <?php
 	$url_next = Minz_Request::currentRequest();
 	$url_next = Minz_Request::currentRequest();
 	$url_next['params']['next'] = FreshRSS_Context::$next_id;
 	$url_next['params']['next'] = FreshRSS_Context::$next_id;
+	$url_next['params']['state'] = FreshRSS_Context::$state;
 	$url_next['params']['ajax'] = 1;
 	$url_next['params']['ajax'] = 1;
 
 
 	$url_mark_read = array(
 	$url_mark_read = array(
@@ -26,7 +27,7 @@
 		</a>
 		</a>
 	<?php } elseif ($url_mark_read) { ?>
 	<?php } elseif ($url_mark_read) { ?>
 		<button id="bigMarkAsRead"
 		<button id="bigMarkAsRead"
-		        class="as-link <?php echo FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?>"
+		        class="as-link <?php echo FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : ''; ?>"
 		        form="mark-read-pagination"
 		        form="mark-read-pagination"
 		        formaction="<?php echo Minz_Url::display($url_mark_read); ?>"
 		        formaction="<?php echo Minz_Url::display($url_mark_read); ?>"
 		        type="submit">
 		        type="submit">

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

@@ -44,7 +44,7 @@
 						$select_args = ' size="' . min(10, count($this->feeds)) .'" multiple="multiple"';
 						$select_args = ' size="' . min(10, count($this->feeds)) .'" multiple="multiple"';
 					}
 					}
 				?>
 				?>
-				<select name="export_feeds[]"<?php echo $select_args; ?>>
+				<select name="export_feeds[]"<?php echo $select_args; ?> size="10">
 					<?php echo extension_loaded('zip') ? '' : '<option></option>'; ?>
 					<?php echo extension_loaded('zip') ? '' : '<option></option>'; ?>
 					<?php foreach ($this->feeds as $feed) { ?>
 					<?php foreach ($this->feeds as $feed) { ?>
 					<option value="<?php echo $feed->id(); ?>"><?php echo $feed->name(); ?></option>
 					<option value="<?php echo $feed->id(); ?>"><?php echo $feed->name(); ?></option>

+ 4 - 1
app/views/index/global.phtml

@@ -11,10 +11,13 @@
 
 
 <div id="stream" class="global<?php echo $class; ?>">
 <div id="stream" class="global<?php echo $class; ?>">
 <?php
 <?php
+	$params = Minz_Request::fetchGET();
+	unset($params['c']);
+	unset($params['a']);
 	$url_base = array(
 	$url_base = array(
 		'c' => 'index',
 		'c' => 'index',
 		'a' => 'normal',
 		'a' => 'normal',
-		'params' => Minz_Request::fetchGET(),
+		'params' => $params,
 	);
 	);
 
 
 	foreach ($this->categories as $cat) {
 	foreach ($this->categories as $cat) {

+ 1 - 1
app/views/index/normal.phtml

@@ -66,7 +66,7 @@ if (!empty($this->entries)) {
 
 
 		?><div class="flux_content">
 		?><div class="flux_content">
 			<div class="content <?php echo $content_width; ?>">
 			<div class="content <?php echo $content_width; ?>">
-				<h1 class="title"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></h1>
+				<h1 class="title"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></h1>
 				<?php
 				<?php
 					$author = $this->entry->author();
 					$author = $this->entry->author();
 					echo $author != '' ? '<div class="author">' . _t('gen.short.by_author', $author) . '</div>' : '',
 					echo $author != '' ? '<div class="author">' . _t('gen.short.by_author', $author) . '</div>' : '',

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

@@ -23,18 +23,18 @@
 				</tr>
 				</tr>
 				<tr>
 				<tr>
 					<th><?php echo _t('admin.stats.status_read'); ?></th>
 					<th><?php echo _t('admin.stats.status_read'); ?></th>
-					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['read']); ?></td>
-					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['read']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_reads']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_reads']); ?></td>
 				</tr>
 				</tr>
 				<tr>
 				<tr>
 					<th><?php echo _t('admin.stats.status_unread'); ?></th>
 					<th><?php echo _t('admin.stats.status_unread'); ?></th>
-					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['unread']); ?></td>
-					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['unread']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_unreads']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_unreads']); ?></td>
 				</tr>
 				</tr>
 				<tr>
 				<tr>
 					<th><?php echo _t('admin.stats.status_favorites'); ?></th>
 					<th><?php echo _t('admin.stats.status_favorites'); ?></th>
-					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['favorite']); ?></td>
-					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['favorite']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_favorites']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_favorites']); ?></td>
 				</tr>
 				</tr>
 			</tbody>
 			</tbody>
 		</table>
 		</table>

+ 4 - 4
app/views/stats/repartition.phtml

@@ -12,7 +12,7 @@
 		if (!empty($feeds)) {
 		if (!empty($feeds)) {
 			echo '<optgroup label="', $category->name(), '">';
 			echo '<optgroup label="', $category->name(), '">';
 			foreach ($feeds as $feed) {
 			foreach ($feeds as $feed) {
-				if ($this->feed && $feed->id() == $this->feed->id()){
+				if ($this->feed && $feed->id() == $this->feed->id()) {
 					echo '<option value="', $feed->id(), '" selected="selected" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
 					echo '<option value="', $feed->id(), '" selected="selected" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
 				} else {
 				} else {
 					echo '<option value="', $feed->id(), '" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
 					echo '<option value="', $feed->id(), '" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
@@ -39,9 +39,9 @@
 		</tr>
 		</tr>
 		<tr>
 		<tr>
 			<td class="numeric"><?php echo $this->repartition['total']; ?></td>
 			<td class="numeric"><?php echo $this->repartition['total']; ?></td>
-			<td class="numeric"><?php echo $this->repartition['read']; ?></td>
-			<td class="numeric"><?php echo $this->repartition['unread']; ?></td>
-			<td class="numeric"><?php echo $this->repartition['favorite']; ?></td>
+			<td class="numeric"><?php echo $this->repartition['count_reads']; ?></td>
+			<td class="numeric"><?php echo $this->repartition['count_unreads']; ?></td>
+			<td class="numeric"><?php echo $this->repartition['count_favorites']; ?></td>
 		</tr>
 		</tr>
 		</table>
 		</table>
 	</div>
 	</div>

+ 2 - 2
app/views/user/manage.phtml

@@ -3,7 +3,7 @@
 <div class="post">
 <div class="post">
 	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('gen.action.back_to_rss_feeds'); ?></a>
 	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('gen.action.back_to_rss_feeds'); ?></a>
 
 
-	<form method="post" action="<?php echo _url('user', 'create'); ?>">
+	<form method="post" action="<?php echo _url('user', 'create'); ?>" autocomplete="off">
 		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
 		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
 		<legend><?php echo _t('admin.user.create'); ?></legend>
 		<legend><?php echo _t('admin.user.create'); ?></legend>
 
 
@@ -30,7 +30,7 @@
 			<label class="group-name" for="new_user_passwordPlain"><?php echo _t('admin.user.password_form'); ?></label>
 			<label class="group-name" for="new_user_passwordPlain"><?php echo _t('admin.user.password_form'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
 				<div class="stick">
 				<div class="stick">
-					<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="off" pattern=".{7,}" />
+					<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="new-password" pattern=".{7,}" />
 					<a class="btn toggle-password" data-toggle="new_user_passwordPlain"><?php echo _i('key'); ?></a>
 					<a class="btn toggle-password" data-toggle="new_user_passwordPlain"><?php echo _i('key'); ?></a>
 				</div>
 				</div>
 				<?php echo _i('help'); ?> <?php echo _t('admin.user.password_format'); ?>
 				<?php echo _i('help'); ?> <?php echo _t('admin.user.password_format'); ?>

+ 3 - 0
cli/.htaccess

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

+ 58 - 0
cli/README.md

@@ -0,0 +1,58 @@
+* Back to [main read-me](../README.md)
+
+# FreshRSS Command-Line Interface (CLI)
+
+## Note on access rights
+
+When using the command-line interface, remember that your user might not be the same as the one used by your Web server.
+This might create some access right problems.
+
+It is recommended to invoke commands using the same user as your Web server:
+
+```sh
+cd /usr/share/FreshRSS
+sudo -u www-data sh -c './cli/list-users.php'
+```
+
+In any case, when you are done with a series of commands, you should re-apply the access rights:
+
+```sh
+cd /usr/share/FreshRSS
+sudo chown -R :www-data .
+sudo chmod -R g+r .
+sudo chmod -R g+w ./data/
+```
+
+
+## Commands
+
+Options in parenthesis are optional.
+
+
+```sh
+cd /usr/share/FreshRSS
+
+./cli/do-install.php --default_user admin --auth_type form  ( --environment production --base_url https://rss.example.net/ --title FreshRSS --allow_anonymous --api_enabled --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123 --db-base freshrss --db-prefix freshrss )
+# --auth_type can be: 'form' (recommended), 'http_auth' (using the Web server access control), 'none' (dangerous)
+# --db-type can be: 'sqlite' (default), 'mysql' (MySQL or MariaDB), 'pgsql' (PostgreSQL)
+# --environment can be: 'production' (default), 'development' (for additional log messages)
+# --db-prefix is an optional prefix in front of the names of the tables
+# This command does not create the default user. Do that with ./cli/create-user.php
+
+./cli/create-user.php --user username ( --password 'password' --api-password 'api_password' --language en --email user@example.net --token 'longRandomString' --no-default-feeds )
+# --language can be: 'en' (default), 'fr', or one of the [supported languages](../app/i18n/)
+
+./cli/delete-user.php --user username
+
+./cli/list-users.php
+# Return a list of users, with the default/admin user first
+
+./cli/actualize-user.php --user username
+
+./cli/import-for-user.php --user username --filename /path/to/file.ext
+# The extension of the file { .json, .opml, .xml, .zip } is used to detect the type of import
+
+./cli/export-opml-for-user.php --user username > /path/to/file.opml.xml
+
+./cli/export-zip-for-user.php --user username ( --max-feed-entries 100 ) > /path/to/file.zip
+```

+ 49 - 0
cli/_cli.php

@@ -0,0 +1,49 @@
+<?php
+if (php_sapi_name() !== 'cli') {
+	die('FreshRSS error: This PHP script may only be invoked from command line!');
+}
+
+require(dirname(__FILE__) . '/../constants.php');
+require(LIB_PATH . '/lib_rss.php');
+
+Minz_Configuration::register('system',
+	DATA_PATH . '/config.php',
+	DATA_PATH . '/config.default.php');
+FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
+Minz_Translate::init('en');
+
+FreshRSS_Context::$isCli = true;
+
+function fail($message) {
+	fwrite(STDERR, $message . "\n");
+	die(1);
+}
+
+function cliInitUser($username) {
+	if (!ctype_alnum($username)) {
+		fail('FreshRSS error: invalid username: ' . $username . "\n");
+	}
+
+	$usernames = listUsers();
+	if (!in_array($username, $usernames)) {
+		fail('FreshRSS error: user not found: ' . $username . "\n");
+	}
+
+	FreshRSS_Context::$user_conf = get_user_configuration($username);
+	if (FreshRSS_Context::$user_conf == null) {
+		fail('FreshRSS error: invalid configuration for user: ' . $username . "\n");
+	}
+	new Minz_ModelPdo($username);
+
+	return $username;
+}
+
+function accessRights() {
+	echo '• Remember to re-apply the appropriate access rights, such as:' , "\n",
+		"\t", 'sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/', "\n";
+}
+
+function done($ok = true) {
+	fwrite(STDERR, 'Result: ' . ($ok ? 'success' : 'fail') . "\n");
+	exit($ok ? 0 : 1);
+}

+ 23 - 0
cli/actualize-user.php

@@ -0,0 +1,23 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+		'user:',
+	));
+
+if (empty($options['user'])) {
+	fail('Usage: ' . basename(__FILE__) . " --user username");
+}
+
+$username = cliInitUser($options['user']);
+
+fwrite(STDERR, 'FreshRSS actualizing user “' . $username . "”…\n");
+
+list($nbUpdatedFeeds, $feed) = FreshRSS_feed_Controller::actualizeFeed(0, '', true);
+
+echo "FreshRSS actualized $nbUpdatedFeeds feeds for $username\n";
+
+invalidateHttpCache($username);
+
+done($nbUpdatedFeeds > 0);

+ 48 - 0
cli/create-user.php

@@ -0,0 +1,48 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+		'user:',
+		'password:',
+		'api-password:',
+		'language:',
+		'email:',
+		'token:',
+		'no-default-feeds',
+	));
+
+if (empty($options['user'])) {
+	fail('Usage: ' . basename(__FILE__) . " --user username ( --password 'password' --api-password 'api_password'" .
+		" --language en --email user@example.net --token 'longRandomString --no-default-feeds' )");
+}
+$username = $options['user'];
+if (!ctype_alnum($username)) {
+	fail('FreshRSS error: invalid username “' . $username . '”');
+}
+
+$usernames = listUsers();
+if (preg_grep("/^$username$/i", $usernames)) {
+	fail('FreshRSS error: username already taken “' . $username . '”');
+}
+
+echo 'FreshRSS creating user “', $username, "”…\n";
+
+$ok = FreshRSS_user_Controller::createUser($username,
+	empty($options['password']) ? '' : $options['password'],
+	empty($options['api-password']) ? '' : $options['api-password'],
+	array(
+		'language' => empty($options['language']) ? '' : $options['language'],
+		'token' => empty($options['token']) ? '' : $options['token'],
+	),
+	!isset($options['no-default-feeds']));
+
+if (!$ok) {
+	fail('FreshRSS could not create user!');
+}
+
+invalidateHttpCache(FreshRSS_Context::$system_conf->default_user);
+
+accessRights();
+
+done($ok);

+ 32 - 0
cli/delete-user.php

@@ -0,0 +1,32 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+		'user:',
+	));
+
+if (empty($options['user'])) {
+	fail('Usage: ' . basename(__FILE__) . " --user username");
+}
+$username = $options['user'];
+if (!ctype_alnum($username)) {
+	fail('FreshRSS error: invalid username “' . $username . '”');
+}
+
+$usernames = listUsers();
+if (!preg_grep("/^$username$/i", $usernames)) {
+	fail('FreshRSS error: username not found “' . $username . '”');
+}
+
+if (strcasecmp($username, FreshRSS_Context::$system_conf->default_user) === 0) {
+	fail('FreshRSS error: default user must not be deleted: “' . $username . '”');
+}
+
+echo 'FreshRSS deleting user “', $username, "”…\n";
+
+$ok = FreshRSS_user_Controller::deleteUser($username);
+
+invalidateHttpCache(FreshRSS_Context::$system_conf->default_user);
+
+done($ok);

+ 101 - 0
cli/do-install.php

@@ -0,0 +1,101 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+require(LIB_PATH . '/lib_install.php');
+
+$params = array(
+		'environment:',
+		'base_url:',
+		'title:',
+		'default_user:',
+		'allow_anonymous',
+		'allow_anonymous_refresh',
+		'auth_type:',
+		'api_enabled',
+		'allow_robots',
+	);
+
+$dBparams = array(
+		'db-type:',
+		'db-host:',
+		'db-user:',
+		'db-password:',
+		'db-base:',
+		'db-prefix:',
+	);
+
+$options = getopt('', array_merge($params, $dBparams));
+
+if (empty($options['default_user']) || empty($options['auth_type'])) {
+	fail('Usage: ' . basename(__FILE__) . " --default_user admin --auth_type form" .
+		" ( --environment production --base_url https://rss.example.net/" .
+		" --title FreshRSS --allow_anonymous --api_enabled" .
+		" --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123" .
+		" --db-base freshrss --db-prefix freshrss )");
+}
+
+fwrite(STDERR, 'FreshRSS install…' . "\n");
+
+$requirements = checkRequirements();
+if ($requirements['all'] !== 'ok') {
+	$message = 'FreshRSS install failed requirements:' . "\n";
+	foreach ($requirements as $requirement => $check) {
+		if ($check !== 'ok' && $requirement !== 'all') {
+			$message .= '• ' . $requirement . "\n";
+		}
+	}
+	fail($message);
+}
+
+if (!ctype_alnum($options['default_user'])) {
+	fail('FreshRSS invalid default username (must be ASCII alphanumeric): ' . $options['default_user']);
+}
+
+if (!in_array($options['auth_type'], array('form', 'http_auth', 'none'))) {
+	fail('FreshRSS invalid authentication method (auth_type must be one of { form, http_auth, none }: ' . $options['auth_type']);
+}
+
+$config = array(
+		'salt' => generateSalt(),
+		'db' => FreshRSS_Context::$system_conf->db,
+	);
+
+foreach ($params as $param) {
+	$param = rtrim($param, ':');
+	if (isset($options[$param])) {
+		$config[$param] = $options[$param] === false ? true : $options[$param];
+	}
+}
+
+if ((!empty($config['base_url'])) && server_is_public($config['base_url'])) {
+	$config['pubsubhubbub_enabled'] = true;
+}
+
+foreach ($dBparams as $dBparam) {
+	$dBparam = rtrim($dBparam, ':');
+	if (!empty($options[$dBparam])) {
+		$param = substr($dBparam, strlen('db-'));
+		$config['db'][$param] = $options[$dBparam];
+	}
+}
+
+if (file_put_contents(join_path(DATA_PATH, 'config.php'), "<?php\n return " . var_export($config, true) . ";\n") === false) {
+	fail('FreshRSS could not write configuration file!: ' . join_path(DATA_PATH, 'config.php'));
+}
+
+$config['db']['default_user'] = $config['default_user'];
+if (!checkDb($config['db'])) {
+	@unlink(join_path(DATA_PATH, 'config.php'));
+	fail('FreshRSS database error: ' . (empty($config['db']['bd_error']) ? 'Unknown error' : $config['db']['bd_error']));
+}
+
+echo '• Remember to create the default user: ', $config['default_user'] , "\n",
+	"\t", './cli/create-user.php --user ', $config['default_user'] , " --password 'password' --more-options\n";
+
+accessRights();
+
+if (!deleteInstall()) {
+	fail('FreshRSS access right problem while deleting install file!');
+}
+
+done();

+ 24 - 0
cli/export-opml-for-user.php

@@ -0,0 +1,24 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+		'user:',
+	));
+
+if (empty($options['user'])) {
+	fail('Usage: ' . basename(__FILE__) . " --user username > /path/to/file.opml.xml");
+}
+
+$username = cliInitUser($options['user']);
+
+fwrite(STDERR, 'FreshRSS exporting OPML for user “' . $username . "”…\n");
+
+$importController = new FreshRSS_importExport_Controller();
+
+$ok = false;
+$ok = $importController->exportFile(true, false, array(), 0, $username);
+
+invalidateHttpCache($username);
+
+done($ok);

+ 30 - 0
cli/export-zip-for-user.php

@@ -0,0 +1,30 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+		'user:',
+		'max-feed-entries:',
+	));
+
+if (empty($options['user'])) {
+	fail('Usage: ' . basename(__FILE__) . " --user username ( --max-feed-entries 100 ) > /path/to/file.zip");
+}
+
+$username = cliInitUser($options['user']);
+
+fwrite(STDERR, 'FreshRSS exporting ZIP for user “' . $username . "”…\n");
+
+$importController = new FreshRSS_importExport_Controller();
+
+$ok = false;
+try {
+	$ok = $importController->exportFile(true, true, true,
+		empty($options['max-feed-entries']) ? 100 : intval($options['max-feed-entries']),
+		$username);
+} catch (FreshRSS_ZipMissing_Exception $zme) {
+	fail('FreshRSS error: Lacking php-zip extension!');
+}
+invalidateHttpCache($username);
+
+done($ok);

+ 35 - 0
cli/import-for-user.php

@@ -0,0 +1,35 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+		'user:',
+		'filename:',
+	));
+
+if (empty($options['user']) || empty($options['filename'])) {
+	fail('Usage: ' . basename(__FILE__) . " --user username --filename /path/to/file.ext");
+}
+
+$username = cliInitUser($options['user']);
+
+$filename = $options['filename'];
+if (!is_readable($filename)) {
+	fail('FreshRSS error: file is not readable “' . $filename . '”');
+}
+
+echo 'FreshRSS importing ZIP/OPML/JSON for user “', $username, "”…\n";
+
+$importController = new FreshRSS_importExport_Controller();
+
+$ok = false;
+try {
+	$ok = $importController->importFile($filename, $filename, $username);
+} catch (FreshRSS_ZipMissing_Exception $zme) {
+	fail('FreshRSS error: Lacking php-zip extension!');
+} catch (FreshRSS_Zip_Exception $ze) {
+	fail('FreshRSS error: ZIP archive cannot be imported! Error code: ' . $ze->zipErrorCode());
+}
+invalidateHttpCache($username);
+
+done($ok);

+ 0 - 0
data/persona/index.html → cli/index.html


+ 14 - 0
cli/list-users.php

@@ -0,0 +1,14 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$users = listUsers();
+sort($users);
+if (FreshRSS_Context::$system_conf->default_user !== '') {
+	array_unshift($users, FreshRSS_Context::$system_conf->default_user);
+	$users = array_unique($users);
+}
+
+foreach ($users as $user) {
+	echo $user, "\n";
+}

+ 1 - 1
constants.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-define('FRESHRSS_VERSION', '1.5.0');
+define('FRESHRSS_VERSION', '1.6.0');
 define('FRESHRSS_WEBSITE', 'http://freshrss.org');
 define('FRESHRSS_WEBSITE', 'http://freshrss.org');
 define('FRESHRSS_WIKI', 'http://doc.freshrss.org');
 define('FRESHRSS_WIKI', 'http://doc.freshrss.org');
 
 

+ 3 - 7
data/.gitignore

@@ -1,10 +1,6 @@
-application.ini
 config.php
 config.php
-*.sqlite
-touch.txt
-no-cache.txt
-*.bak.php
-*.lock.txt
+config.php.bak.php
+force-https.txt
 last_update.txt
 last_update.txt
+no-cache.txt
 update.php
 update.php
-force-https.txt

+ 3 - 1
data/PubSubHubbub/feeds/.gitignore

@@ -1 +1,3 @@
-*/*
+*/
+*/*.json
+*/*.txt

+ 3 - 0
data/config.default.php

@@ -27,6 +27,9 @@ return array(
 	# Title of this FreshRSS instance in the Web user interface.
 	# Title of this FreshRSS instance in the Web user interface.
 	'title' => 'FreshRSS',
 	'title' => 'FreshRSS',
 
 
+	# Meta description used when `allow_robots` is true.
+	'meta_description' => '',
+
 	# Name of the user that has administration rights.
 	# Name of the user that has administration rights.
 	'default_user' => '_',
 	'default_user' => '_',
 
 

Деякі файли не було показано, через те що забагато файлів було змінено