فهرست منبع

Merge pull request #2599 from FreshRSS/dev

FreshRSS 1.15
Alexandre Alapetite 6 سال پیش
والد
کامیت
3aa66f317b
100فایلهای تغییر یافته به همراه2484 افزوده شده و 1252 حذف شده
  1. 1 0
      .github/FUNDING.yml
  2. 4 0
      .stylelintignore
  3. 74 0
      .stylelintrc
  4. 6 11
      .travis.yml
  5. 63 4
      CHANGELOG.md
  6. 1 1
      CONTRIBUTING.md
  7. 11 0
      CREDITS.md
  8. 5 2
      Docker/Dockerfile
  9. 4 1
      Docker/Dockerfile-Alpine
  10. 4 1
      Docker/Dockerfile-QEMU-ARM
  11. 50 35
      Docker/README.md
  12. 18 25
      Docker/docker-compose.yml
  13. 3 0
      Docker/entrypoint.sh
  14. 38 0
      Makefile
  15. 13 12
      README.fr.md
  16. 12 88
      README.md
  17. 11 3
      app/.htaccess
  18. 10 4
      app/Controllers/authController.php
  19. 54 5
      app/Controllers/configureController.php
  20. 5 17
      app/Controllers/entryController.php
  21. 2 2
      app/Controllers/extensionController.php
  22. 4 24
      app/Controllers/feedController.php
  23. 19 3
      app/Controllers/importExportController.php
  24. 19 2
      app/Controllers/indexController.php
  25. 1 1
      app/Controllers/javascriptController.php
  26. 61 6
      app/Controllers/subscriptionController.php
  27. 2 2
      app/Controllers/tagController.php
  28. 1 1
      app/Controllers/updateController.php
  29. 230 46
      app/Controllers/userController.php
  30. 22 0
      app/FreshRSS.php
  31. 31 0
      app/Mailers/UserMailer.php
  32. 1 6
      app/Models/Auth.php
  33. 29 2
      app/Models/Category.php
  34. 186 65
      app/Models/CategoryDAO.php
  35. 17 0
      app/Models/CategoryDAOSQLite.php
  36. 7 10
      app/Models/ConfigurationSetter.php
  37. 18 0
      app/Models/Context.php
  38. 215 35
      app/Models/DatabaseDAO.php
  39. 28 21
      app/Models/DatabaseDAOPGSQL.php
  40. 18 18
      app/Models/DatabaseDAOSQLite.php
  41. 4 8
      app/Models/Entry.php
  42. 222 245
      app/Models/EntryDAO.php
  43. 17 11
      app/Models/EntryDAOPGSQL.php
  44. 46 46
      app/Models/EntryDAOSQLite.php
  45. 11 1
      app/Models/Factory.php
  46. 31 24
      app/Models/Feed.php
  47. 103 105
      app/Models/FeedDAO.php
  48. 2 2
      app/Models/FeedDAOSQLite.php
  49. 16 31
      app/Models/StatsDAO.php
  50. 2 3
      app/Models/StatsDAOPGSQL.php
  51. 2 3
      app/Models/StatsDAOSQLite.php
  52. 1 1
      app/Models/Tag.php
  53. 74 65
      app/Models/TagDAO.php
  54. 1 1
      app/Models/TagDAOSQLite.php
  55. 29 60
      app/Models/UserDAO.php
  56. 44 60
      app/SQL/install.sql.mysql.php
  57. 50 51
      app/SQL/install.sql.pgsql.php
  58. 47 40
      app/SQL/install.sql.sqlite.php
  59. 1 0
      app/i18n/cz/admin.php
  60. 12 3
      app/i18n/cz/conf.php
  61. 10 1
      app/i18n/cz/gen.php
  62. 4 1
      app/i18n/cz/index.php
  63. 5 1
      app/i18n/cz/sub.php
  64. 37 0
      app/i18n/cz/user.php
  65. 1 0
      app/i18n/de/admin.php
  66. 12 3
      app/i18n/de/conf.php
  67. 10 1
      app/i18n/de/gen.php
  68. 4 1
      app/i18n/de/index.php
  69. 5 1
      app/i18n/de/sub.php
  70. 37 0
      app/i18n/de/user.php
  71. 1 0
      app/i18n/en/admin.php
  72. 12 3
      app/i18n/en/conf.php
  73. 11 1
      app/i18n/en/gen.php
  74. 4 1
      app/i18n/en/index.php
  75. 5 1
      app/i18n/en/sub.php
  76. 37 0
      app/i18n/en/user.php
  77. 1 0
      app/i18n/es/admin.php
  78. 12 3
      app/i18n/es/conf.php
  79. 10 1
      app/i18n/es/gen.php
  80. 4 1
      app/i18n/es/index.php
  81. 5 1
      app/i18n/es/sub.php
  82. 37 0
      app/i18n/es/user.php
  83. 1 0
      app/i18n/fr/admin.php
  84. 12 3
      app/i18n/fr/conf.php
  85. 10 1
      app/i18n/fr/gen.php
  86. 4 1
      app/i18n/fr/index.php
  87. 5 1
      app/i18n/fr/sub.php
  88. 37 0
      app/i18n/fr/user.php
  89. 1 0
      app/i18n/he/admin.php
  90. 12 3
      app/i18n/he/conf.php
  91. 10 1
      app/i18n/he/gen.php
  92. 4 1
      app/i18n/he/index.php
  93. 5 1
      app/i18n/he/sub.php
  94. 37 0
      app/i18n/he/user.php
  95. 1 0
      app/i18n/it/admin.php
  96. 12 3
      app/i18n/it/conf.php
  97. 10 1
      app/i18n/it/gen.php
  98. 4 1
      app/i18n/it/index.php
  99. 5 1
      app/i18n/it/sub.php
  100. 37 0
      app/i18n/it/user.php

+ 1 - 0
.github/FUNDING.yml

@@ -0,0 +1 @@
+liberapay: FreshRSS

+ 4 - 0
.stylelintignore

@@ -0,0 +1,4 @@
+# ignore SASS-generated CSS
+p/themes/Ansum/*.css
+p/themes/Mapco/*.css
+p/themes/Swage/*.css

+ 74 - 0
.stylelintrc

@@ -0,0 +1,74 @@
+{
+  "extends": "stylelint-config-recommended-scss",
+  "plugins": [
+    "stylelint-order",
+    "stylelint-scss"
+  ],
+  "rules": {
+    "at-rule-empty-line-before": [
+      "always", {
+        "ignoreAtRules": [ "after-comment", "else" ]
+      }
+    ],
+    "at-rule-name-space-after": [
+      "always", {
+        "ignoreAtRules": [ "after-comment" ]
+      }
+    ],
+    "block-closing-brace-newline-after": [
+      "always", {
+        "ignoreAtRules": [ "if", "else" ]
+      }
+    ],
+    "block-closing-brace-newline-before": "always-multi-line",
+    "block-opening-brace-newline-after": "always-multi-line",
+    "block-opening-brace-space-before": "always",
+    "color-hex-case": "lower",
+    "color-hex-length": "short",
+    "color-no-invalid-hex": true,
+    "declaration-colon-space-after": "always",
+    "declaration-colon-space-before": "never",
+    "indentation": "tab",
+    "no-descending-specificity": null,
+    "no-eol-whitespace": true,
+    "property-no-vendor-prefix": true,
+    "rule-empty-line-before": [
+      "always",
+      "except": [
+        "after-single-line-comment",
+        "first-nested"
+      ]
+    ],
+    "order/properties-order": [
+      "margin",
+      "padding",
+      "background",
+      "display",
+      "float",
+      "max-width",
+      "width",
+      "max-height",
+      "height",
+      "color",
+      "font",
+      "font-family",
+      "font-size",
+      "border",
+      "border-top",
+      "border-top-color",
+      "border-right",
+      "border-right-color",
+      "border-bottom",
+      "border-bottom-color",
+      "border-left",
+      "border-left-color",
+      "border-radius",
+      "box-shadow"
+    ],
+    "scss/at-else-closing-brace-newline-after": "always-last-in-chain",
+    "scss/at-else-closing-brace-space-after": "always-intermediate",
+    "scss/at-else-empty-line-before": "never",
+    "scss/at-if-closing-brace-newline-after": "always-last-in-chain",
+    "scss/at-if-closing-brace-space-after": "always-intermediate"
+  }
+}

+ 6 - 11
.travis.yml

@@ -1,11 +1,6 @@
 language: php
 language: php
 php:
 php:
-  - 5.4
-  - 5.5
   - 5.6
   - 5.6
-  - 7.0
-  - 7.1
-  - 7.2
   - 7.3
   - 7.3
 
 
 install:
 install:
@@ -14,7 +9,7 @@ install:
 
 
 script:
 script:
   - phpenv rehash
   - phpenv rehash
-  - find . -not -path "./lib/JSON.php" -name \*.php -print0 | xargs -0 -n1 -P4 php -l 1>/dev/null 2>php-l-results
+  - find . -name \*.php -print0 | xargs -0 -n1 -P4 php -l 1>/dev/null 2>php-l-results
   - if [ -s php-l-results ]; then cat php-l-results; exit 1; fi
   - if [ -s php-l-results ]; then cat php-l-results; exit 1; fi
   - |
   - |
     if [[ $VALIDATE_STANDARD == yes ]]; then
     if [[ $VALIDATE_STANDARD == yes ]]; then
@@ -32,9 +27,6 @@ env:
 matrix:
 matrix:
   fast_finish: true
   fast_finish: true
   include:
   include:
-    # PHP 5.3 only runs on Ubuntu 12.04 (precise), not 14.04 (trusty)
-    - php: "5.3"
-      dist: precise
     - php: "7.2"
     - php: "7.2"
       env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
       env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
     - language: node_js
     - language: node_js
@@ -45,12 +37,15 @@ matrix:
       env:
       env:
         - HADOLINT="$HOME/hadolint"
         - HADOLINT="$HOME/hadolint"
       install:
       install:
-        - npm install jshint
+        - npm install --save-dev jshint stylelint stylelint-order stylelint-scss stylelint-config-recommended-scss
         - curl -sLo "$HADOLINT" $(curl -s https://api.github.com/repos/hadolint/hadolint/releases/latest?access_token="$GITHUB_TOKEN" | jq -r '.assets | .[] | select(.name=="hadolint-Linux-x86_64") | .browser_download_url') && chmod 700 ${HADOLINT}
         - curl -sLo "$HADOLINT" $(curl -s https://api.github.com/repos/hadolint/hadolint/releases/latest?access_token="$GITHUB_TOKEN" | jq -r '.assets | .[] | select(.name=="hadolint-Linux-x86_64") | .browser_download_url') && chmod 700 ${HADOLINT}
       script:
       script:
         - node_modules/jshint/bin/jshint .
         - node_modules/jshint/bin/jshint .
+        # check SCSS separately
+        - stylelint --syntax scss "**/*.scss"
+        - stylelint "**/*.css"
         - bash tests/shellchecks.sh
         - bash tests/shellchecks.sh
         - git ls-files --exclude='*Dockerfile*' --ignored | xargs --max-lines=1 "$HADOLINT"
         - git ls-files --exclude='*Dockerfile*' --ignored | xargs --max-lines=1 "$HADOLINT"
   allow_failures:
   allow_failures:
     - env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
     - env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
-    - dist: precise
+

+ 63 - 4
CHANGELOG.md

@@ -1,5 +1,64 @@
 # FreshRSS changelog
 # FreshRSS changelog
 
 
+## 2019-10-31 FreshRSS 1.15.0
+
+* CLI
+	* Command line to export/import any database to/from SQLite [#2496](https://github.com/FreshRSS/FreshRSS/pull/2496)
+* Features
+	* New archiving method, including maximum number of articles per feed, and settings at feed, category, global levels [#2335](https://github.com/FreshRSS/FreshRSS/pull/2335)
+	* New option to control category sort order [#2592](https://github.com/FreshRSS/FreshRSS/pull/2592)
+	* New option to display article authors underneath the article title [#2487](https://github.com/FreshRSS/FreshRSS/pull/2487)
+	* Add e-mail capability [#2476](https://github.com/FreshRSS/FreshRSS/pull/2476), [#2481](https://github.com/FreshRSS/FreshRSS/pull/2481)
+	* Ability to define default user settings in `data/config-user.custom.php` [#2490](https://github.com/FreshRSS/FreshRSS/pull/2490)
+		* Including default feeds [#2515](https://github.com/FreshRSS/FreshRSS/pull/2515)
+	* Allow recreating users if they still exist in database [#2555](https://github.com/FreshRSS/FreshRSS/pull/2555)
+	* Add optional database connection URI parameters [#2549](https://github.com/FreshRSS/FreshRSS/issues/2549), [#2559](https://github.com/FreshRSS/FreshRSS/pull/2559)
+	* Allow longer articles with MySQL / MariaDB (up to 16MB compressed instead of 64kB) [#2448](https://github.com/FreshRSS/FreshRSS/issues/2448)
+	* Add support for terms of service [#2520](https://github.com/FreshRSS/FreshRSS/pull/2520)
+	* Add sharing with [Lemmy](https://github.com/dessalines/lemmy) [#2510](https://github.com/FreshRSS/FreshRSS/pull/2510)
+* API
+	* Add support for [Reeder-4](https://www.reederapp.com/) client [#2513](https://github.com/FreshRSS/FreshRSS/issues/2513)
+* Compatibility
+	* Require at least PHP 5.6+ [#2495](https://github.com/FreshRSS/FreshRSS/pull/2495), [#2527](https://github.com/FreshRSS/FreshRSS/pull/2527), [#2585](https://github.com/FreshRSS/FreshRSS/pull/2585)
+	* Require `php-json` and remove remove `JSON.php` fallback [#2528](https://github.com/FreshRSS/FreshRSS/pull/2528)
+	* Require at least PostgreSQL 9.5+ [#2554](https://github.com/FreshRSS/FreshRSS/pull/2554)
+* Deployment
+	* Take advantage of `mod_authz_core` instead of `mod_access_compat` when running on Apache 2.4+ [#2461](https://github.com/FreshRSS/FreshRSS/pull/2461)
+	* Docker: Ubuntu image updated to 19.10 with PHP 7.3.8 and Apache 2.4.41 [#2577](https://github.com/FreshRSS/FreshRSS/pull/2577)
+	* Docker: Alpine image updated to 3.10 with PHP 7.3.11 and Apache 2.4.41 [#2238](https://github.com/FreshRSS/FreshRSS/pull/2238)
+	* Docker: Increase default PHP POST/upload size to ease importing ZIP files [#2563](https://github.com/FreshRSS/FreshRSS/pull/2563)
+	* New environment variable `COPY_LOG_TO_SYSLOG` to see all logs at once in e.g. `docker logs -f` [#2591](https://github.com/FreshRSS/FreshRSS/pull/2591)
+	* New environment variable `FRESHRSS_ENV` to control Minz development mode [#2508](https://github.com/FreshRSS/FreshRSS/pull/2508)
+	* Git ignore `themes/xTheme-*` [#2511](https://github.com/FreshRSS/FreshRSS/pull/2511)
+* Bug fixing
+	* Fix missing PHP `opcache` package in Docker Alpine [#2498](https://github.com/FreshRSS/FreshRSS/pull/2498)
+	* Fix IE11 / Edge keyboard compatibility [#2507](https://github.com/FreshRSS/FreshRSS/pull/2507)
+	* Use `<dc:creator>` instead of `<author>` for RSS 2.0 outputs [#2542](https://github.com/FreshRSS/FreshRSS/pull/2542)
+	* Fix PostgreSQL and SQLite database size estimation [#2562](https://github.com/FreshRSS/FreshRSS/pull/2562)
+	* Fix broken SVG icons in Swage theme [#2568](https://github.com/FreshRSS/FreshRSS/issues/2568), [#2571](https://github.com/FreshRSS/FreshRSS/pull/2571)
+* Security
+	* Fix referrer vulnerability when opening an article original link with a shortcut [#2506](https://github.com/FreshRSS/FreshRSS/pull/2506)
+	* Slight refactoring of access check [#2471](https://github.com/FreshRSS/FreshRSS/pull/2471)
+* UI
+	* Optimize dynamic favicon for HiDPI screens [#2539](https://github.com/FreshRSS/FreshRSS/pull/2539)
+	* Hide the admin checkbox if user is not admin [#2531](https://github.com/FreshRSS/FreshRSS/pull/2531)
+* I18n
+	* Add Slovak [#2497](https://github.com/FreshRSS/FreshRSS/pull/2497)
+	* Improve Dutch [#2503](https://github.com/FreshRSS/FreshRSS/pull/2503)
+	* Improve Occitan [#2519](https://github.com/FreshRSS/FreshRSS/pull/2519), [#2583](https://github.com/FreshRSS/FreshRSS/pull/2583), [#2603](https://github.com/FreshRSS/FreshRSS/pull/2603)
+* Extensions
+	* Additional hooks [#2482](https://github.com/FreshRSS/FreshRSS/pull/2482)
+	* New call to change the layout [#2467](https://github.com/FreshRSS/FreshRSS/pull/2467)
+* Misc.
+	* Make our JavaScript compatible with LibreJS [#2576](https://github.com/FreshRSS/FreshRSS/pull/2576)
+	* PDO (database) refactoring for code simplification [#2522](https://github.com/FreshRSS/FreshRSS/pull/2522)
+	* Automatic check of CSS syntax in Travis CI [#2477](https://github.com/FreshRSS/FreshRSS/pull/2477)
+	* Make our Travis greener by reducing redundant tests [#2589](https://github.com/FreshRSS/FreshRSS/pull/2589)
+	* Remove support for sharing with Google+ [#2464](https://github.com/FreshRSS/FreshRSS/pull/2464)
+	* Redirect connected users accessing registration page [#2530](https://github.com/FreshRSS/FreshRSS/pull/2530)
+	* Add Makefile [#2481](https://github.com/FreshRSS/FreshRSS/pull/2481)
+
+
 ## 2019-07-25 FreshRSS 1.14.3
 ## 2019-07-25 FreshRSS 1.14.3
 
 
 * UI
 * UI
@@ -280,8 +339,8 @@
 
 
 * API
 * API
 	* Add support for Fever compatible API, enabling more clients [#1406](https://github.com/FreshRSS/FreshRSS/pull/1406)
 	* Add support for Fever compatible API, enabling more clients [#1406](https://github.com/FreshRSS/FreshRSS/pull/1406)
-		* iOS: [Fiery Feeds](https://itunes.apple.com/app/fiery-feeds-rss-reader/id1158763303), [Unread](https://itunes.apple.com/app/unread-rss-reader/id1252376153)
-		* MacOS: [Readkit](https://itunes.apple.com/app/readkit/id588726889)
+		* iOS: [Fiery Feeds](https://apps.apple.com/app/fiery-feeds-rss-reader/id1158763303), [Unread](https://apps.apple.com/app/unread-rss-reader/id1252376153)
+		* MacOS: [Readkit](https://apps.apple.com/app/readkit/id588726889)
 * Features
 * Features
 	* Several per-feed options (implemented in JSON) [#1838](https://github.com/FreshRSS/FreshRSS/pull/1838)
 	* Several per-feed options (implemented in JSON) [#1838](https://github.com/FreshRSS/FreshRSS/pull/1838)
 		* Mark updated articles as read [#891](https://github.com/FreshRSS/FreshRSS/issues/891)
 		* Mark updated articles as read [#891](https://github.com/FreshRSS/FreshRSS/issues/891)
@@ -478,7 +537,7 @@
 	* Simplified Chinese [#1541](https://github.com/FreshRSS/FreshRSS/pull/1541)
 	* Simplified Chinese [#1541](https://github.com/FreshRSS/FreshRSS/pull/1541)
 	* Improve English [#1465](https://github.com/FreshRSS/FreshRSS/pull/1465)
 	* Improve English [#1465](https://github.com/FreshRSS/FreshRSS/pull/1465)
 	* Improve Dutch [#1559](https://github.com/FreshRSS/FreshRSS/pull/1559)
 	* Improve Dutch [#1559](https://github.com/FreshRSS/FreshRSS/pull/1559)
-	* Added Spanish language [#1631] (https://github.com/FreshRSS/FreshRSS/pull/1631/) 
+	* Added Spanish language [#1631] (https://github.com/FreshRSS/FreshRSS/pull/1631/)
 * Security
 * Security
 	* Do not require write access to check availability of new versions [#1450](https://github.com/FreshRSS/FreshRSS/issues/1450)
 	* Do not require write access to check availability of new versions [#1450](https://github.com/FreshRSS/FreshRSS/issues/1450)
 * Misc.
 * Misc.
@@ -504,7 +563,7 @@
 	* New command `./cli/reconfigure.php` to update an existing installation [#1439](https://github.com/FreshRSS/FreshRSS/pull/1439)
 	* New command `./cli/reconfigure.php` to update an existing installation [#1439](https://github.com/FreshRSS/FreshRSS/pull/1439)
 	* Many CLI improvements [#1447](https://github.com/FreshRSS/FreshRSS/pull/1447)
 	* Many CLI improvements [#1447](https://github.com/FreshRSS/FreshRSS/pull/1447)
 		* More information (number of feeds, articles, etc.) in `./cli/user-info.php`
 		* More information (number of feeds, articles, etc.) in `./cli/user-info.php`
-		* Better idempotency of `./cli/do-install.php` and language parameter [#1449](https://github.com/FreshRSS/FreshRSS/issues/1449) 
+		* Better idempotency of `./cli/do-install.php` and language parameter [#1449](https://github.com/FreshRSS/FreshRSS/issues/1449)
 * Bug fixing
 * Bug fixing
 	* Fix several CLI issues [#1445](https://github.com/FreshRSS/FreshRSS/issues/1445)
 	* Fix several CLI issues [#1445](https://github.com/FreshRSS/FreshRSS/issues/1445)
 		* Fix CLI install bugs with SQLite [#1443](https://github.com/FreshRSS/FreshRSS/issues/1443), [#1448](https://github.com/FreshRSS/FreshRSS/issues/1448)
 		* Fix CLI install bugs with SQLite [#1443](https://github.com/FreshRSS/FreshRSS/issues/1443), [#1448](https://github.com/FreshRSS/FreshRSS/issues/1448)

+ 1 - 1
CONTRIBUTING.md

@@ -21,7 +21,7 @@ If you have to create a new ticket, try to apply the following advices:
 - We also need some information:
 - We also need some information:
     + Your FreshRSS version (on about page or `constants.php` file)
     + Your FreshRSS version (on about page or `constants.php` file)
     + Your server configuration: type of hosting, PHP version
     + Your server configuration: type of hosting, PHP version
-    + Your storage system (MySQL / MariaDB or SQLite)
+    + Your storage system (SQLite, MySQL, MariaDB, PostgreSQL)
     + If possible, the related logs (PHP logs and FreshRSS logs under `data/users/your_user/log.txt`)
     + If possible, the related logs (PHP logs and FreshRSS logs under `data/users/your_user/log.txt`)
 
 
 ## Fix a bug
 ## Fix a bug

+ 11 - 0
CREDITS.md

@@ -13,6 +13,7 @@ People are sorted by name so please keep this order.
 * [Alwaysin](https://github.com/Alwaysin): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Alwaysin)
 * [Alwaysin](https://github.com/Alwaysin): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Alwaysin)
 * [Amaury Carrade](https://github.com/AmauryCarrade): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=AmauryCarrade), [Web](https://amaury.carrade.eu/)
 * [Amaury Carrade](https://github.com/AmauryCarrade): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=AmauryCarrade), [Web](https://amaury.carrade.eu/)
 * [Anton Smirnov](https://github.com/sandfoxme): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:sandfoxme), [Web](http://sandfox.me/)
 * [Anton Smirnov](https://github.com/sandfoxme): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:sandfoxme), [Web](http://sandfox.me/)
+* [ArthurHoaro](https://github.com/ArthurHoaro): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ArthurHoaro), [Web](http://sandfox.me/)
 * [ASMfreaK](https://github.com/ASMfreaK): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ASMfreaK)
 * [ASMfreaK](https://github.com/ASMfreaK): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ASMfreaK)
 * [Benjamin Bouvier](https://github.com/bnjbvr): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:bnjbvr), [Web](https://benj.me/)
 * [Benjamin Bouvier](https://github.com/bnjbvr): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:bnjbvr), [Web](https://benj.me/)
 * [chemical1979](https://github.com/chemical1979): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=chemical1979)
 * [chemical1979](https://github.com/chemical1979): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=chemical1979)
@@ -26,6 +27,7 @@ People are sorted by name so please keep this order.
 * [ealdraed](https://github.com/ealdraed): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ealdraed)
 * [ealdraed](https://github.com/ealdraed): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ealdraed)
 * [Fake4d](https://github.com/Fake4d): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Fake4d)
 * [Fake4d](https://github.com/Fake4d): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Fake4d)
 * [Frans de Jonge](https://github.com/Frenzie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Frenzie), [Web](http://fransdejonge.com/)
 * [Frans de Jonge](https://github.com/Frenzie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Frenzie), [Web](http://fransdejonge.com/)
+* [Gaurav Thakur](https://github.com/notfoss): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:notfoss), [Web](https://blog.notfoss.com/)
 * [Gregor Nathanael Meyer](https://github.com/spackmat): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:spackmat), [Web](https://der-meyer.de)
 * [Gregor Nathanael Meyer](https://github.com/spackmat): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:spackmat), [Web](https://der-meyer.de)
 * [gsongsong](https://github.com/gsongsong): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gsongsong)
 * [gsongsong](https://github.com/gsongsong): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gsongsong)
 * [Guillaume Fillon](https://github.com/kokaz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kokaz), [Web](http://www.guillaume-fillon.com/)
 * [Guillaume Fillon](https://github.com/kokaz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kokaz), [Web](http://www.guillaume-fillon.com/)
@@ -36,6 +38,7 @@ People are sorted by name so please keep this order.
 * [Jaussoin Timothée](https://github.com/edhelas): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=edhelas), [Web](http://edhelas.movim.eu/)
 * [Jaussoin Timothée](https://github.com/edhelas): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=edhelas), [Web](http://edhelas.movim.eu/)
 * [jlefler](https://github.com/jlefler): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jlefler)
 * [jlefler](https://github.com/jlefler): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jlefler)
 * [Jonas Östanbäck](https://github.com/cez81): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=cez81)
 * [Jonas Östanbäck](https://github.com/cez81): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=cez81)
+* [Joris Kinable](https://github.com/jkinable): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jkinable)
 * [Julien Reichardt](https://github.com/j8r): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=j8r), [Web](https://blog.jrei.ch/)
 * [Julien Reichardt](https://github.com/j8r): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=j8r), [Web](https://blog.jrei.ch/)
 * [Kevin Papst](https://github.com/kevinpapst): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=kevinpapst), [Web](http://www.kevinpapst.de/)
 * [Kevin Papst](https://github.com/kevinpapst): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=kevinpapst), [Web](http://www.kevinpapst.de/)
 * [Leepic](https://github.com/Leepic): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Leepic)
 * [Leepic](https://github.com/Leepic): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Leepic)
@@ -52,16 +55,21 @@ People are sorted by name so please keep this order.
 * [Nicolas Elie](https://github.com/nicolaselie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=nicolaselie)
 * [Nicolas Elie](https://github.com/nicolaselie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=nicolaselie)
 * [Nicolas Frandeboeuf](https://github.com/nicofrand): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=nicofrand), [Web](https://nicofrand.ey)
 * [Nicolas Frandeboeuf](https://github.com/nicofrand): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=nicofrand), [Web](https://nicofrand.ey)
 * [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/commits?author=nicosomb), [Web](http://www.loeuillet.org/)
 * [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/commits?author=nicosomb), [Web](http://www.loeuillet.org/)
+* [Offerel](https://github.com/Offerel): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Offerel)
 * [Olivier Dossmann](https://github.com/blankoworld): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=blankoworld), [Web](https://olivier.dossmann.net)
 * [Olivier Dossmann](https://github.com/blankoworld): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=blankoworld), [Web](https://olivier.dossmann.net)
 * [Patrick Crandol](https://github.com/pattems): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:pattems)
 * [Patrick Crandol](https://github.com/pattems): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:pattems)
 * [Paulius Šukys](https://github.com/psukys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:psukys), [Web](http://sukys.eu)
 * [Paulius Šukys](https://github.com/psukys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:psukys), [Web](http://sukys.eu)
 * [perrinjerome](https://github.com/perrinjerome): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:perrinjerome)
 * [perrinjerome](https://github.com/perrinjerome): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:perrinjerome)
+* [Peter Stoinov](https://github.com/stoinov): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:stoinov), [Web](https://stoinov.com)
+* [Pim Snel](https://github.com/mipmip): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is%3Apr+author%3Amipmip), [Web](https://www.pimsnel.com)
 * [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=plopoyop)
 * [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=plopoyop)
 * [primaeval](https://github.com/primaeval): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:primaeval)
 * [primaeval](https://github.com/primaeval): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:primaeval)
 * [purexo](https://github.com/purexo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:purexo), [Web](https://purexo.mom/)
 * [purexo](https://github.com/purexo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:purexo), [Web](https://purexo.mom/)
 * [Quentin Dufour](https://github.com/superboum): [contributions](https://github.com/FreshRSS/documentation/commits?author=superboum), [Web](http://quentin.dufour.io/)
 * [Quentin Dufour](https://github.com/superboum): [contributions](https://github.com/FreshRSS/documentation/commits?author=superboum), [Web](http://quentin.dufour.io/)
 * [Quentin Pagès](https://github.com/Quenty31): [contributions](https://github.com/FreshRSS/documentation/commits?author=Quenty31)
 * [Quentin Pagès](https://github.com/Quenty31): [contributions](https://github.com/FreshRSS/documentation/commits?author=Quenty31)
 * [Ramón Cutanda](https://github.com/rcutanda): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rcutanda)
 * [Ramón Cutanda](https://github.com/rcutanda): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rcutanda)
+* [Robert Kaussow](https://github.com/xoxys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:xoxys), [Web](https://geeklabor.de/)
+* [rocka](https://github.com/rocka): [contributions](https://github.com/FreshRSS/FreshRss/commits/dev?author=rocka)
 * [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/commits/dev?author=romibi)
 * [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/commits/dev?author=romibi)
 * [Rosemary Le Faive](https://github.com/rosiel): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rosiel)
 * [Rosemary Le Faive](https://github.com/rosiel): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rosiel)
 * [Sandro Jäckel](https://github.com/SuperSandro2000): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:SuperSandro2000), [Web](https://supersandro.de/)
 * [Sandro Jäckel](https://github.com/SuperSandro2000): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:SuperSandro2000), [Web](https://supersandro.de/)
@@ -72,7 +80,10 @@ People are sorted by name so please keep this order.
 * [Thomas Citharel](https://github.com/tcitworld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:tomgue), [Web](https://www.tcit.fr/)
 * [Thomas Citharel](https://github.com/tcitworld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:tomgue), [Web](https://www.tcit.fr/)
 * [Thomas Guesnon](https://github.com/patjennings): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:patjennings), [Web](http://www.thomasguesnon.fr/)
 * [Thomas Guesnon](https://github.com/patjennings): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:patjennings), [Web](http://www.thomasguesnon.fr/)
 * [thomas-gt](https://github.com/thomas-gt): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:thomas-gt)
 * [thomas-gt](https://github.com/thomas-gt): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:thomas-gt)
+* [Tibor Repček](https://github.com/tiborepcek): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:tiborepcek)
 * [tomgue](https://github.com/tomgue): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=tomgue)
 * [tomgue](https://github.com/tomgue): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=tomgue)
 * [Twilek-de](https://github.com/Twilek-de): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Twilek-de)
 * [Twilek-de](https://github.com/Twilek-de): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Twilek-de)
 * [Uncovery](https://github.com/uncovery): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:uncovery)
 * [Uncovery](https://github.com/uncovery): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:uncovery)
 * [Wanabo](https://github.com/Wanabo): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Wanabo)
 * [Wanabo](https://github.com/Wanabo): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Wanabo)
+* [wtoscer](https://github.com/wtoscer): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:wtoscer)
+* [Yamakuni](https://github.com/Yamakuni): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Yamakuni), [Web](https://ofanch.me/)

+ 5 - 2
Docker/Dockerfile

@@ -1,4 +1,4 @@
-FROM ubuntu:19.04
+FROM ubuntu:19.10
 
 
 ENV TZ UTC
 ENV TZ UTC
 SHELL ["/bin/bash", "-o", "pipefail", "-c"]
 SHELL ["/bin/bash", "-o", "pipefail", "-c"]
@@ -43,12 +43,15 @@ RUN a2dismod -f alias autoindex negotiation status && \
 RUN sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" /etc/apache2/apache2.conf && \
 RUN sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" /etc/apache2/apache2.conf && \
 	sed -r -i "/^\s*Listen /s/^/#/" /etc/apache2/ports.conf && \
 	sed -r -i "/^\s*Listen /s/^/#/" /etc/apache2/ports.conf && \
 	touch /var/www/FreshRSS/Docker/env.txt && \
 	touch /var/www/FreshRSS/Docker/env.txt && \
-	echo "17,47 * * * * . /var/www/FreshRSS/Docker/env.txt; \
+	echo "7,37 * * * * . /var/www/FreshRSS/Docker/env.txt; \
 		su www-data -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' \
 		su www-data -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' \
 		2>> /proc/1/fd/2 > /tmp/FreshRSS.log" | crontab -
 		2>> /proc/1/fd/2 > /tmp/FreshRSS.log" | crontab -
 
 
+ENV COPY_LOG_TO_SYSLOG On
 ENV COPY_SYSLOG_TO_STDERR On
 ENV COPY_SYSLOG_TO_STDERR On
 ENV CRON_MIN ''
 ENV CRON_MIN ''
+ENV FRESHRSS_ENV ''
+
 ENTRYPOINT ["./Docker/entrypoint.sh"]
 ENTRYPOINT ["./Docker/entrypoint.sh"]
 
 
 EXPOSE 80
 EXPOSE 80

+ 4 - 1
Docker/Dockerfile-Alpine

@@ -5,7 +5,7 @@ SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
 RUN apk add --no-cache \
 RUN apk add --no-cache \
 	apache2 php7-apache2 \
 	apache2 php7-apache2 \
 	php7 php7-curl php7-gmp php7-intl php7-mbstring php7-xml php7-zip \
 	php7 php7-curl php7-gmp php7-intl php7-mbstring php7-xml php7-zip \
-	php7-ctype php7-dom php7-fileinfo php7-iconv php7-json php7-session php7-simplexml php7-xmlreader php7-zlib \
+	php7-ctype php7-dom php7-fileinfo php7-iconv php7-json php7-opcache php7-session php7-simplexml php7-xmlreader php7-zlib \
 	php7-pdo_sqlite php7-pdo_mysql php7-pdo_pgsql
 	php7-pdo_sqlite php7-pdo_mysql php7-pdo_pgsql
 
 
 RUN mkdir -p /var/www/FreshRSS /run/apache2/
 RUN mkdir -p /var/www/FreshRSS /run/apache2/
@@ -43,8 +43,11 @@ RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
 		su apache -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' \
 		su apache -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' \
 		2>> /proc/1/fd/2 > /tmp/FreshRSS.log" | crontab -
 		2>> /proc/1/fd/2 > /tmp/FreshRSS.log" | crontab -
 
 
+ENV COPY_LOG_TO_SYSLOG On
 ENV COPY_SYSLOG_TO_STDERR On
 ENV COPY_SYSLOG_TO_STDERR On
 ENV CRON_MIN ''
 ENV CRON_MIN ''
+ENV FRESHRSS_ENV ''
+
 ENTRYPOINT ["./Docker/entrypoint.sh"]
 ENTRYPOINT ["./Docker/entrypoint.sh"]
 
 
 EXPOSE 80
 EXPOSE 80

+ 4 - 1
Docker/Dockerfile-QEMU-ARM

@@ -1,7 +1,7 @@
 # Only relevant for Docker Hub or QEMU multi-architecture builds.
 # Only relevant for Docker Hub or QEMU multi-architecture builds.
 # Prefer the normal `Dockerfile` if you are building manually on the targeted architecture.
 # Prefer the normal `Dockerfile` if you are building manually on the targeted architecture.
 
 
-FROM arm32v7/ubuntu:19.04
+FROM arm32v7/ubuntu:19.10
 
 
 # Requires ./hooks/*
 # Requires ./hooks/*
 COPY ./Docker/qemu-arm-* /usr/bin/
 COPY ./Docker/qemu-arm-* /usr/bin/
@@ -59,8 +59,11 @@ RUN update-ca-certificates -f
 # Useful with the `--squash` build option
 # Useful with the `--squash` build option
 RUN rm /usr/bin/qemu-* /var/www/FreshRSS/Docker/qemu-*
 RUN rm /usr/bin/qemu-* /var/www/FreshRSS/Docker/qemu-*
 
 
+ENV COPY_LOG_TO_SYSLOG On
 ENV COPY_SYSLOG_TO_STDERR On
 ENV COPY_SYSLOG_TO_STDERR On
 ENV CRON_MIN ''
 ENV CRON_MIN ''
+ENV FRESHRSS_ENV ''
+
 ENTRYPOINT ["./Docker/entrypoint.sh"]
 ENTRYPOINT ["./Docker/entrypoint.sh"]
 
 
 EXPOSE 80
 EXPOSE 80

+ 50 - 35
Docker/README.md

@@ -17,7 +17,7 @@ sh get-docker.sh
 
 
 ## Create an isolated network
 ## Create an isolated network
 ```sh
 ```sh
-sudo docker network create freshrss-network
+docker network create freshrss-network
 ```
 ```
 
 
 ## Recommended: use [Træfik](https://traefik.io/) reverse proxy
 ## Recommended: use [Træfik](https://traefik.io/) reverse proxy
@@ -25,18 +25,18 @@ It is a good idea to use a reverse proxy on your host server, providing HTTPS.
 Here is the recommended configuration using automatic [Let’s Encrypt](https://letsencrypt.org/) HTTPS certificates and with a redirection from HTTP to HTTPS. See further below for alternatives.
 Here is the recommended configuration using automatic [Let’s Encrypt](https://letsencrypt.org/) HTTPS certificates and with a redirection from HTTP to HTTPS. See further below for alternatives.
 
 
 ```sh
 ```sh
-sudo docker volume create traefik-letsencrypt
-sudo docker volume create traefik-tmp
+docker volume create traefik-letsencrypt
+docker volume create traefik-tmp
 
 
 # Just change your e-mail address in the command below:
 # Just change your e-mail address in the command below:
-sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+docker run -d --restart unless-stopped --log-opt max-size=10m \
   -v traefik-letsencrypt:/etc/traefik/acme \
   -v traefik-letsencrypt:/etc/traefik/acme \
   -v traefik-tmp:/tmp \
   -v traefik-tmp:/tmp \
   -v /var/run/docker.sock:/var/run/docker.sock:ro \
   -v /var/run/docker.sock:/var/run/docker.sock:ro \
   --net freshrss-network \
   --net freshrss-network \
   -p 80:80 \
   -p 80:80 \
   -p 443:443 \
   -p 443:443 \
-  --name traefik traefik --docker \
+  --name traefik traefik:1.7 --docker \
   --loglevel=info \
   --loglevel=info \
   --entryPoints='Name:http Address::80 Compress:true Redirect.EntryPoint:https' \
   --entryPoints='Name:http Address::80 Compress:true Redirect.EntryPoint:https' \
   --entryPoints='Name:https Address::443 Compress:true TLS TLS.MinVersion:VersionTLS12 TLS.SniStrict:true TLS.CipherSuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA' \
   --entryPoints='Name:https Address::443 Compress:true TLS TLS.MinVersion:VersionTLS12 TLS.SniStrict:true TLS.CipherSuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA' \
@@ -48,17 +48,17 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
 See [more information about Docker and Let’s Encrypt in Træfik](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/).
 See [more information about Docker and Let’s Encrypt in Træfik](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/).
 
 
 
 
-## Run FreshRSS 
+## Run FreshRSS
 Example using the built-in refresh cron job (see further below for alternatives).
 Example using the built-in refresh cron job (see further below for alternatives).
 You must first chose a domain (DNS) or sub-domain, e.g. `freshrss.example.net`.
 You must first chose a domain (DNS) or sub-domain, e.g. `freshrss.example.net`.
 
 
 > **N.B.:** Default images are for x64 (Intel, AMD) platforms. For ARM (e.g. Raspberry Pi), use the `*-arm` tags. For other platforms, see the section *Build Docker image* further below.
 > **N.B.:** Default images are for x64 (Intel, AMD) platforms. For ARM (e.g. Raspberry Pi), use the `*-arm` tags. For other platforms, see the section *Build Docker image* further below.
 
 
 ```sh
 ```sh
-sudo docker volume create freshrss-data
+docker volume create freshrss-data
 
 
 # Remember to replace freshrss.example.net by your server address in the command below:
 # Remember to replace freshrss.example.net by your server address in the command below:
-sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+docker run -d --restart unless-stopped --log-opt max-size=10m \
   -v freshrss-data:/var/www/FreshRSS/data \
   -v freshrss-data:/var/www/FreshRSS/data \
   -e 'CRON_MIN=4,34' \
   -e 'CRON_MIN=4,34' \
   -e TZ=Europe/Paris \
   -e TZ=Europe/Paris \
@@ -79,16 +79,16 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
 
 
 This already works with a built-in **SQLite** database (easiest), but more powerful databases are supported:
 This already works with a built-in **SQLite** database (easiest), but more powerful databases are supported:
 
 
-### [MySQL](https://hub.docker.com/_/mysql/)
+### [MySQL](https://hub.docker.com/_/mysql/) or [MariaDB](https://hub.docker.com/_/mariadb)
 ```sh
 ```sh
-# If you already have a MySQL instance running, just attach it to the FreshRSS network:
-sudo docker network connect freshrss-network mysql
+# If you already have a MySQL or MariaDB instance running, just attach it to the FreshRSS network:
+docker network connect freshrss-network mysql
 
 
 # Otherwise, start a new MySQL instance, remembering to change the passwords:
 # Otherwise, start a new MySQL instance, remembering to change the passwords:
-sudo docker volume create mysql-data
-sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+docker volume create mysql-data
+docker run -d --restart unless-stopped --log-opt max-size=10m \
   -v mysql-data:/var/lib/mysql \
   -v mysql-data:/var/lib/mysql \
-  -e MYSQL_ROOT_PASSWORD=rootpass
+  -e MYSQL_ROOT_PASSWORD=rootpass \
   -e MYSQL_DATABASE=freshrss \
   -e MYSQL_DATABASE=freshrss \
   -e MYSQL_USER=freshrss \
   -e MYSQL_USER=freshrss \
   -e MYSQL_PASSWORD=pass \
   -e MYSQL_PASSWORD=pass \
@@ -99,11 +99,11 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
 ### [PostgreSQL](https://hub.docker.com/_/postgres/)
 ### [PostgreSQL](https://hub.docker.com/_/postgres/)
 ```sh
 ```sh
 # If you already have a PostgreSQL instance running, just attach it to the FreshRSS network:
 # If you already have a PostgreSQL instance running, just attach it to the FreshRSS network:
-sudo docker network connect freshrss-network postgres
+docker network connect freshrss-network postgres
 
 
 # Otherwise, start a new PostgreSQL instance, remembering to change the passwords:
 # Otherwise, start a new PostgreSQL instance, remembering to change the passwords:
-sudo docker volume create pgsql-data
-sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+docker volume create pgsql-data
+docker run -d --restart unless-stopped --log-opt max-size=10m \
   -v pgsql-data:/var/lib/postgresql/data \
   -v pgsql-data:/var/lib/postgresql/data \
   -e POSTGRES_DB=freshrss \
   -e POSTGRES_DB=freshrss \
   -e POSTGRES_USER=freshrss \
   -e POSTGRES_USER=freshrss \
@@ -121,14 +121,14 @@ or use the command line described below.
 
 
 ```sh
 ```sh
 # Rebuild an image (see build section above) or get a new online version:
 # Rebuild an image (see build section above) or get a new online version:
-sudo docker pull freshrss/freshrss
+docker pull freshrss/freshrss
 # And then
 # And then
-sudo docker stop freshrss
-sudo docker rename freshrss freshrss_old
+docker stop freshrss
+docker rename freshrss freshrss_old
 # See the run section above for the full command
 # See the run section above for the full command
-sudo docker run ... --name freshrss freshrss/freshrss
+docker run ... --name freshrss freshrss/freshrss
 # If everything is working, delete the old container
 # If everything is working, delete the old container
-sudo docker rm freshrss_old
+docker rm freshrss_old
 ```
 ```
 
 
 
 
@@ -153,17 +153,16 @@ Note that prebuilt images are less recent and only available for x64 (Intel, AMD
 # First time only
 # First time only
 git clone https://github.com/FreshRSS/FreshRSS.git
 git clone https://github.com/FreshRSS/FreshRSS.git
 
 
-cd ./FreshRSS/
+cd FreshRSS/
 git pull
 git pull
-sudo docker pull ubuntu:18.10
-sudo docker build --tag freshrss/freshrss -f Docker/Dockerfile .
+docker build --pull --tag freshrss/freshrss -f Docker/Dockerfile .
 ```
 ```
 
 
 
 
 ## Command line
 ## Command line
 
 
 ```sh
 ```sh
-sudo docker exec --user apache -it freshrss php ./cli/list-users.php
+docker exec --user apache -it freshrss php ./cli/list-users.php
 ```
 ```
 
 
 See the [CLI documentation](../cli/) for all the other commands.
 See the [CLI documentation](../cli/) for all the other commands.
@@ -173,14 +172,14 @@ See the [CLI documentation](../cli/) for all the other commands.
 
 
 ```sh
 ```sh
 # See FreshRSS data if you use Docker volume
 # See FreshRSS data if you use Docker volume
-sudo docker volume inspect freshrss-data
+docker volume inspect freshrss-data
 sudo ls /var/lib/docker/volumes/freshrss-data/_data/
 sudo ls /var/lib/docker/volumes/freshrss-data/_data/
 
 
 # See Web server logs
 # See Web server logs
-sudo docker logs -f freshrss
+docker logs -f freshrss
 
 
 # Enter inside FreshRSS docker container
 # Enter inside FreshRSS docker container
-sudo docker exec -it freshrss sh
+docker exec -it freshrss sh
 ## See FreshRSS root inside the container
 ## See FreshRSS root inside the container
 ls /var/www/FreshRSS/
 ls /var/www/FreshRSS/
 ```
 ```
@@ -198,7 +197,7 @@ containing a valid cron minute definition such as `'13,43'` (recommended) or `'*
 Not passing the `CRON_MIN` environment variable – or setting it to empty string – will disable the cron daemon.
 Not passing the `CRON_MIN` environment variable – or setting it to empty string – will disable the cron daemon.
 
 
 ```sh
 ```sh
-sudo docker run ... \
+docker run ... \
   -e 'CRON_MIN=13,43' \
   -e 'CRON_MIN=13,43' \
   --name freshrss freshrss/freshrss
   --name freshrss freshrss/freshrss
 ```
 ```
@@ -221,7 +220,7 @@ See cron option 1 for customising the cron schedule.
 
 
 #### For the Ubuntu image (default)
 #### For the Ubuntu image (default)
 ```sh
 ```sh
-sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+docker run -d --restart unless-stopped --log-opt max-size=10m \
   -v freshrss-data:/var/www/FreshRSS/data \
   -v freshrss-data:/var/www/FreshRSS/data \
   -e 'CRON_MIN=17,47' \
   -e 'CRON_MIN=17,47' \
   --net freshrss-network \
   --net freshrss-network \
@@ -231,7 +230,7 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
 
 
 #### For the Alpine image
 #### For the Alpine image
 ```sh
 ```sh
-sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+docker run -d --restart unless-stopped --log-opt max-size=10m \
   -v freshrss-data:/var/www/FreshRSS/data \
   -v freshrss-data:/var/www/FreshRSS/data \
   -e 'CRON_MIN=27,57' \
   -e 'CRON_MIN=27,57' \
   --net freshrss-network \
   --net freshrss-network \
@@ -239,6 +238,22 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
   crond -f -d 6
   crond -f -d 6
 ```
 ```
 
 
+## Development mode
+
+To contribute to FreshRSS development, you can use one of the Docker images to run and serve the PHP code,
+while reading the source code from your local (git) directory, like the following example:
+
+```sh
+cd /path-to-local/FreshRSS/
+docker run --rm -p 8080:80 -e TZ=Europe/Paris -e FRESHRSS_ENV=development \
+  -v $(pwd):/var/www/FreshRSS \
+  freshrss/freshrss:dev
+```
+
+This will start a server on port 8080, based on your local PHP code, which will show the logs directly in your terminal.
+Press <kbd>Control</kbd>+<kbd>c</kbd> to exit.
+
+The `FRESHRSS_ENV=development` environment variable increases the level of logging and ensures that errors are displayed.
 
 
 ## More deployment options
 ## More deployment options
 
 
@@ -248,7 +263,7 @@ Changes in Apache `.htaccess` files are applied when restarting the container.
 In particular, if you want FreshRSS to use HTTP-based login (instead of the easier Web form login), you can mount your own `./FreshRSS/p/i/.htaccess`:
 In particular, if you want FreshRSS to use HTTP-based login (instead of the easier Web form login), you can mount your own `./FreshRSS/p/i/.htaccess`:
 
 
 ```
 ```
-sudo docker run ...
+docker run ...
   -v /your/.htaccess:/var/www/FreshRSS/p/i/.htaccess \
   -v /your/.htaccess:/var/www/FreshRSS/p/i/.htaccess \
   -v /your/.htpasswd:/var/www/FreshRSS/data/.htpasswd \
   -v /your/.htpasswd:/var/www/FreshRSS/data/.htpasswd \
   ...
   ...
@@ -276,7 +291,7 @@ A [docker-compose.yml](docker-compose.yml) file is given as an example, using Po
 
 
 You can then launch the stack (FreshRSS + PostgreSQL) with:
 You can then launch the stack (FreshRSS + PostgreSQL) with:
 ```sh
 ```sh
-sudo docker-compose up -d
+docker-compose up -d
 ```
 ```
 
 
 ### Alternative reverse proxy using [nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)
 ### Alternative reverse proxy using [nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)
@@ -313,7 +328,7 @@ server {
 	}
 	}
 
 
 	location /freshrss/ {
 	location /freshrss/ {
-		proxy_pass http://freshrss/;
+		proxy_pass http://freshrss;
 		add_header X-Frame-Options SAMEORIGIN;
 		add_header X-Frame-Options SAMEORIGIN;
 		add_header X-XSS-Protection "1; mode=block";
 		add_header X-XSS-Protection "1; mode=block";
 		proxy_redirect off;
 		proxy_redirect off;

+ 18 - 25
Docker/docker-compose.yml

@@ -1,38 +1,31 @@
-version: '2.3'
+version: "3"
 
 
 services:
 services:
-  postgresql:
-    image: postgres:latest
+  freshrss_postgresql:
+    image: postgres
     restart: unless-stopped
     restart: unless-stopped
     volumes:
     volumes:
-    - '/path/to/pgsql-data:/var/lib/postgresql/data'
+      - pgsql_data:/var/lib/postgresql/data
     environment:
     environment:
-    - POSTGRES_USER=freshrss
-    - POSTGRES_PASSWORD=password
-    - POSTGRES_DB=freshrss
+      - POSTGRES_USER=freshrss
+      - POSTGRES_PASSWORD=freshrss
+      - POSTGRES_DB=freshrss
 
 
   freshrss:
   freshrss:
-    image: freshrss/freshrss:latest
+    image: freshrss/freshrss
     restart: unless-stopped
     restart: unless-stopped
+    ports:
+      - "8080:80"
     depends_on:
     depends_on:
-      - postgresql
-    networks:
-      - web
-      - default
+      - freshrss_postgresql
     volumes:
     volumes:
-      - '/your/local/directory/data:/var/www/FreshRSS/data'
-    labels:
-      - "traefik.backend=freshrss"
-      - "traefik.docker.network=web"
-      - "traefik.frontend.rule=Host:rss.example.com"
-      - "traefik.enable=true"
-      - "traefik.default.protocol=http"
-      - "traefik.frontend.entryPoints=http,https"
-      - "traefik.port=80"
+      - freshrss_data:/var/www/FreshRSS/data
     environment:
     environment:
       - CRON_MIN=*/20
       - CRON_MIN=*/20
+      - TZ=Europe/Copenhagen
+    labels:
+      - "traefik.port=80"
 
 
-networks:
-  web:
-    external: true
-
+volumes:
+  pgsql_data:
+  freshrss_data:

+ 3 - 0
Docker/entrypoint.sh

@@ -6,10 +6,13 @@ chown -R :www-data .
 chmod -R g+r . && chmod -R g+w ./data/
 chmod -R g+r . && chmod -R g+w ./data/
 
 
 find /etc/php*/ -name php.ini -exec sed -r -i "\\#^;?date.timezone#s#^.*#date.timezone = $TZ#" {} \;
 find /etc/php*/ -name php.ini -exec sed -r -i "\\#^;?date.timezone#s#^.*#date.timezone = $TZ#" {} \;
+find /etc/php*/ -name php.ini -exec sed -r -i "\\#^;?post_max_size#s#^.*#post_max_size = 32M#" {} \;
+find /etc/php*/ -name php.ini -exec sed -r -i "\\#^;?upload_max_filesize#s#^.*#upload_max_filesize = 32M#" {} \;
 
 
 if [ -n "$CRON_MIN" ]; then
 if [ -n "$CRON_MIN" ]; then
 	(
 	(
 		echo "export TZ=$TZ"
 		echo "export TZ=$TZ"
+		echo "export COPY_LOG_TO_SYSLOG=$COPY_LOG_TO_SYSLOG"
 		echo "export COPY_SYSLOG_TO_STDERR=$COPY_SYSLOG_TO_STDERR"
 		echo "export COPY_SYSLOG_TO_STDERR=$COPY_SYSLOG_TO_STDERR"
 	) >/var/www/FreshRSS/Docker/env.txt
 	) >/var/www/FreshRSS/Docker/env.txt
 	crontab -l | sed -r "\\#FreshRSS#s#^[^ ]+ #$CRON_MIN #" | crontab -
 	crontab -l | sed -r "\\#FreshRSS#s#^[^ ]+ #$CRON_MIN #" | crontab -

+ 38 - 0
Makefile

@@ -0,0 +1,38 @@
+.DEFAULT_GOAL := help
+
+ifndef TAG
+	TAG=dev-alpine
+endif
+
+ifeq ($(findstring alpine,$(TAG)),alpine)
+	DOCKERFILE=Dockerfile-Alpine
+else ifeq ($(findstring arm,$(TAG)),arm)
+	DOCKERFILE=Dockerfile-QEMU-ARM
+else
+	DOCKERFILE=Dockerfile
+endif
+
+.PHONY: build
+build: ## Build a Docker image
+	docker build \
+		--pull \
+		--tag freshrss/freshrss:$(TAG) \
+		-f Docker/$(DOCKERFILE) .
+
+.PHONY: start
+start: ## Start the development environment (use Docker)
+	docker run \
+		--rm \
+		-v $(shell pwd):/var/www/FreshRSS:z \
+		-p 8080:80 \
+		-e FRESHRSS_ENV=development \
+		--name freshrss-dev \
+		freshrss/freshrss:$(TAG)
+
+.PHONY: stop
+stop: ## Stop FreshRSS container if any
+	docker stop freshrss-dev
+
+.PHONY: help
+help:
+	@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

+ 13 - 12
README.fr.md

@@ -5,7 +5,7 @@
 * [English version](README.md)
 * [English version](README.md)
 
 
 # FreshRSS
 # FreshRSS
-FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](http://leed.idleman.fr/) ou de [Kriss Feed](https://tontof.net/kriss/feed/).
+FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](https://github.com/LeedRSS/Leed) ou de [Kriss Feed](https://tontof.net/kriss/feed/).
 
 
 Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
 Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
 
 
@@ -43,10 +43,10 @@ FreshRSS n’est fourni avec aucune garantie.
 * Serveur modeste, par exemple sous Linux ou Windows
 * Serveur modeste, par exemple sous Linux ou Windows
 	* 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.8+ (PHP 5.4+ recommandé, et PHP 5.5+ pour les performances, ou PHP 7+ pour d’encore meilleures performances)
-	* Requis : [cURL](https://secure.php.net/curl), [DOM](https://secure.php.net/dom), [XML](https://secure.php.net/xml), [session](https://secure.php.net/session), [ctype](https://secure.php.net/ctype), et [PDO_MySQL](https://secure.php.net/pdo-mysql) ou [PDO_SQLite](https://secure.php.net/pdo-sqlite) ou [PDO_PGSQL](https://secure.php.net/pdo-pgsql)
-	* Recommandés : [JSON](https://secure.php.net/json), [GMP](https://secure.php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](https://secure.php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](https://secure.php.net/mbstring) (pour le texte Unicode), [iconv](https://secure.php.net/iconv) (pour conversion d’encodages), [ZIP](https://secure.php.net/zip) (pour import/export), [zlib](https://secure.php.net/zlib) (pour les flux compressés)
-* MySQL 5.5.3+ (recommandé), ou SQLite 3.7.4+, ou PostgreSQL 9.2+
+* PHP 5.6+ (PHP 7+ recommandé pour de meilleures performances)
+	* Requis : [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype), et [PDO_MySQL](https://www.php.net/pdo-mysql) ou [PDO_SQLite](https://www.php.net/pdo-sqlite) ou [PDO_PGSQL](https://www.php.net/pdo-pgsql)
+	* Recommandés : [GMP](https://www.php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](https://www.php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](https://www.php.net/mbstring) (pour le texte Unicode), [iconv](https://www.php.net/iconv) (pour conversion d’encodages), [ZIP](https://www.php.net/zip) (pour import/export), [zlib](https://www.php.net/zlib) (pour les flux compressés)
+* MySQL 5.5.3+ ou équivalent MariaDB, ou SQLite 3.7.4+, ou PostgreSQL 9.5+
 
 
 
 
 # Téléchargement
 # Téléchargement
@@ -121,7 +121,7 @@ Voir la [documentation de la ligne de commande](cli/README.md) pour plus de dét
 
 
 ## 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.5+ recommandé)
+* En utilisant l’identification par formulaire (requiert JavaScript)
 * 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
 	* Voir par exemple la [documentation d’Apache sur l’authentification](https://httpd.apache.org/docs/trunk/howto/auth.html)
 	* Voir par exemple la [documentation d’Apache sur l’authentification](https://httpd.apache.org/docs/trunk/howto/auth.html)
 		* Créer dans ce cas un fichier `./p/i/.htaccess` avec un fichier `.htpasswd` correspondant.
 		* Créer dans ce cas un fichier `./p/i/.htaccess` avec un fichier `.htpasswd` correspondant.
@@ -187,8 +187,11 @@ Tout client supportant une API de type Google Reader ; Sélection :
 	* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Libre, [F-Droid](https://f-droid.org/fr/packages/org.freshrss.easyrss/))
 	* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Libre, [F-Droid](https://f-droid.org/fr/packages/org.freshrss.easyrss/))
 * GNU/Linux
 * GNU/Linux
 	* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre)
 	* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre)
+* iOS
+	* [Reeder](https://www.reederapp.com/) (Commercial)
 * MacOS
 * MacOS
 	* [Vienna RSS](http://www.vienna-rss.com/) (Libre)
 	* [Vienna RSS](http://www.vienna-rss.com/) (Libre)
+	* [Reeder](https://www.reederapp.com/) (Commercial)
 
 
 ## Via l’API compatible Fever
 ## Via l’API compatible Fever
 
 
@@ -199,11 +202,10 @@ Tout client supportant une API de type Fever ; Sélection :
 * Android
 * Android
 	* [Readably](https://play.google.com/store/apps/details?id=com.isaiasmatewos.readably) (Propriétaire)
 	* [Readably](https://play.google.com/store/apps/details?id=com.isaiasmatewos.readably) (Propriétaire)
 * iOS
 * iOS
-	* [Fiery Feeds](https://itunes.apple.com/app/fiery-feeds-rss-reader/id1158763303) (Propriétaire)
-	* [Unread](https://itunes.apple.com/app/unread-rss-reader/id1252376153) (Propriétaire)
-	* [Reeder-3](https://itunes.apple.com/app/reeder-3/id697846300) (Propriétaire)
+	* [Fiery Feeds](https://apps.apple.com/app/fiery-feeds-rss-reader/id1158763303) (Propriétaire)
+	* [Unread](https://apps.apple.com/app/unread-rss-reader/id1252376153) (Commercial)
 * MacOS
 * MacOS
-	* [Readkit](https://itunes.apple.com/app/readkit/id588726889) (Propriétaire)
+	* [Readkit](https://apps.apple.com/app/readkit/id588726889) (Commercial)
 
 
 
 
 # Bibliothèques incluses
 # Bibliothèques incluses
@@ -213,12 +215,11 @@ Tout client supportant une API de type Fever ; Sélection :
 * [jQuery](https://jquery.com/)
 * [jQuery](https://jquery.com/)
 * [lib_opml](https://github.com/marienfressinaud/lib_opml)
 * [lib_opml](https://github.com/marienfressinaud/lib_opml)
 * [flotr2](http://www.humblesoftware.com/flotr2)
 * [flotr2](http://www.humblesoftware.com/flotr2)
+* [PHPMailer](https://github.com/PHPMailer/PHPMailer)
 
 
 ## Uniquement pour certaines options ou configurations
 ## Uniquement pour certaines options ou configurations
 * [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
 * [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
 * [phpQuery](https://github.com/phpquery/phpquery)
 * [phpQuery](https://github.com/phpquery/phpquery)
-* [Services_JSON](https://pear.php.net/pepr/pepr-proposal-show.php?id=198)
-* [password_compat](https://github.com/ircmaxell/password_compat)
 
 
 
 
 
 

+ 12 - 88
README.md

@@ -5,7 +5,7 @@
 * [Version française](README.fr.md)
 * [Version française](README.fr.md)
 
 
 # FreshRSS
 # FreshRSS
-FreshRSS is a self-hosted RSS feed aggregator like [Leed](http://leed.idleman.fr/) or [Kriss Feed](https://tontof.net/kriss/feed/).
+FreshRSS is a self-hosted RSS feed aggregator like [Leed](https://github.com/LeedRSS/Leed) or [Kriss Feed](https://tontof.net/kriss/feed/).
 
 
 It is lightweight, easy to work with, powerful, and customizable.
 It is lightweight, easy to work with, powerful, and customizable.
 
 
@@ -43,10 +43,10 @@ FreshRSS comes with absolutely no warranty.
 * Light server running Linux or Windows
 * Light server running Linux or Windows
 	* 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.8+ (PHP 5.4+ recommended, and PHP 5.5+ for performance, or PHP 7 for even higher performance)
-	* Required extensions: [cURL](https://secure.php.net/curl), [DOM](https://secure.php.net/dom), [XML](https://secure.php.net/xml), [session](https://secure.php.net/session), [ctype](https://secure.php.net/ctype), and [PDO_MySQL](https://secure.php.net/pdo-mysql) or [PDO_SQLite](https://secure.php.net/pdo-sqlite) or [PDO_PGSQL](https://secure.php.net/pdo-pgsql)
-	* Recommended extensions: [JSON](https://secure.php.net/json), [GMP](https://secure.php.net/gmp) (for API access on 32-bit platforms), [IDN](https://secure.php.net/intl.idn) (for Internationalized Domain Names), [mbstring](https://secure.php.net/mbstring) (for Unicode strings), [iconv](https://secure.php.net/iconv) (for charset conversion), [ZIP](https://secure.php.net/zip) (for import/export), [zlib](https://secure.php.net/zlib) (for compressed feeds)
-* MySQL 5.5.3+ (recommended), or SQLite 3.7.4+, or PostgreSQL 9.2+
+* PHP 5.6+ (PHP 7+ recommended for higher performance)
+	* Required extensions: [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype), and [PDO_MySQL](https://www.php.net/pdo-mysql) or [PDO_SQLite](https://www.php.net/pdo-sqlite) or [PDO_PGSQL](https://www.php.net/pdo-pgsql)
+	* Recommended extensions: [GMP](https://www.php.net/gmp) (for API access on 32-bit platforms), [IDN](https://www.php.net/intl.idn) (for Internationalized Domain Names), [mbstring](https://www.php.net/mbstring) (for Unicode strings), [iconv](https://www.php.net/iconv) (for charset conversion), [ZIP](https://www.php.net/zip) (for import/export), [zlib](https://www.php.net/zlib) (for compressed feeds)
+* MySQL 5.5.3+ or MariaDB equivalent, or SQLite 3.7.4+, or PostgreSQL 9.5+
 
 
 
 
 # Releases
 # Releases
@@ -76,73 +76,6 @@ See the [list of releases](../../releases).
 
 
 More detailed information about installation and server configuration can be found in [our documentation](https://freshrss.github.io/FreshRSS/en/admins/02_Installation.html).
 More detailed information about installation and server configuration can be found in [our documentation](https://freshrss.github.io/FreshRSS/en/admins/02_Installation.html).
 
 
-### Example of full installation on Linux Debian/Ubuntu
-```sh
-# If you use an Apache Web server (otherwise you need another Web server)
-sudo apt-get install apache2
-sudo a2enmod headers expires rewrite ssl	#Apache modules
-
-# Example 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
-sudo service apache2 restart
-
-# For FreshRSS itself (git is optional if you manually download the installation files)
-cd /usr/share/
-sudo apt-get install git
-sudo git clone https://github.com/FreshRSS/FreshRSS.git
-cd FreshRSS
-
-# 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/
-# If you would like to allow updates from the Web interface
-sudo chmod -R g+w .
-
-# Publish FreshRSS in your public HTML directory
-sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
-# 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)
-# or use the Command-Line Interface
-
-# Update to a newer version of FreshRSS with git
-cd /usr/share/FreshRSS
-sudo git pull
-sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
-```
-
-See more commands and git commands in the [Command-Line Interface documentation](cli/README.md).
-
-## Access control
-This is needed if you will be using the multi-user mode, to limit access to FreshRSS. Options Available:
-* form authentication (needs JavaScript, and PHP 5.5+ recommended)
-* HTTP authentication supported by your web server
-	* See [Apache documentation](https://httpd.apache.org/docs/trunk/howto/auth.html)
-		* In that case, create a `./p/i/.htaccess` file with a matching `.htpasswd` file.
-
-## Automatic feed update
-* You can add a Cron job to launch the update script.
-Check the Cron documentation related to your distribution ([Debian/Ubuntu](https://help.ubuntu.com/community/CronHowto), [Red Hat/Fedora](https://fedoraproject.org/wiki/Administration_Guide_Draft/Cron), [Slackware](https://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](https://wiki.gentoo.org/wiki/Cron), [Arch Linux](https://wiki.archlinux.org/index.php/Cron)…).
-It is a good idea to run the cron job as the webserver user (often “www-data”).
-For instance, if you want to run the script every hour:
-
-```
-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
-```
-
 ## Advice
 ## Advice
 * For better security, expose only the `./p/` folder to the Web.
 * For better security, expose only the `./p/` folder to 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.
@@ -156,16 +89,6 @@ Create `/etc/cron.d/FreshRSS` with:
 	* In particular, when importing a new feed, all of its articles will appear at the top of the feed list regardless of their declared date.
 	* In particular, when importing a new feed, all of its articles will appear at the top of the feed list regardless of their declared date.
 
 
 
 
-# Backup
-* You need to keep `./data/config.php`, and `./data/users/*/config.php` files
-* You can export your feed list in OPML format either from the Web interface, or from the [Command-Line Interface](cli/README.md)
-* To save articles, you can use [phpMyAdmin](https://www.phpmyadmin.net) or MySQL tools:
-
-```bash
-mysqldump --skip-comments --disable-keys --user=<db_user> --password --host <db_host> --result-file=freshrss.dump.sql --databases <freshrss_db>
-```
-
-
 # Extensions
 # Extensions
 FreshRSS supports further customizations by adding extensions on top of its core functionality.
 FreshRSS supports further customizations by adding extensions on top of its core functionality.
 See the [repository dedicated to those extensions](https://github.com/FreshRSS/Extensions).
 See the [repository dedicated to those extensions](https://github.com/FreshRSS/Extensions).
@@ -187,8 +110,11 @@ Supported clients are:
 	* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Open source, [F-Droid](https://f-droid.org/packages/org.freshrss.easyrss/))
 	* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Open source, [F-Droid](https://f-droid.org/packages/org.freshrss.easyrss/))
 * GNU/Linux
 * GNU/Linux
 	* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source)
 	* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source)
+* iOS
+	* [Reeder](https://www.reederapp.com/) (Commercial)
 * MacOS
 * MacOS
 	* [Vienna RSS](http://www.vienna-rss.com/) (Open source)
 	* [Vienna RSS](http://www.vienna-rss.com/) (Open source)
+	* [Reeder](https://www.reederapp.com/) (Commercial)
 
 
 ## Fever API
 ## Fever API
 
 
@@ -199,11 +125,10 @@ Supported clients are:
 * Android
 * Android
 	* [Readably](https://play.google.com/store/apps/details?id=com.isaiasmatewos.readably) (Closed source)
 	* [Readably](https://play.google.com/store/apps/details?id=com.isaiasmatewos.readably) (Closed source)
 * iOS
 * iOS
-	* [Fiery Feeds](https://itunes.apple.com/app/fiery-feeds-rss-reader/id1158763303) (Closed source)
-	* [Unread](https://itunes.apple.com/app/unread-rss-reader/id1252376153) (Closed source)
-	* [Reeder-3](https://itunes.apple.com/app/reeder-3/id697846300) (Closed source)
+	* [Fiery Feeds](https://apps.apple.com/app/fiery-feeds-rss-reader/id1158763303) (Closed source)
+	* [Unread](https://apps.apple.com/app/unread-rss-reader/id1252376153) (Commercial)
 * MacOS
 * MacOS
-	* [Readkit](https://itunes.apple.com/app/readkit/id588726889) (Closed source)
+	* [Readkit](https://apps.apple.com/app/readkit/id588726889) (Commercial)
 
 
 
 
 # Included libraries
 # Included libraries
@@ -213,12 +138,11 @@ Supported clients are:
 * [jQuery](https://jquery.com/)
 * [jQuery](https://jquery.com/)
 * [lib_opml](https://github.com/marienfressinaud/lib_opml)
 * [lib_opml](https://github.com/marienfressinaud/lib_opml)
 * [flotr2](http://www.humblesoftware.com/flotr2)
 * [flotr2](http://www.humblesoftware.com/flotr2)
+* [PHPMailer](https://github.com/PHPMailer/PHPMailer)
 
 
 ## Only for some options or configurations
 ## Only for some options or configurations
 * [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
 * [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
 * [phpQuery](https://github.com/phpquery/phpquery)
 * [phpQuery](https://github.com/phpquery/phpquery)
-* [Services_JSON](https://pear.php.net/pepr/pepr-proposal-show.php?id=198)
-* [password_compat](https://github.com/ircmaxell/password_compat)
 
 
 [travis-badge]:https://travis-ci.org/FreshRSS/FreshRSS.svg?branch=master
 [travis-badge]:https://travis-ci.org/FreshRSS/FreshRSS.svg?branch=master
 [travis-link]:https://travis-ci.org/FreshRSS/FreshRSS
 [travis-link]:https://travis-ci.org/FreshRSS/FreshRSS

+ 11 - 3
app/.htaccess

@@ -1,3 +1,11 @@
-Order	Allow,Deny
-Deny	from all
-Satisfy	all
+# Apache 2.2
+<IfModule !mod_authz_core.c>
+	Order	Allow,Deny
+	Deny	from all
+	Satisfy	all
+</IfModule>
+
+# Apache 2.4
+<IfModule mod_authz_core.c>
+	Require all denied
+</IfModule>

+ 10 - 4
app/Controllers/authController.php

@@ -169,10 +169,6 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 				return;
 				return;
 			}
 			}
 
 
-			if (!function_exists('password_verify')) {
-				include_once(LIB_PATH . '/password_compat.php');
-			}
-
 			$s = $conf->passwordHash;
 			$s = $conf->passwordHash;
 			$ok = password_verify($password, $s);
 			$ok = password_verify($password, $s);
 			unset($password);
 			unset($password);
@@ -203,12 +199,22 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 
 
 	/**
 	/**
 	 * This action gives possibility to a user to create an account.
 	 * This action gives possibility to a user to create an account.
+	 *
+	 * The user is redirected to the home if he's connected.
+	 *
+	 * A 403 is sent if max number of registrations is reached.
 	 */
 	 */
 	public function registerAction() {
 	public function registerAction() {
+		if (FreshRSS_Auth::hasAccess()) {
+			Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+		}
+
 		if (max_registrations_reached()) {
 		if (max_registrations_reached()) {
 			Minz_Error::error(403);
 			Minz_Error::error(403);
 		}
 		}
 
 
+		$this->view->show_tos_checkbox = file_exists(join_path(DATA_PATH, 'tos.html'));
+		$this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation;
 		Minz_View::prependTitle(_t('gen.auth.registration.title') . ' · ');
 		Minz_View::prependTitle(_t('gen.auth.registration.title') . ' · ');
 	}
 	}
 }
 }

+ 54 - 5
app/Controllers/configureController.php

@@ -48,6 +48,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::param('topline_favorite', false);
 			FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::param('topline_favorite', false);
 			FreshRSS_Context::$user_conf->topline_date = Minz_Request::param('topline_date', false);
 			FreshRSS_Context::$user_conf->topline_date = Minz_Request::param('topline_date', false);
 			FreshRSS_Context::$user_conf->topline_link = Minz_Request::param('topline_link', false);
 			FreshRSS_Context::$user_conf->topline_link = Minz_Request::param('topline_link', false);
+			FreshRSS_Context::$user_conf->topline_display_authors = Minz_Request::param('topline_display_authors', false);
 			FreshRSS_Context::$user_conf->bottomline_read = Minz_Request::param('bottomline_read', false);
 			FreshRSS_Context::$user_conf->bottomline_read = Minz_Request::param('bottomline_read', false);
 			FreshRSS_Context::$user_conf->bottomline_favorite = Minz_Request::param('bottomline_favorite', false);
 			FreshRSS_Context::$user_conf->bottomline_favorite = Minz_Request::param('bottomline_favorite', false);
 			FreshRSS_Context::$user_conf->bottomline_sharing = Minz_Request::param('bottomline_sharing', false);
 			FreshRSS_Context::$user_conf->bottomline_sharing = Minz_Request::param('bottomline_sharing', false);
@@ -166,8 +167,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 	 * tab and up.
 	 * tab and up.
 	 */
 	 */
 	public function shortcutAction() {
 	public function shortcutAction() {
-		global $SHORTCUT_KEYS;
-		$this->view->list_keys = $SHORTCUT_KEYS;
+		$this->view->list_keys = SHORTCUT_KEYS;
 
 
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
 			FreshRSS_Context::$user_conf->shortcuts = validateShortcutList(Minz_Request::param('shortcuts'));
 			FreshRSS_Context::$user_conf->shortcuts = validateShortcutList(Minz_Request::param('shortcuts'));
@@ -196,9 +196,31 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 	 */
 	 */
 	public function archivingAction() {
 	public function archivingAction() {
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
-			FreshRSS_Context::$user_conf->old_entries = Minz_Request::param('old_entries', 3);
-			FreshRSS_Context::$user_conf->keep_history_default = Minz_Request::param('keep_history_default', 0);
+			if (!Minz_Request::paramBoolean('enable_keep_max')) {
+				$keepMax = false;
+			} elseif (!$keepMax = Minz_Request::param('keep_max')) {
+				$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
+			}
+			if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) {
+				$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
+				if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) {
+					$keepPeriod = str_replace('1', Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
+				}
+			} else {
+				$keepPeriod = false;
+			}
+
 			FreshRSS_Context::$user_conf->ttl_default = Minz_Request::param('ttl_default', FreshRSS_Feed::TTL_DEFAULT);
 			FreshRSS_Context::$user_conf->ttl_default = Minz_Request::param('ttl_default', FreshRSS_Feed::TTL_DEFAULT);
+			FreshRSS_Context::$user_conf->archiving = [
+				'keep_period' => $keepPeriod,
+				'keep_max' => $keepMax,
+				'keep_min' => Minz_Request::param('keep_min_default', 0),
+				'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
+				'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
+				'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
+			];
+			FreshRSS_Context::$user_conf->keep_history_default = null;	//Legacy < FreshRSS 1.15
+			FreshRSS_Context::$user_conf->old_entries = null;	//Legacy < FreshRSS 1.15
 			FreshRSS_Context::$user_conf->save();
 			FreshRSS_Context::$user_conf->save();
 			invalidateHttpCache();
 			invalidateHttpCache();
 
 
@@ -206,7 +228,20 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			                   array('c' => 'configure', 'a' => 'archiving'));
 			                   array('c' => 'configure', 'a' => 'archiving'));
 		}
 		}
 
 
-		Minz_View::prependTitle(_t('conf.archiving.title') . ' · ');
+		$volatile = [
+				'enable_keep_period' => false,
+				'keep_period_count' => '3',
+				'keep_period_unit' => 'P1M',
+			];
+		$keepPeriod = FreshRSS_Context::$user_conf->archiving['keep_period'];
+		if (preg_match('/^PT?(?P<count>\d+)[YMWDH]$/', $keepPeriod, $matches)) {
+			$volatile = [
+				'enable_keep_period' => true,
+				'keep_period_count' => $matches['count'],
+				'keep_period_unit' => str_replace($matches['count'], 1, $keepPeriod),
+			];
+		}
+		FreshRSS_Context::$user_conf->volatile = $volatile;
 
 
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$this->view->nb_total = $entryDAO->count();
 		$this->view->nb_total = $entryDAO->count();
@@ -217,6 +252,8 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 		if (FreshRSS_Auth::hasAccess('admin')) {
 		if (FreshRSS_Auth::hasAccess('admin')) {
 			$this->view->size_total = $databaseDAO->size(true);
 			$this->view->size_total = $databaseDAO->size(true);
 		}
 		}
+
+		Minz_View::prependTitle(_t('conf.archiving.title') . ' · ');
 	}
 	}
 
 
 	/**
 	/**
@@ -292,15 +329,24 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 	 * configuration values then sends a notification to the user.
 	 * configuration values then sends a notification to the user.
 	 *
 	 *
 	 * The options available on the page are:
 	 * The options available on the page are:
+	 *   - instance name (default: FreshRSS)
+	 *   - auto update URL (default: false)
+	 *   - force emails validation (default: false)
 	 *   - user limit (default: 1)
 	 *   - user limit (default: 1)
 	 *   - user category limit (default: 16384)
 	 *   - user category limit (default: 16384)
 	 *   - user feed limit (default: 16384)
 	 *   - user feed limit (default: 16384)
 	 *   - user login duration for form auth (default: 2592000)
 	 *   - user login duration for form auth (default: 2592000)
+	 *
+	 * The `force-email-validation` is ignored with PHP < 5.5
 	 */
 	 */
 	public function systemAction() {
 	public function systemAction() {
 		if (!FreshRSS_Auth::hasAccess('admin')) {
 		if (!FreshRSS_Auth::hasAccess('admin')) {
 			Minz_Error::error(403);
 			Minz_Error::error(403);
 		}
 		}
+
+		$can_enable_email_validation = version_compare(PHP_VERSION, '5.5') >= 0;
+		$this->view->can_enable_email_validation = $can_enable_email_validation;
+
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
 			$limits = FreshRSS_Context::$system_conf->limits;
 			$limits = FreshRSS_Context::$system_conf->limits;
 			$limits['max_registrations'] = Minz_Request::param('max-registrations', 1);
 			$limits['max_registrations'] = Minz_Request::param('max-registrations', 1);
@@ -310,6 +356,9 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			FreshRSS_Context::$system_conf->limits = $limits;
 			FreshRSS_Context::$system_conf->limits = $limits;
 			FreshRSS_Context::$system_conf->title = Minz_Request::param('instance-name', 'FreshRSS');
 			FreshRSS_Context::$system_conf->title = Minz_Request::param('instance-name', 'FreshRSS');
 			FreshRSS_Context::$system_conf->auto_update_url = Minz_Request::param('auto-update-url', false);
 			FreshRSS_Context::$system_conf->auto_update_url = Minz_Request::param('auto-update-url', false);
+			if ($can_enable_email_validation) {
+				FreshRSS_Context::$system_conf->force_email_validation = Minz_Request::param('force-email-validation', false);
+			}
 			FreshRSS_Context::$system_conf->save();
 			FreshRSS_Context::$system_conf->save();
 
 
 			invalidateHttpCache();
 			invalidateHttpCache();

+ 5 - 17
app/Controllers/entryController.php

@@ -17,7 +17,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 		// If ajax request, we do not print layout
 		// If ajax request, we do not print layout
 		$this->ajax = Minz_Request::param('ajax');
 		$this->ajax = Minz_Request::param('ajax');
 		if ($this->ajax) {
 		if ($this->ajax) {
-			$this->view->_useLayout(false);
+			$this->view->_layout(false);
 			Minz_Request::_param('ajax');
 			Minz_Request::_param('ajax');
 		}
 		}
 	}
 	}
@@ -181,32 +181,20 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 	public function purgeAction() {
 	public function purgeAction() {
 		@set_time_limit(300);
 		@set_time_limit(300);
 
 
-		$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
-		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
-
-		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feeds = $feedDAO->listFeeds();
 		$feeds = $feedDAO->listFeeds();
 		$nb_total = 0;
 		$nb_total = 0;
 
 
 		invalidateHttpCache();
 		invalidateHttpCache();
 
 
-		foreach ($feeds as $feed) {
-			$feed_history = $feed->keepHistory();
-			if (FreshRSS_Feed::KEEP_HISTORY_DEFAULT === $feed_history) {
-				$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
-			}
+		$feedDAO->beginTransaction();
 
 
-			if ($feed_history >= 0) {
-				$nb = $entryDAO->cleanOldEntries($feed->id(), $date_min, $feed_history);
-				if ($nb > 0) {
-					$nb_total += $nb;
-					Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url(false) . ']');
-				}
-			}
+		foreach ($feeds as $feed) {
+			$nb_total += $feed->cleanOldEntries();
 		}
 		}
 
 
 		$feedDAO->updateCachedValues();
 		$feedDAO->updateCachedValues();
+		$feedDAO->commit();
 
 
 		$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
 		$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
 		$databaseDAO->minorDbMaintenance();
 		$databaseDAO->minorDbMaintenance();

+ 2 - 2
app/Controllers/extensionController.php

@@ -80,10 +80,10 @@ class FreshRSS_extension_Controller extends Minz_ActionController {
 	 */
 	 */
 	public function configureAction() {
 	public function configureAction() {
 		if (Minz_Request::param('ajax')) {
 		if (Minz_Request::param('ajax')) {
-			$this->view->_useLayout(false);
+			$this->view->_layout(false);
 		} else {
 		} else {
 			$this->indexAction();
 			$this->indexAction();
-			$this->view->change_view('extension', 'index');
+			$this->view->_path('extension/index.phtml');
 		}
 		}
 
 
 		$ext_name = urldecode(Minz_Request::param('e'));
 		$ext_name = urldecode(Minz_Request::param('e'));

+ 4 - 24
app/Controllers/feedController.php

@@ -267,10 +267,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$maxFeeds = 10;
 			$maxFeeds = 10;
 		}
 		}
 
 
-		// Calculate date of oldest entries we accept in DB.
-		$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
-		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
-
 		// WebSub (PubSubHubbub) support
 		// WebSub (PubSubHubbub) support
 		$pubsubhubbubEnabledGeneral = FreshRSS_Context::$system_conf->pubsubhubbub_enabled;
 		$pubsubhubbubEnabledGeneral = FreshRSS_Context::$system_conf->pubsubhubbub_enabled;
 		$pshbMinAge = time() - (3600 * 24);  //TODO: Make a configuration.
 		$pshbMinAge = time() - (3600 * 24);  //TODO: Make a configuration.
@@ -323,12 +319,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				continue;
 				continue;
 			}
 			}
 
 
-			$feed_history = $feed->keepHistory();
-			if ($isNewFeed) {
-				$feed_history = FreshRSS_Feed::KEEP_HISTORY_INFINITE;
-			} elseif (FreshRSS_Feed::KEEP_HISTORY_DEFAULT === $feed_history) {
-				$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
-			}
 			$needFeedCacheRefresh = false;
 			$needFeedCacheRefresh = false;
 
 
 			// We want chronological order and SimplePie uses reverse order.
 			// We want chronological order and SimplePie uses reverse order.
@@ -376,15 +366,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 							}
 							}
 							$entryDAO->updateEntry($entry->toArray());
 							$entryDAO->updateEntry($entry->toArray());
 						}
 						}
-					} elseif ($feed_history == 0 && $entry_date < $date_min) {
-						// This entry should not be added considering configuration and date.
-						$oldGuids[] = $entry->guid();
 					} else {
 					} else {
 						$id = uTimeString();
 						$id = uTimeString();
 						$entry->_id($id);
 						$entry->_id($id);
-						if ($entry_date < $date_min) {
-							$entry->_isRead(true);	//Old article that was not in database. Probably an error, so mark as read
-						}
 
 
 						$entry->applyFilterActions();
 						$entry->applyFilterActions();
 
 
@@ -413,23 +397,19 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				$entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime);
 				$entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime);
 			}
 			}
 
 
-			if ($feed_history >= 0 && mt_rand(0, 30) === 1) {
-				// TODO: move this function in web cron when available (see entry::purge)
-				// Remove old entries once in 30.
+			if (mt_rand(0, 30) === 1) {	// Remove old entries once in 30.
 				if (!$entryDAO->inTransaction()) {
 				if (!$entryDAO->inTransaction()) {
 					$entryDAO->beginTransaction();
 					$entryDAO->beginTransaction();
 				}
 				}
-
-				$nb = $entryDAO->cleanOldEntries($feed->id(), $date_min, max($feed_history, count($entries) + 10));
+				$nb = $feed->cleanOldEntries();
 				if ($nb > 0) {
 				if ($nb > 0) {
 					$needFeedCacheRefresh = true;
 					$needFeedCacheRefresh = true;
-					Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url(false) . ']');
 				}
 				}
 			}
 			}
 
 
 			$feedDAO->updateLastUpdate($feed->id(), false, $mtime);
 			$feedDAO->updateLastUpdate($feed->id(), false, $mtime);
 			if ($needFeedCacheRefresh) {
 			if ($needFeedCacheRefresh) {
-				$feedDAO->updateCachedValue($feed->id());
+				$feedDAO->updateCachedValues($feed->id());
 			}
 			}
 			if ($entryDAO->inTransaction()) {
 			if ($entryDAO->inTransaction()) {
 				$entryDAO->commit();
 				$entryDAO->commit();
@@ -530,7 +510,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			);
 			);
 			Minz_Session::_param('notification', $notif);
 			Minz_Session::_param('notification', $notif);
 			// No layout in ajax request.
 			// No layout in ajax request.
-			$this->view->_useLayout(false);
+			$this->view->_layout(false);
 		} 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) {

+ 19 - 3
app/Controllers/importExportController.php

@@ -29,7 +29,25 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		Minz_View::prependTitle(_t('sub.import_export.title') . ' · ');
 		Minz_View::prependTitle(_t('sub.import_export.title') . ' · ');
 	}
 	}
 
 
+	private static function megabytes($size_str) {
+		switch (substr($size_str, -1)) {
+			case 'M': case 'm': return (int)$size_str;
+			case 'K': case 'k': return (int)$size_str / 1024;
+			case 'G': case 'g': return (int)$size_str * 1024;
+		}
+		return $size_str;
+	}
+
+	private static function minimumMemory($mb) {
+		$mb = (int)$mb;
+		$ini = self::megabytes(ini_get('memory_limit'));
+		if ($ini < $mb) {
+			ini_set('memory_limit', $mb . 'M');
+		}
+	}
+
 	public function importFile($name, $path, $username = null) {
 	public function importFile($name, $path, $username = null) {
+		self::minimumMemory(256);
 		require_once(LIB_PATH . '/lib_opml.php');
 		require_once(LIB_PATH . '/lib_opml.php');
 
 
 		$this->catDAO = new FreshRSS_CategoryDAO($username);
 		$this->catDAO = new FreshRSS_CategoryDAO($username);
@@ -709,8 +727,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
 		$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
 		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
 		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
 
 
-		$this->entryDAO->disableBuffering();
-
 		if ($export_feeds === true) {
 		if ($export_feeds === true) {
 			//All feeds
 			//All feeds
 			$export_feeds = $this->feedDAO->listFeedsIds();
 			$export_feeds = $this->feedDAO->listFeedsIds();
@@ -773,7 +789,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		if (!Minz_Request::isPost()) {
 		if (!Minz_Request::isPost()) {
 			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
 			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
 		}
 		}
-		$this->view->_useLayout(false);
+		$this->view->_layout(false);
 
 
 		$nb_files = 0;
 		$nb_files = 0;
 		try {
 		try {

+ 19 - 2
app/Controllers/indexController.php

@@ -155,7 +155,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		// No layout for RSS output.
 		// No layout for RSS output.
 		$this->view->url = PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
 		$this->view->url = PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
 		$this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title();
 		$this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title();
-		$this->view->_useLayout(false);
+		$this->view->_layout(false);
 		header('Content-Type: application/rss+xml; charset=utf-8');
 		header('Content-Type: application/rss+xml; charset=utf-8');
 	}
 	}
 
 
@@ -173,7 +173,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 	private function updateContext() {
 	private function updateContext() {
 		if (empty(FreshRSS_Context::$categories)) {
 		if (empty(FreshRSS_Context::$categories)) {
 			$catDAO = FreshRSS_Factory::createCategoryDao();
 			$catDAO = FreshRSS_Factory::createCategoryDao();
-			FreshRSS_Context::$categories = $catDAO->listCategories();
+			FreshRSS_Context::$categories = $catDAO->listSortedCategories();
 		}
 		}
 
 
 		// Update number of read / unread variables.
 		// Update number of read / unread variables.
@@ -259,6 +259,23 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		Minz_View::prependTitle(_t('index.about.title') . ' · ');
 		Minz_View::prependTitle(_t('index.about.title') . ' · ');
 	}
 	}
 
 
+	/**
+	 * This action displays the EULA page of FreshRSS.
+	 * This page is enabled only if admin created a data/tos.html file.
+	 * The content of the page is the content of data/tos.html.
+	 * It returns 404 if there is no EULA.
+	 */
+	public function tosAction() {
+		$terms_of_service = file_get_contents(join_path(DATA_PATH, 'tos.html'));
+		if (!$terms_of_service) {
+			Minz_Error::error(404);
+		}
+
+		$this->view->terms_of_service = $terms_of_service;
+		$this->view->can_register = !max_registrations_reached();
+		Minz_View::prependTitle(_t('index.tos.title') . ' · ');
+	}
+
 	/**
 	/**
 	 * This action displays logs of FreshRSS for the current user.
 	 * This action displays logs of FreshRSS for the current user.
 	 */
 	 */

+ 1 - 1
app/Controllers/javascriptController.php

@@ -2,7 +2,7 @@
 
 
 class FreshRSS_javascript_Controller extends Minz_ActionController {
 class FreshRSS_javascript_Controller extends Minz_ActionController {
 	public function firstAction() {
 	public function firstAction() {
-		$this->view->_useLayout(false);
+		$this->view->_layout(false);
 	}
 	}
 
 
 	public function actualizeAction() {
 	public function actualizeAction() {

+ 61 - 6
app/Controllers/subscriptionController.php

@@ -19,7 +19,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 
 
 		$catDAO->checkDefault();
 		$catDAO->checkDefault();
 		$feedDAO->updateTTL();
 		$feedDAO->updateTTL();
-		$this->view->categories = $catDAO->listCategories(false);
+		$this->view->categories = $catDAO->listSortedCategories(false);
 		$this->view->default_category = $catDAO->getDefault();
 		$this->view->default_category = $catDAO->getDefault();
 	}
 	}
 
 
@@ -74,7 +74,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 	 */
 	 */
 	public function feedAction() {
 	public function feedAction() {
 		if (Minz_Request::param('ajax')) {
 		if (Minz_Request::param('ajax')) {
-			$this->view->_useLayout(false);
+			$this->view->_layout(false);
 		}
 		}
 
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -121,6 +121,32 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 				$feed->_attributes('timeout', null);
 				$feed->_attributes('timeout', null);
 			}
 			}
 
 
+			if (Minz_Request::paramBoolean('use_default_purge_options')) {
+				$feed->_attributes('archiving', null);
+			} else {
+				if (!Minz_Request::paramBoolean('enable_keep_max')) {
+					$keepMax = false;
+				} elseif (!$keepMax = Minz_Request::param('keep_max')) {
+					$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
+				}
+				if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) {
+					$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
+					if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) {
+						$keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
+					}
+				} else {
+					$keepPeriod = false;
+				}
+				$feed->_attributes('archiving', [
+					'keep_period' => $keepPeriod,
+					'keep_max' => $keepMax,
+					'keep_min' => intval(Minz_Request::param('keep_min', 0)),
+					'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
+					'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
+					'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
+				]);
+			}
+
 			$feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::param('filteractions_read', '')));
 			$feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::param('filteractions_read', '')));
 
 
 			$values = array(
 			$values = array(
@@ -132,7 +158,6 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 				'pathEntries' => Minz_Request::param('path_entries', ''),
 				'pathEntries' => Minz_Request::param('path_entries', ''),
 				'priority' => intval(Minz_Request::param('priority', FreshRSS_Feed::PRIORITY_MAIN_STREAM)),
 				'priority' => intval(Minz_Request::param('priority', FreshRSS_Feed::PRIORITY_MAIN_STREAM)),
 				'httpAuth' => $httpAuth,
 				'httpAuth' => $httpAuth,
-				'keep_history' => intval(Minz_Request::param('keep_history', FreshRSS_Feed::KEEP_HISTORY_DEFAULT)),
 				'ttl' => $ttl * ($mute ? -1 : 1),
 				'ttl' => $ttl * ($mute ? -1 : 1),
 				'attributes' => $feed->attributes(),
 				'attributes' => $feed->attributes(),
 			);
 			);
@@ -152,7 +177,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 	}
 	}
 
 
 	public function categoryAction() {
 	public function categoryAction() {
-		$this->view->_useLayout(false);
+		$this->view->_layout(false);
 
 
 		$categoryDAO = FreshRSS_Factory::createCategoryDao();
 		$categoryDAO = FreshRSS_Factory::createCategoryDao();
 
 
@@ -165,9 +190,39 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 		$this->view->category = $category;
 		$this->view->category = $category;
 
 
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
-			$values = array(
+			if (Minz_Request::paramBoolean('use_default_purge_options')) {
+				$category->_attributes('archiving', null);
+			} else {
+				if (!Minz_Request::paramBoolean('enable_keep_max')) {
+					$keepMax = false;
+				} elseif (!$keepMax = Minz_Request::param('keep_max')) {
+					$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
+				}
+				if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) {
+					$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
+					if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) {
+						$keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
+					}
+				} else {
+					$keepPeriod = false;
+				}
+				$category->_attributes('archiving', [
+					'keep_period' => $keepPeriod,
+					'keep_max' => $keepMax,
+					'keep_min' => intval(Minz_Request::param('keep_min', 0)),
+					'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
+					'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
+					'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
+				]);
+			}
+
+			$position = Minz_Request::param('position');
+			$category->_attributes('position', '' === $position ? null : (int) $position);
+
+			$values = [
 				'name' => Minz_Request::param('name', ''),
 				'name' => Minz_Request::param('name', ''),
-			);
+				'attributes' => $category->attributes(),
+			];
 
 
 			invalidateHttpCache();
 			invalidateHttpCache();
 
 

+ 2 - 2
app/Controllers/tagController.php

@@ -16,7 +16,7 @@ class FreshRSS_tag_Controller extends Minz_ActionController {
 		// If ajax request, we do not print layout
 		// If ajax request, we do not print layout
 		$this->ajax = Minz_Request::param('ajax');
 		$this->ajax = Minz_Request::param('ajax');
 		if ($this->ajax) {
 		if ($this->ajax) {
-			$this->view->_useLayout(false);
+			$this->view->_layout(false);
 			Minz_Request::_param('ajax');
 			Minz_Request::_param('ajax');
 		}
 		}
 	}
 	}
@@ -70,7 +70,7 @@ class FreshRSS_tag_Controller extends Minz_ActionController {
 	}
 	}
 
 
 	public function getTagsForEntryAction() {
 	public function getTagsForEntryAction() {
-		$this->view->_useLayout(false);
+		$this->view->_layout(false);
 		header('Content-Type: application/json; charset=UTF-8');
 		header('Content-Type: application/json; charset=UTF-8');
 		header('Cache-Control: private, no-cache, no-store, must-revalidate');
 		header('Cache-Control: private, no-cache, no-store, must-revalidate');
 		$id_entry = Minz_Request::param('id_entry', 0);
 		$id_entry = Minz_Request::param('id_entry', 0);

+ 1 - 1
app/Controllers/updateController.php

@@ -89,7 +89,7 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 	}
 	}
 
 
 	public function checkAction() {
 	public function checkAction() {
-		$this->view->change_view('update', 'index');
+		$this->view->_path('update/index.phtml');
 
 
 		if (file_exists(UPDATE_FILENAME)) {
 		if (file_exists(UPDATE_FILENAME)) {
 			// There is already an update file to apply: we don't need to check
 			// There is already an update file to apply: we don't need to check

+ 230 - 46
app/Controllers/userController.php

@@ -8,26 +8,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	// so do not use a too high cost
 	// so do not use a too high cost
 	const BCRYPT_COST = 9;
 	const BCRYPT_COST = 9;
 
 
-	/**
-	 * This action is called before every other action in that class. It is
-	 * the common boiler plate for every action. It is triggered by the
-	 * underlying framework.
-	 *
-	 * @todo clean up the access condition.
-	 */
-	public function firstAction() {
-		if (!FreshRSS_Auth::hasAccess() && !(
-				Minz_Request::actionName() === 'create' &&
-				!max_registrations_reached()
-		)) {
-			Minz_Error::error(403);
-		}
-	}
-
 	public static function hashPassword($passwordPlain) {
 	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));
 		$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
 		$passwordPlain = '';
 		$passwordPlain = '';
 		$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);	//Compatibility with bcrypt.js
 		$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);	//Compatibility with bcrypt.js
@@ -52,12 +33,23 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		return false;
 		return false;
 	}
 	}
 
 
-	public static function updateUser($user, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) {
+	public static function updateUser($user, $email, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) {
 		$userConfig = get_user_configuration($user);
 		$userConfig = get_user_configuration($user);
 		if ($userConfig === null) {
 		if ($userConfig === null) {
 			return false;
 			return false;
 		}
 		}
 
 
+		if ($email !== null && $userConfig->mail_login !== $email) {
+			$userConfig->mail_login = $email;
+
+			if (FreshRSS_Context::$system_conf->force_email_validation) {
+				$salt = FreshRSS_Context::$system_conf->salt;
+				$userConfig->email_validation_token = sha1($salt . uniqid(mt_rand(), true));
+				$mailer = new FreshRSS_User_Mailer();
+				$mailer->send_email_need_validation($user, $userConfig);
+			}
+		}
+
 		if ($passwordPlain != '') {
 		if ($passwordPlain != '') {
 			$passwordHash = self::hashPassword($passwordPlain);
 			$passwordHash = self::hashPassword($passwordPlain);
 			$userConfig->passwordHash = $passwordHash;
 			$userConfig->passwordHash = $passwordHash;
@@ -103,7 +95,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			$apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true);
 			$apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true);
 
 
 			$username = Minz_Request::param('username');
 			$username = Minz_Request::param('username');
-			$ok = self::updateUser($username, $passwordPlain, $apiPasswordPlain, array(
+			$ok = self::updateUser($username, null, $passwordPlain, $apiPasswordPlain, array(
 				'token' => Minz_Request::param('token', null),
 				'token' => Minz_Request::param('token', null),
 			));
 			));
 
 
@@ -126,25 +118,63 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	 * This action displays the user profile page.
 	 * This action displays the user profile page.
 	 */
 	 */
 	public function profileAction() {
 	public function profileAction() {
+		if (!FreshRSS_Auth::hasAccess()) {
+			Minz_Error::error(403);
+		}
+
+		$email_not_verified = FreshRSS_Context::$user_conf->email_validation_token !== '';
+		$this->view->disable_aside = false;
+		if ($email_not_verified) {
+			$this->view->_layout('simple');
+			$this->view->disable_aside = true;
+		}
+
 		Minz_View::prependTitle(_t('conf.profile.title') . ' · ');
 		Minz_View::prependTitle(_t('conf.profile.title') . ' · ');
 
 
 		Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
 		Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
 
 
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
+			$system_conf = FreshRSS_Context::$system_conf;
+			$user_config = FreshRSS_Context::$user_conf;
+			$old_email = $user_config->mail_login;
+
+			$email = trim(Minz_Request::param('email', ''));
 			$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
 			$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
 			Minz_Request::_param('newPasswordPlain');	//Discard plain-text password ASAP
 			Minz_Request::_param('newPasswordPlain');	//Discard plain-text password ASAP
 			$_POST['newPasswordPlain'] = '';
 			$_POST['newPasswordPlain'] = '';
 
 
 			$apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true);
 			$apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true);
 
 
-			$ok = self::updateUser(Minz_Session::param('currentUser'), $passwordPlain, $apiPasswordPlain, array(
+			if ($system_conf->force_email_validation && empty($email)) {
+				Minz_Request::bad(
+					_t('user.email.feedback.required'),
+					array('c' => 'user', 'a' => 'profile')
+				);
+			}
+
+			if (!empty($email) && !validateEmailAddress($email)) {
+				Minz_Request::bad(
+					_t('user.email.feedback.invalid'),
+					array('c' => 'user', 'a' => 'profile')
+				);
+			}
+
+			$ok = self::updateUser(
+				Minz_Session::param('currentUser'),
+				$email,
+				$passwordPlain,
+				$apiPasswordPlain,
+				array(
 					'token' => Minz_Request::param('token', null),
 					'token' => Minz_Request::param('token', null),
-				));
+				)
+			);
 
 
 			Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash);
 			Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash);
 
 
 			if ($ok) {
 			if ($ok) {
-				if ($passwordPlain == '') {
+				if ($system_conf->force_email_validation && $email !== $old_email) {
+					Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'validateEmail'));
+				} elseif ($passwordPlain == '') {
 					Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'profile'));
 					Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'profile'));
 				} else {
 				} else {
 					Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'index', 'a' => 'index'));
 					Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'index', 'a' => 'index'));
@@ -166,6 +196,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 
 
 		Minz_View::prependTitle(_t('admin.user.title') . ' · ');
 		Minz_View::prependTitle(_t('admin.user.title') . ' · ');
 
 
+		$this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation;
 		$this->view->current_user = Minz_Request::param('u');
 		$this->view->current_user = Minz_Request::param('u');
 
 
 		$this->view->nb_articles = 0;
 		$this->view->nb_articles = 0;
@@ -180,9 +211,19 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		}
 		}
 	}
 	}
 
 
-	public static function createUser($new_user_name, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) {
-		if (!is_array($userConfig)) {
-			$userConfig = array();
+	public static function createUser($new_user_name, $email, $passwordPlain, $apiPasswordPlain = '', $userConfigOverride = [], $insertDefaultFeeds = true) {
+		$userConfig = [];
+
+		$customUserConfigPath = join_path(DATA_PATH, 'config-user.custom.php');
+		if (file_exists($customUserConfigPath)) {
+			$customUserConfig = include($customUserConfigPath);
+			if (is_array($customUserConfig)) {
+				$userConfig = $customUserConfig;
+			}
+		}
+
+		if (is_array($userConfigOverride)) {
+			$userConfig = array_merge($userConfig, $userConfigOverride);
 		}
 		}
 
 
 		$ok = self::checkUsername($new_user_name);
 		$ok = self::checkUsername($new_user_name);
@@ -206,9 +247,9 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
 			$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
 		}
 		}
 		if ($ok) {
 		if ($ok) {
-			$userDAO = new FreshRSS_UserDAO();
-			$ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds);
-			$ok &= self::updateUser($new_user_name, $passwordPlain, $apiPasswordPlain);
+			$newUserDAO = FreshRSS_Factory::createUserDao($new_user_name);
+			$ok &= $newUserDAO->createUser($insertDefaultFeeds);
+			$ok &= self::updateUser($new_user_name, $email, $passwordPlain, $apiPasswordPlain);
 		}
 		}
 		return $ok;
 		return $ok;
 	}
 	}
@@ -219,6 +260,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	 * Request parameters are:
 	 * Request parameters are:
 	 *   - new_user_language
 	 *   - new_user_language
 	 *   - new_user_name
 	 *   - new_user_name
+	 *   - new_user_email
 	 *   - new_user_passwordPlain
 	 *   - new_user_passwordPlain
 	 *   - r (i.e. a redirection url, optional)
 	 *   - r (i.e. a redirection url, optional)
 	 *
 	 *
@@ -226,15 +268,43 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	 * @todo handle r redirection in Minz_Request::forward directly?
 	 * @todo handle r redirection in Minz_Request::forward directly?
 	 */
 	 */
 	public function createAction() {
 	public function createAction() {
-		if (Minz_Request::isPost() && (
-				FreshRSS_Auth::hasAccess('admin') ||
-				!max_registrations_reached()
-		)) {
+		if (!FreshRSS_Auth::hasAccess('admin') && max_registrations_reached()) {
+			Minz_Error::error(403);
+		}
+
+		if (Minz_Request::isPost()) {
+			$system_conf = FreshRSS_Context::$system_conf;
+
 			$new_user_name = Minz_Request::param('new_user_name');
 			$new_user_name = Minz_Request::param('new_user_name');
+			$email = Minz_Request::param('new_user_email', '');
 			$passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
 			$passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
 			$new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language);
 			$new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language);
 
 
-			$ok = self::createUser($new_user_name, $passwordPlain, '', array('language' => $new_user_language));
+			$tos_enabled = file_exists(join_path(DATA_PATH, 'tos.html'));
+			$accept_tos = Minz_Request::param('accept_tos', false);
+
+			if ($system_conf->force_email_validation && empty($email)) {
+				Minz_Request::bad(
+					_t('user.email.feedback.required'),
+					array('c' => 'auth', 'a' => 'register')
+				);
+			}
+
+			if (!empty($email) && !validateEmailAddress($email)) {
+				Minz_Request::bad(
+					_t('user.email.feedback.invalid'),
+					array('c' => 'auth', 'a' => 'register')
+				);
+			}
+
+			if ($tos_enabled && !$accept_tos) {
+				Minz_Request::bad(
+					_t('user.tos.feedback.invalid'),
+					array('c' => 'auth', 'a' => 'register')
+				);
+			}
+
+			$ok = self::createUser($new_user_name, $email, $passwordPlain, '', array('language' => $new_user_language));
 			Minz_Request::_param('new_user_passwordPlain');	//Discard plain-text password ASAP
 			Minz_Request::_param('new_user_passwordPlain');	//Discard plain-text password ASAP
 			$_POST['new_user_passwordPlain'] = '';
 			$_POST['new_user_passwordPlain'] = '';
 			invalidateHttpCache();
 			invalidateHttpCache();
@@ -266,9 +336,6 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	}
 	}
 
 
 	public static function deleteUser($username) {
 	public static function deleteUser($username) {
-		$db = FreshRSS_Context::$system_conf->db;
-		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
-
 		$ok = self::checkUsername($username);
 		$ok = self::checkUsername($username);
 		if ($ok) {
 		if ($ok) {
 			$default_user = FreshRSS_Context::$system_conf->default_user;
 			$default_user = FreshRSS_Context::$system_conf->default_user;
@@ -278,14 +345,130 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		$ok &= is_dir($user_data);
 		$ok &= is_dir($user_data);
 		if ($ok) {
 		if ($ok) {
 			self::deleteFeverKey($username);
 			self::deleteFeverKey($username);
-			$userDAO = new FreshRSS_UserDAO();
-			$ok &= $userDAO->deleteUser($username);
+			$oldUserDAO = FreshRSS_Factory::createUserDao($username);
+			$ok &= $oldUserDAO->deleteUser();
 			$ok &= recursive_unlink($user_data);
 			$ok &= recursive_unlink($user_data);
 			array_map('unlink', glob(PSHB_PATH . '/feeds/*/' . $username . '.txt'));
 			array_map('unlink', glob(PSHB_PATH . '/feeds/*/' . $username . '.txt'));
 		}
 		}
 		return $ok;
 		return $ok;
 	}
 	}
 
 
+	/**
+	 * This action validates an email address, based on the token sent by email.
+	 * It also serves the main page when user is blocked.
+	 *
+	 * Request parameters are:
+	 *   - username
+	 *   - token
+	 *
+	 * This route works with GET requests since the URL is provided by email.
+	 * The security risks (e.g. forged URL by an attacker) are not very high so
+	 * it's ok.
+	 *
+	 * It returns 404 error if `force_email_validation` is disabled or if the
+	 * user doesn't exist.
+	 *
+	 * It returns 403 if user isn't logged in and `username` param isn't passed.
+	 */
+	public function validateEmailAction() {
+		if (!FreshRSS_Context::$system_conf->force_email_validation) {
+			Minz_Error::error(404);
+		}
+
+		Minz_View::prependTitle(_t('user.email.validation.title') . ' · ');
+		$this->view->_layout('simple');
+
+		$username = Minz_Request::param('username');
+		$token = Minz_Request::param('token');
+
+		if ($username) {
+			$user_config = get_user_configuration($username);
+		} elseif (FreshRSS_Auth::hasAccess()) {
+			$user_config = FreshRSS_Context::$user_conf;
+		} else {
+			Minz_Error::error(403);
+		}
+
+		if (!FreshRSS_UserDAO::exists($username) || $user_config === null) {
+			Minz_Error::error(404);
+		}
+
+		if ($user_config->email_validation_token === '') {
+			Minz_Request::good(
+				_t('user.email.validation.feedback.unnecessary'),
+				array('c' => 'index', 'a' => 'index')
+			);
+		}
+
+		if ($token) {
+			if ($user_config->email_validation_token !== $token) {
+				Minz_Request::bad(
+					_t('user.email.validation.feedback.wrong_token'),
+					array('c' => 'user', 'a' => 'validateEmail')
+				);
+			}
+
+			$user_config->email_validation_token = '';
+			if ($user_config->save()) {
+				Minz_Request::good(
+					_t('user.email.validation.feedback.ok'),
+					array('c' => 'index', 'a' => 'index')
+				);
+			} else {
+				Minz_Request::bad(
+					_t('user.email.validation.feedback.error'),
+					array('c' => 'user', 'a' => 'validateEmail')
+				);
+			}
+		}
+	}
+
+	/**
+	 * This action resends a validation email to the current user.
+	 *
+	 * It only acts on POST requests but doesn't require any param (except the
+	 * CSRF token).
+	 *
+	 * It returns 403 error if the user is not logged in or 404 if request is
+	 * not POST. Else it redirects silently to the index if user has already
+	 * validated its email, or to the user#validateEmail route.
+	 */
+	public function sendValidationEmailAction() {
+		if (!FreshRSS_Auth::hasAccess()) {
+			Minz_Error::error(403);
+		}
+
+		if (!Minz_Request::isPost()) {
+			Minz_Error::error(404);
+		}
+
+		$username = Minz_Session::param('currentUser', '_');
+		$user_config = FreshRSS_Context::$user_conf;
+
+		if ($user_config->email_validation_token === '') {
+			Minz_Request::forward(array(
+				'c' => 'index',
+				'a' => 'index',
+			), true);
+		}
+
+		$mailer = new FreshRSS_User_Mailer();
+		$ok = $mailer->send_email_need_validation($username, $user_config);
+
+		$redirect_url = array('c' => 'user', 'a' => 'validateEmail');
+		if ($ok) {
+			Minz_Request::good(
+				_t('user.email.validation.feedback.email_sent'),
+				$redirect_url
+			);
+		} else {
+			Minz_Request::bad(
+				_t('user.email.validation.feedback.email_failed'),
+				$redirect_url
+			);
+		}
+	}
+
 	/**
 	/**
 	 * This action delete an existing user.
 	 * This action delete an existing user.
 	 *
 	 *
@@ -296,17 +479,18 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	 */
 	 */
 	public function deleteAction() {
 	public function deleteAction() {
 		$username = Minz_Request::param('username');
 		$username = Minz_Request::param('username');
+		$self_deletion = Minz_Session::param('currentUser', '_') === $username;
+
+		if (!FreshRSS_Auth::hasAccess('admin') && !$self_deletion) {
+			Minz_Error::error(403);
+		}
+
 		$redirect_url = urldecode(Minz_Request::param('r', false, true));
 		$redirect_url = urldecode(Minz_Request::param('r', false, true));
 		if (!$redirect_url) {
 		if (!$redirect_url) {
 			$redirect_url = array('c' => 'user', 'a' => 'manage');
 			$redirect_url = array('c' => 'user', 'a' => 'manage');
 		}
 		}
 
 
-		$self_deletion = Minz_Session::param('currentUser', '_') === $username;
-
-		if (Minz_Request::isPost() && (
-				FreshRSS_Auth::hasAccess('admin') ||
-				$self_deletion
-		)) {
+		if (Minz_Request::isPost()) {
 			$ok = true;
 			$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

+ 22 - 0
app/FreshRSS.php

@@ -53,6 +53,10 @@ class FreshRSS extends Minz_FrontController {
 			$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);
 		}
 		}
+
+		self::checkEmailValidated();
+
+		Minz_ExtensionManager::callHook('freshrss_init');
 	}
 	}
 
 
 	private static function initAuth() {
 	private static function initAuth() {
@@ -142,4 +146,22 @@ class FreshRSS extends Minz_FrontController {
 		FreshRSS_Share::load(join_path(APP_PATH, 'shares.php'));
 		FreshRSS_Share::load(join_path(APP_PATH, 'shares.php'));
 		self::loadStylesAndScripts();
 		self::loadStylesAndScripts();
 	}
 	}
+
+	private static function checkEmailValidated() {
+		$email_not_verified = FreshRSS_Auth::hasAccess() && FreshRSS_Context::$user_conf->email_validation_token !== '';
+		$action_is_allowed = (
+			Minz_Request::is('user', 'validateEmail') ||
+			Minz_Request::is('user', 'sendValidationEmail') ||
+			Minz_Request::is('user', 'profile') ||
+			Minz_Request::is('user', 'delete') ||
+			Minz_Request::is('auth', 'logout') ||
+			Minz_Request::is('javascript', 'nonce')
+		);
+		if ($email_not_verified && !$action_is_allowed) {
+			Minz_Request::forward(array(
+				'c' => 'user',
+				'a' => 'validateEmail',
+			), true);
+		}
+	}
 }
 }

+ 31 - 0
app/Mailers/UserMailer.php

@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * Manage the emails sent to the users.
+ */
+class FreshRSS_User_Mailer extends Minz_Mailer {
+	public function send_email_need_validation($username, $user_config) {
+		$this->view->_path('user_mailer/email_need_validation.txt');
+
+		$this->view->username = $username;
+		$this->view->site_title = FreshRSS_Context::$system_conf->title;
+		$this->view->validation_url = Minz_Url::display(
+			array(
+				'c' => 'user',
+				'a' => 'validateEmail',
+				'params' => array(
+					'username' => $username,
+					'token' => $user_config->email_validation_token
+				)
+			),
+			'txt',
+			true
+		);
+
+		$subject_prefix = '[' . FreshRSS_Context::$system_conf->title . ']';
+		return $this->mail(
+			$user_config->mail_login,
+			$subject_prefix . ' ' ._t('user.mailer.email_need_validation.title')
+		);
+	}
+}

+ 1 - 6
app/Models/Auth.php

@@ -219,10 +219,6 @@ class FreshRSS_FormAuth {
 			return false;
 			return false;
 		}
 		}
 
 
-		if (!function_exists('password_verify')) {
-			include_once(LIB_PATH . '/password_compat.php');
-		}
-
 		return password_verify($nonce . $hash, $challenge);
 		return password_verify($nonce . $hash, $challenge);
 	}
 	}
 
 
@@ -283,8 +279,7 @@ class FreshRSS_FormAuth {
 		$cookie_duration = empty($limits['cookie_duration']) ? 2592000 : $limits['cookie_duration'];
 		$cookie_duration = empty($limits['cookie_duration']) ? 2592000 : $limits['cookie_duration'];
 		$oldest = time() - $cookie_duration;
 		$oldest = time() - $cookie_duration;
 		foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $file_info) {
 		foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $file_info) {
-			// $extension = $file_info->getExtension(); doesn't work in PHP < 5.3.7
-			$extension = pathinfo($file_info->getFilename(), PATHINFO_EXTENSION);
+			$extension = $file_info->getExtension();
 			if ($extension === 'txt' && $file_info->getMTime() < $oldest) {
 			if ($extension === 'txt' && $file_info->getMTime() < $oldest) {
 				@unlink($file_info->getPathname());
 				@unlink($file_info->getPathname());
 			}
 			}

+ 29 - 2
app/Models/Category.php

@@ -8,6 +8,7 @@ class FreshRSS_Category extends Minz_Model {
 	private $feeds = null;
 	private $feeds = null;
 	private $hasFeedsWithError = false;
 	private $hasFeedsWithError = false;
 	private $isDefault = false;
 	private $isDefault = false;
+	private $attributes = [];
 
 
 	public function __construct($name = '', $feeds = null) {
 	public function __construct($name = '', $feeds = null) {
 		$this->_name($name);
 		$this->_name($name);
@@ -68,8 +69,19 @@ class FreshRSS_Category extends Minz_Model {
 		return $this->hasFeedsWithError;
 		return $this->hasFeedsWithError;
 	}
 	}
 
 
-	public function _id($value) {
-		$this->id = $value;
+	public function attributes($key = '') {
+		if ($key == '') {
+			return $this->attributes;
+		} else {
+			return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
+		}
+	}
+
+	public function _id($id) {
+		$this->id = $id;
+		if ($id == FreshRSS_CategoryDAO::DEFAULTCATEGORYID) {
+			$this->_name(_t('gen.short.default_category'));
+		}
 	}
 	}
 	public function _name($value) {
 	public function _name($value) {
 		$this->name = trim($value);
 		$this->name = trim($value);
@@ -84,4 +96,19 @@ class FreshRSS_Category extends Minz_Model {
 
 
 		$this->feeds = $values;
 		$this->feeds = $values;
 	}
 	}
+
+	public function _attributes($key, $value) {
+		if ('' == $key) {
+			if (is_string($value)) {
+				$value = json_decode($value, true);
+			}
+			if (is_array($value)) {
+				$this->attributes = $value;
+			}
+		} elseif (null === $value) {
+			unset($this->attributes[$key]);
+		} else {
+			$this->attributes[$key] = $value;
+		}
+	}
 }
 }

+ 186 - 65
app/Models/CategoryDAO.php

@@ -4,23 +4,92 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 
 
 	const DEFAULTCATEGORYID = 1;
 	const DEFAULTCATEGORYID = 1;
 
 
+	protected function addColumn($name) {
+		Minz_Log::warning(__method__ . ': ' . $name);
+		try {
+			if ('attributes' === $name) {	//v1.15.0
+				$ok = $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN attributes TEXT') !== false;
+
+				$stm = $this->pdo->query('SELECT * FROM `_feed`');
+				$feeds = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+				$stm = $this->pdo->prepare('UPDATE `_feed` SET attributes = :attributes WHERE id = :id');
+				foreach ($feeds as $feed) {
+					if (empty($feed['keep_history']) || empty($feed['id'])) {
+						continue;
+					}
+					$keepHistory = $feed['keep_history'];
+					$attributes = empty($feed['attributes']) ? [] : json_decode($feed['attributes'], true);
+					if (is_string($attributes)) {	//Legacy risk of double-encoding
+						$attributes = json_decode($attributes, true);
+					}
+					if (!is_array($attributes)) {
+						$attributes = [];
+					}
+					if ($keepHistory > 0) {
+						$attributes['archiving']['keep_min'] = intval($keepHistory);
+					} elseif ($keepHistory == -1) {	//Infinite
+						$attributes['archiving']['keep_period'] = false;
+						$attributes['archiving']['keep_max'] = false;
+						$attributes['archiving']['keep_min'] = false;
+					} else {
+						continue;
+					}
+					$stm->bindValue(':id', $feed['id'], PDO::PARAM_INT);
+					$stm->bindValue(':attributes', json_encode($attributes, JSON_UNESCAPED_SLASHES));
+					$stm->execute();
+				}
+
+				if ($this->pdo->dbType() !== 'sqlite') {	//SQLite does not support DROP COLUMN
+					$this->pdo->exec('ALTER TABLE `_feed` DROP COLUMN keep_history');
+				} else {
+					$this->pdo->exec('DROP INDEX IF EXISTS feed_keep_history_index');	//SQLite at least drop index
+				}
+				return $ok;
+			}
+		} catch (Exception $e) {
+			Minz_Log::error(__method__ . ': ' . $e->getMessage());
+		}
+		return false;
+	}
+
+	protected function autoUpdateDb($errorInfo) {
+		if (isset($errorInfo[0])) {
+			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
+				foreach (['attributes'] as $column) {
+					if (stripos($errorInfo[2], $column) !== false) {
+						return $this->addColumn($column);
+					}
+				}
+			}
+		}
+		return false;
+	}
+
 	public function addCategory($valuesTmp) {
 	public function addCategory($valuesTmp) {
-		$sql = 'INSERT INTO `' . $this->prefix . 'category`(name) '
-		     . 'SELECT * FROM (SELECT TRIM(?)) c2 '	//TRIM() to provide a type hint as text for PostgreSQL
-		     . 'WHERE NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'tag` WHERE name = TRIM(?))';	//No tag of the same name
-		$stm = $this->bd->prepare($sql);
+		$sql = 'INSERT INTO `_category`(name, attributes) '
+		     . 'SELECT * FROM (SELECT TRIM(?), ?) c2 '	//TRIM() to provide a type hint as text for PostgreSQL
+		     . 'WHERE NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = TRIM(?))';	//No tag of the same name
+		$stm = $this->pdo->prepare($sql);
 
 
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
+		if (!isset($valuesTmp['attributes'])) {
+			$valuesTmp['attributes'] = [];
+		}
 		$values = array(
 		$values = array(
 			$valuesTmp['name'],
 			$valuesTmp['name'],
+			is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
 			$valuesTmp['name'],
 			$valuesTmp['name'],
 		);
 		);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
-			return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"');
+			return $this->pdo->lastInsertId('`_category_id_seq`');
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::error('SQL error addCategory: ' . $info[2]);
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
+			if ($this->autoUpdateDb($info)) {
+				return $this->addCategory($valuesTmp);
+			}
+			Minz_Log::error('SQL error addCategory: ' . json_encode($info));
 			return false;
 			return false;
 		}
 		}
 	}
 	}
@@ -39,13 +108,17 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 	}
 	}
 
 
 	public function updateCategory($id, $valuesTmp) {
 	public function updateCategory($id, $valuesTmp) {
-		$sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=? '
-		     . 'AND NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'tag` WHERE name = ?)';	//No tag of the same name
-		$stm = $this->bd->prepare($sql);
+		$sql = 'UPDATE `_category` SET name=?, attributes=? WHERE id=? '
+		     . 'AND NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = ?)';	//No tag of the same name
+		$stm = $this->pdo->prepare($sql);
 
 
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
+		if (!isset($valuesTmp['attributes'])) {
+			$valuesTmp['attributes'] = [];
+		}
 		$values = array(
 		$values = array(
 			$valuesTmp['name'],
 			$valuesTmp['name'],
+			is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
 			$id,
 			$id,
 			$valuesTmp['name'],
 			$valuesTmp['name'],
 		);
 		);
@@ -53,8 +126,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::error('SQL error updateCategory: ' . $info[2]);
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
+			if ($this->autoUpdateDb($info)) {
+				return $this->updateCategory($valuesTmp);
+			}
+			Minz_Log::error('SQL error updateCategory: ' . json_encode($info));
 			return false;
 			return false;
 		}
 		}
 	}
 	}
@@ -63,27 +139,42 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		if ($id <= self::DEFAULTCATEGORYID) {
 		if ($id <= self::DEFAULTCATEGORYID) {
 			return false;
 			return false;
 		}
 		}
-		$sql = 'DELETE FROM `' . $this->prefix . 'category` WHERE id=?';
-		$stm = $this->bd->prepare($sql);
-
-		$values = array($id);
-
-		if ($stm && $stm->execute($values)) {
+		$sql = 'DELETE FROM `_category` WHERE id=:id';
+		$stm = $this->pdo->prepare($sql);
+		$stm->bindParam(':id', $id, PDO::PARAM_INT);
+		if ($stm && $stm->execute()) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::error('SQL error deleteCategory: ' . $info[2]);
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
+			Minz_Log::error('SQL error deleteCategory: ' . json_encode($info));
 			return false;
 			return false;
 		}
 		}
 	}
 	}
 
 
-	public function searchById($id) {
-		$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=?';
-		$stm = $this->bd->prepare($sql);
-
-		$values = array($id);
+	public function selectAll() {
+		$sql = 'SELECT id, name, attributes FROM `_category`';
+		$stm = $this->pdo->query($sql);
+		if ($stm != false) {
+			while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
+				yield $row;
+			}
+		} else {
+			$info = $this->pdo->errorInfo();
+			if ($this->autoUpdateDb($info)) {
+				foreach ($this->selectAll() as $category) {	// `yield from` requires PHP 7+
+					yield $category;
+				}
+			}
+			Minz_Log::error(__method__ . ' error: ' . json_encode($info));
+			yield false;
+		}
+	}
 
 
-		$stm->execute($values);
+	public function searchById($id) {
+		$sql = 'SELECT * FROM `_category` WHERE id=:id';
+		$stm = $this->pdo->prepare($sql);
+		$stm->bindParam(':id', $id, PDO::PARAM_INT);
+		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$cat = self::daoToCategory($res);
 		$cat = self::daoToCategory($res);
 
 
@@ -94,15 +185,15 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		}
 		}
 	}
 	}
 	public function searchByName($name) {
 	public function searchByName($name) {
-		$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE name=?';
-		$stm = $this->bd->prepare($sql);
-
-		$values = array($name);
-
-		$stm->execute($values);
+		$sql = 'SELECT * FROM `_category` WHERE name=:name';
+		$stm = $this->pdo->prepare($sql);
+		if ($stm == false) {
+			return false;
+		}
+		$stm->bindParam(':name', $name);
+		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$cat = self::daoToCategory($res);
 		$cat = self::daoToCategory($res);
-
 		if (isset($cat[0])) {
 		if (isset($cat[0])) {
 			return $cat[0];
 			return $cat[0];
 		} else {
 		} else {
@@ -110,30 +201,61 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		}
 		}
 	}
 	}
 
 
+	public function listSortedCategories($prePopulateFeeds = true, $details = false) {
+		$categories = $this->listCategories($prePopulateFeeds, $details);
+
+		if (!is_array($categories)) {
+			return $categories;
+		}
+
+		uasort($categories, function ($a, $b) {
+			$aPosition = $a->attributes('position');
+			$bPosition = $b->attributes('position');
+			if ($aPosition === $bPosition) {
+				return ($a->name() < $b->name()) ? -1 : 1;
+			} elseif (null === $aPosition) {
+				return 1;
+			} elseif (null === $bPosition) {
+				return -1;
+			}
+			return ($aPosition < $bPosition) ? -1 : 1;
+		});
+
+		return $categories;
+	}
+
 	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, c.attributes AS c_attributes, '
 			     . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads`, f.ttl ')
 			     . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads`, f.ttl ')
-			     . 'FROM `' . $this->prefix . 'category` c '
-			     . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id '
+			     . 'FROM `_category` c '
+			     . 'LEFT OUTER JOIN `_feed` f ON f.category=c.id '
 			     . 'WHERE f.priority >= :priority_normal '
 			     . 'WHERE f.priority >= :priority_normal '
 			     . 'GROUP BY f.id, c_id '
 			     . 'GROUP BY f.id, c_id '
 			     . 'ORDER BY c.name, f.name';
 			     . 'ORDER BY c.name, f.name';
-			$stm = $this->bd->prepare($sql);
-			$stm->execute(array(':priority_normal' => FreshRSS_Feed::PRIORITY_NORMAL));
-			return self::daoToCategoryPrepopulated($stm->fetchAll(PDO::FETCH_ASSOC));
+			$stm = $this->pdo->prepare($sql);
+			$values = [ ':priority_normal' => FreshRSS_Feed::PRIORITY_NORMAL ];
+			if ($stm && $stm->execute($values)) {
+				return self::daoToCategoryPrepopulated($stm->fetchAll(PDO::FETCH_ASSOC));
+			} else {
+				$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
+				if ($this->autoUpdateDb($info)) {
+					return $this->listCategories($prePopulateFeeds, $details);
+				}
+				Minz_Log::error('SQL error listCategories: ' . json_encode($info));
+				return false;
+			}
 		} else {
 		} else {
-			$sql = 'SELECT * FROM `' . $this->prefix . 'category` ORDER BY name';
-			$stm = $this->bd->prepare($sql);
-			$stm->execute();
+			$sql = 'SELECT * FROM `_category` ORDER BY name';
+			$stm = $this->pdo->query($sql);
 			return self::daoToCategory($stm->fetchAll(PDO::FETCH_ASSOC));
 			return self::daoToCategory($stm->fetchAll(PDO::FETCH_ASSOC));
 		}
 		}
 	}
 	}
 
 
 	public function getDefault() {
 	public function getDefault() {
-		$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=' . self::DEFAULTCATEGORYID;
-		$stm = $this->bd->prepare($sql);
-
+		$sql = 'SELECT * FROM `_category` WHERE id=:id';
+		$stm = $this->pdo->prepare($sql);
+		$stm->bindValue(':id', self::DEFAULTCATEGORYID, PDO::PARAM_INT);
 		$stm->execute();
 		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$cat = self::daoToCategory($res);
 		$cat = self::daoToCategory($res);
@@ -155,12 +277,12 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 			$cat = new FreshRSS_Category(_t('gen.short.default_category'));
 			$cat = new FreshRSS_Category(_t('gen.short.default_category'));
 			$cat->_id(self::DEFAULTCATEGORYID);
 			$cat->_id(self::DEFAULTCATEGORYID);
 
 
-			$sql = 'INSERT INTO `' . $this->prefix . 'category`(id, name) VALUES(?, ?)';
-			if (parent::$sharedDbType === 'pgsql') {
+			$sql = 'INSERT INTO `_category`(id, name) VALUES(?, ?)';
+			if ($this->pdo->dbType() === 'pgsql') {
 				//Force call to nextval()
 				//Force call to nextval()
-				$sql .= ' RETURNING nextval(\'"' . $this->prefix . 'category_id_seq"\');';
+				$sql .= " RETURNING nextval('`_category_id_seq`');";
 			}
 			}
-			$stm = $this->bd->prepare($sql);
+			$stm = $this->pdo->prepare($sql);
 
 
 			$values = array(
 			$values = array(
 				$cat->id(),
 				$cat->id(),
@@ -168,9 +290,9 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 			);
 			);
 
 
 			if ($stm && $stm->execute($values)) {
 			if ($stm && $stm->execute($values)) {
-				return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"');
+				return $this->pdo->lastInsertId('`_category_id_seq`');
 			} else {
 			} else {
-				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 				Minz_Log::error('SQL error check default category: ' . json_encode($info));
 				Minz_Log::error('SQL error check default category: ' . json_encode($info));
 				return false;
 				return false;
 			}
 			}
@@ -179,31 +301,27 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 	}
 	}
 
 
 	public function count() {
 	public function count() {
-		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'category`';
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$sql = 'SELECT COUNT(*) AS count FROM `_category`';
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-
 		return $res[0]['count'];
 		return $res[0]['count'];
 	}
 	}
 
 
 	public function countFeed($id) {
 	public function countFeed($id) {
-		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'feed` WHERE category=?';
-		$stm = $this->bd->prepare($sql);
-		$values = array($id);
-		$stm->execute($values);
+		$sql = 'SELECT COUNT(*) AS count FROM `_feed` WHERE category=:id';
+		$stm = $this->pdo->prepare($sql);
+		$stm->bindParam(':id', $id, PDO::PARAM_INT);
+		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-
 		return $res[0]['count'];
 		return $res[0]['count'];
 	}
 	}
 
 
 	public function countNotRead($id) {
 	public function countNotRead($id) {
-		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE category=? AND e.is_read=0';
-		$stm = $this->bd->prepare($sql);
-		$values = array($id);
-		$stm->execute($values);
+		$sql = 'SELECT COUNT(*) AS count FROM `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id WHERE category=:id AND e.is_read=0';
+		$stm = $this->pdo->prepare($sql);
+		$stm->bindParam(':id', $id, PDO::PARAM_INT);
+		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-
 		return $res[0]['count'];
 		return $res[0]['count'];
 	}
 	}
 
 
@@ -248,6 +366,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 					$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
 					$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
 				);
 				);
 				$cat->_id($previousLine['c_id']);
 				$cat->_id($previousLine['c_id']);
+				$cat->_attributes('', $previousLine['c_attributes']);
 				$list[$previousLine['c_id']] = $cat;
 				$list[$previousLine['c_id']] = $cat;
 
 
 				$feedsDao = array();	//Prepare for next category
 				$feedsDao = array();	//Prepare for next category
@@ -264,6 +383,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 				$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
 				$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
 			);
 			);
 			$cat->_id($previousLine['c_id']);
 			$cat->_id($previousLine['c_id']);
+			$cat->_attributes('', $previousLine['c_attributes']);
 			$list[$previousLine['c_id']] = $cat;
 			$list[$previousLine['c_id']] = $cat;
 		}
 		}
 
 
@@ -282,6 +402,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 				$dao['name']
 				$dao['name']
 			);
 			);
 			$cat->_id($dao['id']);
 			$cat->_id($dao['id']);
+			$cat->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : '');
 			$cat->_isDefault(static::DEFAULTCATEGORYID === intval($dao['id']));
 			$cat->_isDefault(static::DEFAULTCATEGORYID === intval($dao['id']));
 			$list[$key] = $cat;
 			$list[$key] = $cat;
 		}
 		}

+ 17 - 0
app/Models/CategoryDAOSQLite.php

@@ -0,0 +1,17 @@
+<?php
+
+class FreshRSS_CategoryDAOSQLite extends FreshRSS_CategoryDAO {
+
+	protected function autoUpdateDb($errorInfo) {
+		if ($tableInfo = $this->pdo->query("PRAGMA table_info('category')")) {
+			$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
+			foreach (['attributes'] as $column) {
+				if (!in_array($column, $columns)) {
+					return $this->addColumn($column);
+				}
+			}
+		}
+		return false;
+	}
+
+}

+ 7 - 10
app/Models/ConfigurationSetter.php

@@ -79,11 +79,6 @@ class FreshRSS_ConfigurationSetter {
 		$data['html5_notif_timeout'] = $value >= 0 ? $value : 0;
 		$data['html5_notif_timeout'] = $value >= 0 ? $value : 0;
 	}
 	}
 
 
-	private function _keep_history_default(&$data, $value) {
-		$value = intval($value);
-		$data['keep_history_default'] = $value >= FreshRSS_Feed::KEEP_HISTORY_INFINITE ? $value : 0;
-	}
-
 	// It works for system config too!
 	// It works for system config too!
 	private function _language(&$data, $value) {
 	private function _language(&$data, $value) {
 		$value = strtolower($value);
 		$value = strtolower($value);
@@ -94,11 +89,6 @@ class FreshRSS_ConfigurationSetter {
 		$data['language'] = $value;
 		$data['language'] = $value;
 	}
 	}
 
 
-	private function _old_entries(&$data, $value) {
-		$value = intval($value);
-		$data['old_entries'] = $value > 0 ? $value : 3;
-	}
-
 	private function _passwordHash(&$data, $value) {
 	private function _passwordHash(&$data, $value) {
 		$data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
 		$data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
 	}
 	}
@@ -257,6 +247,9 @@ class FreshRSS_ConfigurationSetter {
 	private function _topline_read(&$data, $value) {
 	private function _topline_read(&$data, $value) {
 		$data['topline_read'] = $this->handleBool($value);
 		$data['topline_read'] = $this->handleBool($value);
 	}
 	}
+	private function _topline_display_authors(&$data, $value) {
+		$data['topline_display_authors'] = $this->handleBool($value);
+	}
 
 
 	/**
 	/**
 	 * The (not so long) list of setters for system configuration.
 	 * The (not so long) list of setters for system configuration.
@@ -386,4 +379,8 @@ class FreshRSS_ConfigurationSetter {
 
 
 		$data['auto_update_url'] = $value;
 		$data['auto_update_url'] = $value;
 	}
 	}
+
+	private function _force_email_validation(&$data, $value) {
+		$data['force_email_validation'] = $this->handleBool($value);
+	}
 }
 }

+ 18 - 0
app/Models/Context.php

@@ -51,6 +51,24 @@ 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');
+
+		//Legacy
+		$oldEntries = (int)FreshRSS_Context::$user_conf->param('old_entries', 0);
+		$keepMin = (int)FreshRSS_Context::$user_conf->param('keep_history_default', -5);
+		if ($oldEntries > 0 || $keepMin > -5) {	//Freshrss < 1.15
+			$archiving = FreshRSS_Context::$user_conf->archiving;
+			$archiving['keep_max'] = false;
+			if ($oldEntries > 0) {
+				$archiving['keep_period'] = 'P' . $oldEntries . 'M';
+			}
+			if ($keepMin > 0) {
+				$archiving['keep_min'] = $keepMin;
+			} elseif ($keepMin == -1) {	//Infinite
+				$archiving['keep_period'] = false;
+				$archiving['keep_min'] = false;
+			}
+			FreshRSS_Context::$user_conf->archiving = $archiving;
+		}
 	}
 	}
 
 
 	/**
 	/**

+ 215 - 35
app/Models/DatabaseDAO.php

@@ -8,25 +8,50 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 	//MySQL error codes
 	//MySQL error codes
 	const ER_BAD_FIELD_ERROR = '42S22';
 	const ER_BAD_FIELD_ERROR = '42S22';
 	const ER_BAD_TABLE_ERROR = '42S02';
 	const ER_BAD_TABLE_ERROR = '42S02';
-	const ER_TRUNCATED_WRONG_VALUE_FOR_FIELD = '1366';
+	const ER_DATA_TOO_LONG = '1406';
 
 
 	//MySQL InnoDB maximum index length for UTF8MB4
 	//MySQL InnoDB maximum index length for UTF8MB4
 	//https://dev.mysql.com/doc/refman/8.0/en/innodb-restrictions.html
 	//https://dev.mysql.com/doc/refman/8.0/en/innodb-restrictions.html
 	const LENGTH_INDEX_UNICODE = 191;
 	const LENGTH_INDEX_UNICODE = 191;
 
 
+	public function create() {
+		require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
+		$db = FreshRSS_Context::$system_conf->db;
+
+		try {
+			$sql = sprintf($SQL_CREATE_DB, empty($db['base']) ? '' : $db['base']);
+			return $this->pdo->exec($sql) !== false;
+		} catch (PDOException $e) {
+			$_SESSION['bd_error'] = $e->getMessage();
+			syslog(LOG_DEBUG, __method__ . ' warning: ' . $e->getMessage());
+			return false;
+		}
+	}
+
+	public function testConnection() {
+		try {
+			$sql = 'SELECT 1';
+			$stm = $this->pdo->query($sql);
+			$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+			return $res != false;
+		} catch (PDOException $e) {
+			$_SESSION['bd_error'] = $e->getMessage();
+			syslog(LOG_DEBUG, __method__ . ' warning: ' . $e->getMessage());
+			return false;
+		}
+	}
+
 	public function tablesAreCorrect() {
 	public function tablesAreCorrect() {
-		$sql = 'SHOW TABLES';
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query('SHOW TABLES');
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 
 		$tables = array(
 		$tables = array(
-			$this->prefix . 'category' => false,
-			$this->prefix . 'feed' => false,
-			$this->prefix . 'entry' => false,
-			$this->prefix . 'entrytmp' => false,
-			$this->prefix . 'tag' => false,
-			$this->prefix . 'entrytag' => false,
+			$this->pdo->prefix() . 'category' => false,
+			$this->pdo->prefix() . 'feed' => false,
+			$this->pdo->prefix() . 'entry' => false,
+			$this->pdo->prefix() . 'entrytmp' => false,
+			$this->pdo->prefix() . 'tag' => false,
+			$this->pdo->prefix() . 'entrytag' => false,
 		);
 		);
 		foreach ($res as $value) {
 		foreach ($res as $value) {
 			$tables[array_pop($value)] = true;
 			$tables[array_pop($value)] = true;
@@ -36,10 +61,8 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 	}
 	}
 
 
 	public function getSchema($table) {
 	public function getSchema($table) {
-		$sql = 'DESC ' . $this->prefix . $table;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
-
+		$sql = 'DESC `_' . $table . '`';
+		$stm = $this->pdo->query($sql);
 		return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
 		return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 	}
 
 
@@ -63,7 +86,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 	public function feedIsCorrect() {
 	public function feedIsCorrect() {
 		return $this->checkTable('feed', array(
 		return $this->checkTable('feed', array(
 			'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate',
 			'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate',
-			'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', 'attributes',
+			'priority', 'pathEntries', 'httpAuth', 'error', 'ttl', 'attributes',
 			'cache_nbEntries', 'cache_nbUnreads',
 			'cache_nbEntries', 'cache_nbUnreads',
 		));
 		));
 	}
 	}
@@ -119,9 +142,9 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 		$values = array($db['base']);
 		$values = array($db['base']);
 		if (!$all) {
 		if (!$all) {
 			$sql .= ' AND table_name LIKE ?';
 			$sql .= ' AND table_name LIKE ?';
-			$values[] = $this->prefix . '%';
+			$values[] = $this->pdo->prefix() . '%';
 		}
 		}
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 		$stm->execute($values);
 		$stm->execute($values);
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		return $res[0];
 		return $res[0];
@@ -132,30 +155,23 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 		$tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag');
 		$tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag');
 
 
 		foreach ($tables as $table) {
 		foreach ($tables as $table) {
-			$sql = 'OPTIMIZE TABLE `' . $this->prefix . $table . '`';	//MySQL
-			$stm = $this->bd->prepare($sql);
-			$ok &= $stm != false;
-			if ($stm) {
-				$ok &= $stm->execute();
-			}
+			$sql = 'OPTIMIZE TABLE `_' . $table . '`';	//MySQL
+			$ok &= ($this->pdo->exec($sql) !== false);
 		}
 		}
 		return $ok;
 		return $ok;
 	}
 	}
 
 
 	public function ensureCaseInsensitiveGuids() {
 	public function ensureCaseInsensitiveGuids() {
 		$ok = true;
 		$ok = true;
-		$db = FreshRSS_Context::$system_conf->db;
-		if ($db['type'] === 'mysql') {
-			include_once(APP_PATH . '/SQL/install.sql.mysql.php');
-			if (defined('SQL_UPDATE_GUID_LATIN1_BIN')) {	//FreshRSS 1.12
-				try {
-					$sql = sprintf(SQL_UPDATE_GUID_LATIN1_BIN, $this->prefix);
-					$stm = $this->bd->prepare($sql);
-					$ok = $stm->execute();
-				} catch (Exception $e) {
-					$ok = false;
-					Minz_Log::error('FreshRSS_DatabaseDAO::ensureCaseInsensitiveGuids error: ' . $e->getMessage());
-				}
+		if ($this->pdo->dbType() === 'mysql') {
+			include(APP_PATH . '/SQL/install.sql.mysql.php');
+
+			$ok = false;
+			try {
+				$ok = $this->pdo->exec($SQL_UPDATE_GUID_LATIN1_BIN) !== false;	//FreshRSS 1.12
+			} catch (Exception $e) {
+				$ok = false;
+				Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage());
 			}
 			}
 		}
 		}
 		return $ok;
 		return $ok;
@@ -164,4 +180,168 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 	public function minorDbMaintenance() {
 	public function minorDbMaintenance() {
 		$this->ensureCaseInsensitiveGuids();
 		$this->ensureCaseInsensitiveGuids();
 	}
 	}
+
+	private static function stdError($error) {
+		if (defined('STDERR')) {
+			fwrite(STDERR, $error . "\n");
+		}
+		Minz_Log::error($error);
+		return false;
+	}
+
+	const SQLITE_EXPORT = 1;
+	const SQLITE_IMPORT = 2;
+
+	public function dbCopy($filename, $mode, $clearFirst = false) {
+		$error = '';
+
+		$userDAO = FreshRSS_Factory::createUserDao();
+		$catDAO = FreshRSS_Factory::createCategoryDao();
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$entryDAO = FreshRSS_Factory::createEntryDao();
+		$tagDAO = FreshRSS_Factory::createTagDao();
+
+		switch ($mode) {
+			case self::SQLITE_EXPORT:
+				if (@filesize($filename) > 0) {
+					$error = 'Error: SQLite export file already exists: ' . $filename;
+				}
+				break;
+			case self::SQLITE_IMPORT:
+				if (!is_readable($filename)) {
+					$error = 'Error: SQLite import file is not readable: ' . $filename;
+				} elseif ($clearFirst) {
+					$userDAO->deleteUser();
+					if ($this->pdo->dbType() === 'sqlite') {
+						//We cannot just delete the .sqlite file otherwise PDO gets buggy.
+						//SQLite is the only one with database-level optimization, instead of at table level.
+						$this->optimize();
+					}
+				} else {
+					$nbEntries = $entryDAO->countUnreadRead();
+					if (!empty($nbEntries['all'])) {
+						$error = 'Error: Destination database already contains some entries!';
+					}
+				}
+				break;
+			default:
+				$error = 'Invalid copy mode!';
+				break;
+		}
+		if ($error != '') {
+			return self::stdError($error);
+		}
+
+		$sqlite = null;
+
+		try {
+			$sqlite = new MinzPDOSQLite('sqlite:' . $filename);
+		} catch (Exception $e) {
+			$error = 'Error while initialising SQLite copy: ' . $e->getMessage();
+			return self::stdError($error);
+		}
+
+		Minz_ModelPdo::clean();
+		$userDAOSQLite = new FreshRSS_UserDAO('', $sqlite);
+		$categoryDAOSQLite = new FreshRSS_CategoryDAOSQLite('', $sqlite);
+		$feedDAOSQLite = new FreshRSS_FeedDAOSQLite('', $sqlite);
+		$entryDAOSQLite = new FreshRSS_EntryDAOSQLite('', $sqlite);
+		$tagDAOSQLite = new FreshRSS_TagDAOSQLite('', $sqlite);
+
+		switch ($mode) {
+			case self::SQLITE_EXPORT:
+				$userFrom = $userDAO; $userTo = $userDAOSQLite;
+				$catFrom = $catDAO; $catTo = $categoryDAOSQLite;
+				$feedFrom = $feedDAO; $feedTo = $feedDAOSQLite;
+				$entryFrom = $entryDAO; $entryTo = $entryDAOSQLite;
+				$tagFrom = $tagDAO; $tagTo = $tagDAOSQLite;
+				break;
+			case self::SQLITE_IMPORT:
+				$userFrom = $userDAOSQLite; $userTo = $userDAO;
+				$catFrom = $categoryDAOSQLite; $catTo = $catDAO;
+				$feedFrom = $feedDAOSQLite; $feedTo = $feedDAO;
+				$entryFrom = $entryDAOSQLite; $entryTo = $entryDAO;
+				$tagFrom = $tagDAOSQLite; $tagTo = $tagDAO;
+				break;
+		}
+
+		$idMaps = [];
+
+		if (defined('STDERR')) {
+			fwrite(STDERR, "Start SQL copy…\n");
+		}
+
+		$userTo->createUser();
+
+		$catTo->beginTransaction();
+		foreach ($catFrom->selectAll() as $category) {
+			$cat = $catTo->searchByName($category['name']);	//Useful for the default category
+			if ($cat != null) {
+				$catId = $cat->id();
+			} else {
+				$catId = $catTo->addCategory($category);
+				if ($catId == false) {
+					$error = 'Error during SQLite copy of categories!';
+					return self::stdError($error);
+				}
+			}
+			$idMaps['c' . $category['id']] = $catId;
+		}
+		foreach ($feedFrom->selectAll() as $feed) {
+			$feed['category'] = empty($idMaps['c' . $feed['category']]) ? FreshRSS_CategoryDAO::DEFAULTCATEGORYID : $idMaps['c' . $feed['category']];
+			$feedId = $feedTo->addFeed($feed);
+			if ($feedId == false) {
+				$error = 'Error during SQLite copy of feeds!';
+				return self::stdError($error);
+			}
+			$idMaps['f' . $feed['id']] = $feedId;
+		}
+		$catTo->commit();
+
+		$nbEntries = $entryFrom->count();
+		$n = 0;
+		$entryTo->beginTransaction();
+		foreach ($entryFrom->selectAll() as $entry) {
+			$n++;
+			if (!empty($idMaps['f' . $entry['id_feed']])) {
+				$entry['id_feed'] = $idMaps['f' . $entry['id_feed']];
+				if (!$entryTo->addEntry($entry, false)) {
+					$error = 'Error during SQLite copy of entries!';
+					return self::stdError($error);
+				}
+			}
+			if ($n % 100 === 1 && defined('STDERR')) {	//Display progression
+				fwrite(STDERR, "\033[0G" . $n . '/' . $nbEntries);
+			}
+		}
+		if (defined('STDERR')) {
+			fwrite(STDERR, "\033[0G" . $n . '/' . $nbEntries . "\n");
+		}
+		$entryTo->commit();
+		$feedTo->updateCachedValues();
+
+		$idMaps = [];
+
+		$tagTo->beginTransaction();
+		foreach ($tagFrom->selectAll() as $tag) {
+			$tagId = $tagTo->addTag($tag);
+			if ($tagId == false) {
+				$error = 'Error during SQLite copy of tags!';
+				return self::stdError($error);
+			}
+			$idMaps['t' . $tag['id']] = $tagId;
+		}
+		foreach ($tagFrom->selectEntryTag() as $entryTag) {
+			if (!empty($idMaps['t' . $entryTag['id_tag']])) {
+				$entryTag['id_tag'] = $idMaps['t' . $entryTag['id_tag']];
+				if (!$tagTo->tagEntry($entryTag['id_tag'], $entryTag['id_entry'])) {
+					$error = 'Error during SQLite copy of entry-tags!';
+					return self::stdError($error);
+				}
+			}
+		}
+		$tagTo->commit();
+
+		return true;
+	}
 }
 }

+ 28 - 21
app/Models/DatabaseDAOPGSQL.php

@@ -13,18 +13,18 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
 		$db = FreshRSS_Context::$system_conf->db;
 		$db = FreshRSS_Context::$system_conf->db;
 		$dbowner = $db['user'];
 		$dbowner = $db['user'];
 		$sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=?';
 		$sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=?';
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 		$values = array($dbowner);
 		$values = array($dbowner);
 		$stm->execute($values);
 		$stm->execute($values);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 
 		$tables = array(
 		$tables = array(
-			$this->prefix . 'category' => false,
-			$this->prefix . 'feed' => false,
-			$this->prefix . 'entry' => false,
-			$this->prefix . 'entrytmp' => false,
-			$this->prefix . 'tag' => false,
-			$this->prefix . 'entrytag' => false,
+			$this->pdo->prefix() . 'category' => false,
+			$this->pdo->prefix() . 'feed' => false,
+			$this->pdo->prefix() . 'entry' => false,
+			$this->pdo->prefix() . 'entrytmp' => false,
+			$this->pdo->prefix() . 'tag' => false,
+			$this->pdo->prefix() . 'entrytag' => false,
 		);
 		);
 		foreach ($res as $value) {
 		foreach ($res as $value) {
 			$tables[array_pop($value)] = true;
 			$tables[array_pop($value)] = true;
@@ -35,8 +35,8 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
 
 
 	public function getSchema($table) {
 	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 = ?';
 		$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));
+		$stm = $this->pdo->prepare($sql);
+		$stm->execute(array($this->pdo->prefix() . $table));
 		return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
 		return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 	}
 
 
@@ -49,12 +49,23 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
 		);
 		);
 	}
 	}
 
 
-	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);
+	public function size($all = false) {
+		if ($all) {
+			$db = FreshRSS_Context::$system_conf->db;
+			$sql = 'SELECT pg_database_size(:base)';
+			$stm = $this->pdo->prepare($sql);
+			$stm->bindParam(':base', $db['base']);
+			$stm->execute();
+		} else {
+			$sql = "SELECT "
+			     . "pg_total_relation_size('{$this->pdo->prefix()}category') + "
+			     . "pg_total_relation_size('{$this->pdo->prefix()}feed') + "
+			     . "pg_total_relation_size('{$this->pdo->prefix()}entry') + "
+			     . "pg_total_relation_size('{$this->pdo->prefix()}entrytmp') + "
+			     . "pg_total_relation_size('{$this->pdo->prefix()}tag') + "
+			     . "pg_total_relation_size('{$this->pdo->prefix()}entrytag')";
+			$stm = $this->pdo->query($sql);
+		}
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		return $res[0];
 		return $res[0];
 	}
 	}
@@ -64,12 +75,8 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
 		$tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag');
 		$tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag');
 
 
 		foreach ($tables as $table) {
 		foreach ($tables as $table) {
-			$sql = 'VACUUM `' . $this->prefix . $table . '`';
-			$stm = $this->bd->prepare($sql);
-			$ok &= $stm != false;
-			if ($stm) {
-				$ok &= $stm->execute();
-			}
+			$sql = 'VACUUM `_' . $table . '`';
+			$ok &= ($this->pdo->exec($sql) !== false);
 		}
 		}
 		return $ok;
 		return $ok;
 	}
 	}

+ 18 - 18
app/Models/DatabaseDAOSQLite.php

@@ -6,17 +6,16 @@
 class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 	public function tablesAreCorrect() {
 	public function tablesAreCorrect() {
 		$sql = 'SELECT name FROM sqlite_master WHERE type="table"';
 		$sql = 'SELECT name FROM sqlite_master WHERE type="table"';
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 
 		$tables = array(
 		$tables = array(
-			'category' => false,
-			'feed' => false,
-			'entry' => false,
-			'entrytmp' => false,
-			'tag' => false,
-			'entrytag' => false,
+			$this->pdo->prefix() . 'category' => false,
+			$this->pdo->prefix() . 'feed' => false,
+			$this->pdo->prefix() . 'entry' => false,
+			$this->pdo->prefix() . 'entrytmp' => false,
+			$this->pdo->prefix() . 'tag' => false,
+			$this->pdo->prefix() . 'entrytag' => false,
 		);
 		);
 		foreach ($res as $value) {
 		foreach ($res as $value) {
 			$tables[$value['name']] = true;
 			$tables[$value['name']] = true;
@@ -27,9 +26,7 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 
 
 	public function getSchema($table) {
 	public function getSchema($table) {
 		$sql = 'PRAGMA table_info(' . $table . ')';
 		$sql = 'PRAGMA table_info(' . $table . ')';
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
-
+		$stm = $this->pdo->query($sql);
 		return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
 		return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 	}
 
 
@@ -57,15 +54,18 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 	}
 	}
 
 
 	public function size($all = false) {
 	public function size($all = false) {
-		return @filesize(join_path(DATA_PATH, 'users', $this->current_user, 'db.sqlite'));
+		$sum = 0;
+		if ($all) {
+			foreach (glob(DATA_PATH . '/users/*/db.sqlite') as $filename) {
+				$sum += @filesize($filename);
+			}
+		} else {
+			$sum = @filesize(DATA_PATH . '/users/' . $this->current_user . '/db.sqlite');
+		}
+		return $sum;
 	}
 	}
 
 
 	public function optimize() {
 	public function optimize() {
-		$sql = 'VACUUM';
-		$stm = $this->bd->prepare($sql);
-		if ($stm) {
-			return $stm->execute();
-		}
-		return false;
+		return $this->pdo->exec('VACUUM') !== false;
 	}
 	}
 }
 }

+ 4 - 8
app/Models/Entry.php

@@ -327,7 +327,7 @@ class FreshRSS_Entry extends Minz_Model {
 		}
 		}
 
 
 		$ch = curl_init();
 		$ch = curl_init();
-		curl_setopt_array($ch, array(
+		curl_setopt_array($ch, [
 			CURLOPT_URL => $url,
 			CURLOPT_URL => $url,
 			CURLOPT_REFERER => SimplePie_Misc::url_remove_credentials($url),
 			CURLOPT_REFERER => SimplePie_Misc::url_remove_credentials($url),
 			CURLOPT_HTTPHEADER => array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
 			CURLOPT_HTTPHEADER => array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
@@ -337,13 +337,9 @@ class FreshRSS_Entry extends Minz_Model {
 			//CURLOPT_FAILONERROR => true;
 			//CURLOPT_FAILONERROR => true;
 			CURLOPT_MAXREDIRS => 4,
 			CURLOPT_MAXREDIRS => 4,
 			CURLOPT_RETURNTRANSFER => true,
 			CURLOPT_RETURNTRANSFER => true,
-		));
-		if (version_compare(PHP_VERSION, '5.6.0') >= 0 || ini_get('open_basedir') == '') {
-			curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);	//Keep option separated for open_basedir PHP bug 65646
-		}
-		if (defined('CURLOPT_ENCODING')) {
-			curl_setopt($ch, CURLOPT_ENCODING, '');	//Enable all encodings
-		}
+			CURLOPT_FOLLOWLOCATION => true,
+			CURLOPT_ENCODING => '',	//Enable all encodings
+		]);
 		curl_setopt_array($ch, $system_conf->curl_options);
 		curl_setopt_array($ch, $system_conf->curl_options);
 		if (isset($attributes['ssl_verify'])) {
 		if (isset($attributes['ssl_verify'])) {
 			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $attributes['ssl_verify'] ? 2 : 0);
 			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $attributes['ssl_verify'] ? 2 : 0);

+ 222 - 245
app/Models/EntryDAO.php

@@ -3,11 +3,11 @@
 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 === 'mysql';
+		return true;
 	}
 	}
 
 
 	public function hasNativeHex() {
 	public function hasNativeHex() {
-		return parent::$sharedDbType !== 'sqlite';
+		return true;
 	}
 	}
 
 
 	public function sqlHexDecode($x) {
 	public function sqlHexDecode($x) {
@@ -19,106 +19,40 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	//TODO: Move the database auto-updates to DatabaseDAO
 	//TODO: Move the database auto-updates to DatabaseDAO
-	protected function addColumn($name) {
-		Minz_Log::warning('FreshRSS_EntryDAO::addColumn: ' . $name);
-		$hasTransaction = false;
+	protected function createEntryTempTable() {
+		$ok = false;
+		$hadTransaction = $this->pdo->inTransaction();
+		if ($hadTransaction) {
+			$this->pdo->commit();
+		}
 		try {
 		try {
-			$stm = null;
-			if ($name === 'lastSeen') {	//v1.1.1
-				if (!$this->bd->inTransaction()) {
-					$this->bd->beginTransaction();
-					$hasTransaction = true;
-				}
-				$stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN `lastSeen` INT(11) DEFAULT 0');
-				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
-					if ($stm && $stm->execute()) {
-						if ($hasTransaction) {
-							$this->bd->commit();
-						}
-						return true;
-					}
-				}
-				if ($hasTransaction) {
-					$this->bd->rollBack();
-				}
-			} elseif ($name === 'hash') {	//v1.1.1
-				$stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN hash BINARY(16)');
-				return $stm && $stm->execute();
-			}
-		} catch (Exception $e) {
-			Minz_Log::error('FreshRSS_EntryDAO::addColumn error: ' . $e->getMessage());
-			if ($hasTransaction) {
-				$this->bd->rollBack();
-			}
+			require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
+			Minz_Log::warning('SQL CREATE TABLE entrytmp...');
+			$ok = $this->pdo->exec($SQL_CREATE_TABLE_ENTRYTMP . $SQL_CREATE_INDEX_ENTRY_1) !== false;
+		} catch (Exception $ex) {
+			Minz_Log::error(__method__ . ' error: ' . $ex->getMessage());
 		}
 		}
-		return false;
+		if ($hadTransaction) {
+			$this->pdo->beginTransaction();
+		}
+		return $ok;
 	}
 	}
 
 
-	private $triedUpdateToUtf8mb4 = false;
-
-	//TODO: Move the database auto-updates to DatabaseDAO
-	protected function updateToUtf8mb4() {
-		if ($this->triedUpdateToUtf8mb4) {
+	private function updateToMediumBlob() {
+		if ($this->pdo->dbType() !== 'mysql') {
 			return false;
 			return false;
 		}
 		}
-		$this->triedUpdateToUtf8mb4 = true;
-		$db = FreshRSS_Context::$system_conf->db;
-		if ($db['type'] === 'mysql') {
-			include_once(APP_PATH . '/SQL/install.sql.mysql.php');
-			if (defined('SQL_UPDATE_UTF8MB4')) {
-				Minz_Log::warning('Updating MySQL to UTF8MB4...');	//v1.5.0
-				$hadTransaction = $this->bd->inTransaction();
-				if ($hadTransaction) {
-					$this->bd->commit();
-				}
-				$ok = false;
-				try {
-					$sql = sprintf(SQL_UPDATE_UTF8MB4, $this->prefix, $db['base']);
-					$stm = $this->bd->prepare($sql);
-					$ok = $stm->execute();
-				} catch (Exception $e) {
-					Minz_Log::error('FreshRSS_EntryDAO::updateToUtf8mb4 error: ' . $e->getMessage());
-				}
-				if ($hadTransaction) {
-					$this->bd->beginTransaction();
-					//NB: Transaction not starting. Why? (tested on PHP 7.0.8-0ubuntu and MySQL 5.7.13-0ubuntu)
-				}
-				return $ok;
-			}
-		}
-		return false;
-	}
+		Minz_Log::warning('Update MySQL table to use MEDIUMBLOB...');
 
 
-	//TODO: Move the database auto-updates to DatabaseDAO
-	protected function createEntryTempTable() {
-		$ok = false;
-		$hadTransaction = $this->bd->inTransaction();
-		if ($hadTransaction) {
-			$this->bd->commit();
-		}
+		$sql = <<<'SQL'
+ALTER TABLE `_entry` MODIFY `content_bin` MEDIUMBLOB;
+ALTER TABLE `_entrytmp` MODIFY `content_bin` MEDIUMBLOB;
+SQL;
 		try {
 		try {
-			$db = FreshRSS_Context::$system_conf->db;
-			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
-			Minz_Log::warning('SQL CREATE TABLE entrytmp...');
-			if (defined('SQL_CREATE_TABLE_ENTRYTMP')) {
-				$sql = sprintf(SQL_CREATE_TABLE_ENTRYTMP, $this->prefix);
-				$stm = $this->bd->prepare($sql);
-				$ok = $stm && $stm->execute();
-			} else {
-				global $SQL_CREATE_TABLE_ENTRYTMP;
-				$ok = !empty($SQL_CREATE_TABLE_ENTRYTMP);
-				foreach ($SQL_CREATE_TABLE_ENTRYTMP as $instruction) {
-					$sql = sprintf($instruction, $this->prefix);
-					$stm = $this->bd->prepare($sql);
-					$ok &= $stm && $stm->execute();
-				}
-			}
+			$ok = $this->pdo->exec($sql) !== false;
 		} catch (Exception $e) {
 		} catch (Exception $e) {
-			Minz_Log::error('FreshRSS_EntryDAO::createEntryTempTable error: ' . $e->getMessage());
-		}
-		if ($hadTransaction) {
-			$this->bd->beginTransaction();
+			$ok = false;
+			Minz_Log::error(__method__ . ' error: ' . $e->getMessage());
 		}
 		}
 		return $ok;
 		return $ok;
 	}
 	}
@@ -126,14 +60,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	//TODO: Move the database auto-updates to DatabaseDAO
 	//TODO: Move the database auto-updates to DatabaseDAO
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
 		if (isset($errorInfo[0])) {
 		if (isset($errorInfo[0])) {
-			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR) {
-				//autoAddColumn
-				foreach (array('lastSeen', 'hash') as $column) {
-					if (stripos($errorInfo[2], $column) !== false) {
-						return $this->addColumn($column);
-					}
-				}
-			} elseif ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR) {
+			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR) {
 				if (stripos($errorInfo[2], 'tag') !== false) {
 				if (stripos($errorInfo[2], 'tag') !== false) {
 					$tagDAO = FreshRSS_Factory::createTagDao();
 					$tagDAO = FreshRSS_Factory::createTagDao();
 					return $tagDAO->createTagTable();	//v1.12.0
 					return $tagDAO->createTagTable();	//v1.12.0
@@ -143,8 +70,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			}
 			}
 		}
 		}
 		if (isset($errorInfo[1])) {
 		if (isset($errorInfo[1])) {
-			if ($errorInfo[1] == FreshRSS_DatabaseDAO::ER_TRUNCATED_WRONG_VALUE_FOR_FIELD) {
-				return $this->updateToUtf8mb4();	//v1.5.0
+			if ($errorInfo[1] == FreshRSS_DatabaseDAO::ER_DATA_TOO_LONG) {
+				if (stripos($errorInfo[2], 'content_bin') !== false) {
+					return $this->updateToMediumBlob();	//v1.15.0
+				}
 			}
 			}
 		}
 		}
 		return false;
 		return false;
@@ -152,9 +81,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 	private $addEntryPrepared = null;
 	private $addEntryPrepared = null;
 
 
-	public function addEntry($valuesTmp) {
+	public function addEntry($valuesTmp, $useTmpTable = true) {
 		if ($this->addEntryPrepared == null) {
 		if ($this->addEntryPrepared == null) {
-			$sql = 'INSERT INTO `' . $this->prefix . 'entrytmp` (id, guid, title, author, '
+			$sql = 'INSERT INTO `_' . ($useTmpTable ? 'entrytmp' : '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) '
 				. ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) '
 				. 'VALUES(:id, :guid, :title, :author, '
 				. 'VALUES(:id, :guid, :title, :author, '
@@ -162,7 +91,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 				. ', :link, :date, :last_seen, '
 				. ', :link, :date, :last_seen, '
 				. $this->sqlHexDecode(':hash')
 				. $this->sqlHexDecode(':hash')
 				. ', :is_read, :is_favorite, :id_feed, :tags)';
 				. ', :is_read, :is_favorite, :id_feed, :tags)';
-			$this->addEntryPrepared = $this->bd->prepare($sql);
+			$this->addEntryPrepared = $this->pdo->prepare($sql);
 		}
 		}
 		if ($this->addEntryPrepared) {
 		if ($this->addEntryPrepared) {
 			$this->addEntryPrepared->bindParam(':id', $valuesTmp['id']);
 			$this->addEntryPrepared->bindParam(':id', $valuesTmp['id']);
@@ -178,7 +107,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$valuesTmp['link'] = safe_ascii($valuesTmp['link']);
 			$valuesTmp['link'] = safe_ascii($valuesTmp['link']);
 			$this->addEntryPrepared->bindParam(':link', $valuesTmp['link']);
 			$this->addEntryPrepared->bindParam(':link', $valuesTmp['link']);
 			$this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
 			$this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
-			$valuesTmp['lastSeen'] = time();
+			if (empty($valuesTmp['lastSeen'])) {
+				$valuesTmp['lastSeen'] = time();
+			}
 			$this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
 			$this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
 			$valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0;
 			$valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0;
 			$this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT);
 			$this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT);
@@ -191,14 +122,14 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			if ($this->hasNativeHex()) {
 			if ($this->hasNativeHex()) {
 				$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
 				$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
 			} else {
 			} else {
-				$valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']);	//hex2bin() is PHP5.4+
+				$valuesTmp['hashBin'] = hex2bin($valuesTmp['hash']);
 				$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
 				$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
 			}
 			}
 		}
 		}
 		if ($this->addEntryPrepared && $this->addEntryPrepared->execute()) {
 		if ($this->addEntryPrepared && $this->addEntryPrepared->execute()) {
 			return true;
 			return true;
 		} else {
 		} else {
-			$info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo();
+			$info = $this->addEntryPrepared == null ? $this->pdo->errorInfo() : $this->addEntryPrepared->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
 				$this->addEntryPrepared = null;
 				$this->addEntryPrepared = null;
 				return $this->addEntry($valuesTmp);
 				return $this->addEntry($valuesTmp);
@@ -211,22 +142,26 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function commitNewEntries() {
 	public function commitNewEntries() {
-		$sql = 'SET @rank=(SELECT MAX(id) - COUNT(*) FROM `' . $this->prefix . 'entrytmp`); ' .	//MySQL-specific
-			'INSERT IGNORE INTO `' . $this->prefix . 'entry`
-				(
-					id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
-				) ' .
-				'SELECT @rank:=@rank+1 AS id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
-					FROM `' . $this->prefix . 'entrytmp`
-					ORDER BY date; ' .
-			'DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= @rank;';
-		$hadTransaction = $this->bd->inTransaction();
+		$sql = <<<'SQL'
+SET @rank=(SELECT MAX(id) - COUNT(*) FROM `_entrytmp`);
+
+INSERT IGNORE INTO `_entry` (
+	id, guid, title, author, content_bin, link, date, `lastSeen`,
+	hash, is_read, is_favorite, id_feed, tags
+)
+SELECT @rank:=@rank+1 AS id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
+FROM `_entrytmp`
+ORDER BY date;
+
+DELETE FROM `_entrytmp` WHERE id <= @rank;';
+SQL;
+		$hadTransaction = $this->pdo->inTransaction();
 		if (!$hadTransaction) {
 		if (!$hadTransaction) {
-			$this->bd->beginTransaction();
+			$this->pdo->beginTransaction();
 		}
 		}
-		$result = $this->bd->exec($sql) !== false;
+		$result = $this->pdo->exec($sql) !== false;
 		if (!$hadTransaction) {
 		if (!$hadTransaction) {
-			$this->bd->commit();
+			$this->pdo->commit();
 		}
 		}
 		return $result;
 		return $result;
 	}
 	}
@@ -239,7 +174,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		}
 
 
 		if ($this->updateEntryPrepared === null) {
 		if ($this->updateEntryPrepared === null) {
-			$sql = 'UPDATE `' . $this->prefix . 'entry` '
+			$sql = 'UPDATE `_entry` '
 				. 'SET title=:title, author=:author, '
 				. 'SET title=:title, author=:author, '
 				. ($this->isCompressed() ? 'content_bin=COMPRESS(:content)' : 'content=:content')
 				. ($this->isCompressed() ? 'content_bin=COMPRESS(:content)' : 'content=:content')
 				. ', link=:link, date=:date, `lastSeen`=:last_seen, '
 				. ', link=:link, date=:date, `lastSeen`=:last_seen, '
@@ -247,7 +182,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 				. ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=:is_read, ')
 				. ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=:is_read, ')
 				. 'tags=:tags '
 				. 'tags=:tags '
 				. 'WHERE id_feed=:id_feed AND guid=:guid';
 				. 'WHERE id_feed=:id_feed AND guid=:guid';
-			$this->updateEntryPrepared = $this->bd->prepare($sql);
+			$this->updateEntryPrepared = $this->pdo->prepare($sql);
 		}
 		}
 
 
 		$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
 		$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
@@ -273,14 +208,14 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if ($this->hasNativeHex()) {
 		if ($this->hasNativeHex()) {
 			$this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
 			$this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
 		} else {
 		} else {
-			$valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']);	//hex2bin() is PHP5.4+
+			$valuesTmp['hashBin'] = hex2bin($valuesTmp['hash']);
 			$this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
 			$this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
 		}
 		}
 
 
 		if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute()) {
 		if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute()) {
 			return true;
 			return true;
 		} else {
 		} else {
-			$info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo();
+			$info = $this->updateEntryPrepared == null ? $this->pdo->errorInfo() : $this->updateEntryPrepared->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
 				return $this->updateEntry($valuesTmp);
 				return $this->updateEntry($valuesTmp);
 			}
 			}
@@ -308,16 +243,16 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			return 0;
 			return 0;
 		}
 		}
 		FreshRSS_UserDAO::touch();
 		FreshRSS_UserDAO::touch();
-		$sql = 'UPDATE `' . $this->prefix . 'entry` '
+		$sql = 'UPDATE `_entry` '
 			. 'SET is_favorite=? '
 			. 'SET is_favorite=? '
 			. 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
 			. 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
 		$values = array($is_favorite ? 1 : 0);
 		$values = array($is_favorite ? 1 : 0);
 		$values = array_merge($values, $ids);
 		$values = array_merge($values, $ids);
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error markFavorite: ' . $info[2]);
 			Minz_Log::error('SQL error markFavorite: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -335,11 +270,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 * @return boolean
 	 * @return boolean
 	 */
 	 */
 	protected function updateCacheUnreads($catId = false, $feedId = false) {
 	protected function updateCacheUnreads($catId = false, $feedId = false) {
-		$sql = 'UPDATE `' . $this->prefix . 'feed` f '
+		$sql = 'UPDATE `_feed` f '
 			. 'LEFT OUTER JOIN ('
 			. 'LEFT OUTER JOIN ('
 			.	'SELECT e.id_feed, '
 			.	'SELECT e.id_feed, '
 			.	'COUNT(*) AS nbUnreads '
 			.	'COUNT(*) AS nbUnreads '
-			.	'FROM `' . $this->prefix . 'entry` e '
+			.	'FROM `_entry` e '
 			.	'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 '
@@ -358,11 +293,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$sql .= ' f.category=?';
 			$sql .= ' f.category=?';
 			$values[] = $catId;
 			$values[] = $catId;
 		}
 		}
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return true;
 			return true;
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error updateCacheUnreads: ' . $info[2]);
 			Minz_Log::error('SQL error updateCacheUnreads: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -392,14 +327,14 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 				return $affected;
 				return $affected;
 			}
 			}
 
 
-			$sql = 'UPDATE `' . $this->prefix . 'entry` '
+			$sql = 'UPDATE `_entry` '
 				 . 'SET is_read=? '
 				 . 'SET is_read=? '
 				 . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
 				 . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
 			$values = array($is_read ? 1 : 0);
 			$values = array($is_read ? 1 : 0);
 			$values = array_merge($values, $ids);
 			$values = array_merge($values, $ids);
-			$stm = $this->bd->prepare($sql);
+			$stm = $this->pdo->prepare($sql);
 			if (!($stm && $stm->execute($values))) {
 			if (!($stm && $stm->execute($values))) {
-				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 				Minz_Log::error('SQL error markRead: ' . $info[2]);
 				Minz_Log::error('SQL error markRead: ' . $info[2]);
 				return false;
 				return false;
 			}
 			}
@@ -409,16 +344,16 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			}
 			}
 			return $affected;
 			return $affected;
 		} else {
 		} else {
-			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
+			$sql = 'UPDATE `_entry` e INNER JOIN `_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->pdo->prepare($sql);
 			if ($stm && $stm->execute($values)) {
 			if ($stm && $stm->execute($values)) {
 				return $stm->rowCount();
 				return $stm->rowCount();
 			} else {
 			} else {
-				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 				Minz_Log::error('SQL error markRead: ' . $info[2]);
 				Minz_Log::error('SQL error markRead: ' . $info[2]);
 				return false;
 				return false;
 			}
 			}
@@ -453,7 +388,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
 			Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
 		}
 		}
 
 
-		$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
+		$sql = 'UPDATE `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id '
 			 . 'SET e.is_read=? '
 			 . 'SET e.is_read=? '
 			 . 'WHERE e.is_read <> ? AND e.id <= ?';
 			 . 'WHERE e.is_read <> ? AND e.id <= ?';
 		if ($onlyFavorites) {
 		if ($onlyFavorites) {
@@ -465,9 +400,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 
 
-		$stm = $this->bd->prepare($sql . $search);
+		$stm = $this->pdo->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 ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error markReadEntries: ' . $info[2]);
 			Minz_Log::error('SQL error markReadEntries: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -496,16 +431,16 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			Minz_Log::debug('Calling markReadCat(0) is deprecated!');
 			Minz_Log::debug('Calling markReadCat(0) is deprecated!');
 		}
 		}
 
 
-		$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
+		$sql = 'UPDATE `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id '
 			 . 'SET e.is_read=? '
 			 . 'SET e.is_read=? '
 			 . 'WHERE f.category=? AND e.is_read <> ? AND e.id <= ?';
 			 . 'WHERE f.category=? AND e.is_read <> ? AND e.id <= ?';
 		$values = array($is_read ? 1 : 0, $id, $is_read ? 1 : 0, $idMax);
 		$values = array($is_read ? 1 : 0, $id, $is_read ? 1 : 0, $idMax);
 
 
 		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 
 
-		$stm = $this->bd->prepare($sql . $search);
+		$stm = $this->pdo->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 ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error markReadCat: ' . $info[2]);
 			Minz_Log::error('SQL error markReadCat: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -533,39 +468,39 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$idMax = time() . '000000';
 			$idMax = time() . '000000';
 			Minz_Log::debug('Calling markReadFeed(0) is deprecated!');
 			Minz_Log::debug('Calling markReadFeed(0) is deprecated!');
 		}
 		}
-		$this->bd->beginTransaction();
+		$this->pdo->beginTransaction();
 
 
-		$sql = 'UPDATE `' . $this->prefix . 'entry` '
+		$sql = 'UPDATE `_entry` '
 			 . 'SET is_read=? '
 			 . 'SET is_read=? '
 			 . 'WHERE id_feed=? AND is_read <> ? AND id <= ?';
 			 . 'WHERE id_feed=? AND is_read <> ? AND id <= ?';
 		$values = array($is_read ? 1 : 0, $id_feed, $is_read ? 1 : 0, $idMax);
 		$values = array($is_read ? 1 : 0, $id_feed, $is_read ? 1 : 0, $idMax);
 
 
 		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 
 
-		$stm = $this->bd->prepare($sql . $search);
+		$stm = $this->pdo->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 ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error markReadFeed: ' . $info[2] . ' with SQL: ' . $sql . $search);
 			Minz_Log::error('SQL error markReadFeed: ' . $info[2] . ' with SQL: ' . $sql . $search);
-			$this->bd->rollBack();
+			$this->pdo->rollBack();
 			return false;
 			return false;
 		}
 		}
 		$affected = $stm->rowCount();
 		$affected = $stm->rowCount();
 
 
 		if ($affected > 0) {
 		if ($affected > 0) {
-			$sql = 'UPDATE `' . $this->prefix . 'feed` '
+			$sql = 'UPDATE `_feed` '
 				 . 'SET `cache_nbUnreads`=`cache_nbUnreads`-' . $affected
 				 . 'SET `cache_nbUnreads`=`cache_nbUnreads`-' . $affected
-				 . ' WHERE id=?';
-			$values = array($id_feed);
-			$stm = $this->bd->prepare($sql);
-			if (!($stm && $stm->execute($values))) {
-				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				 . ' WHERE id=:id';
+			$stm = $this->pdo->prepare($sql);
+			$stm->bindParam(':id', $id_feed, PDO::PARAM_INT);
+			if (!($stm && $stm->execute())) {
+				$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 				Minz_Log::error('SQL error markReadFeed cache: ' . $info[2]);
 				Minz_Log::error('SQL error markReadFeed cache: ' . $info[2]);
-				$this->bd->rollBack();
+				$this->pdo->rollBack();
 				return false;
 				return false;
 			}
 			}
 		}
 		}
 
 
-		$this->bd->commit();
+		$this->pdo->commit();
 		return $affected;
 		return $affected;
 	}
 	}
 
 
@@ -582,7 +517,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			Minz_Log::debug('Calling markReadTag(0) is deprecated!');
 			Minz_Log::debug('Calling markReadTag(0) is deprecated!');
 		}
 		}
 
 
-		$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'entrytag` et ON et.id_entry = e.id '
+		$sql = 'UPDATE `_entry` e INNER JOIN `_entrytag` et ON et.id_entry = e.id '
 			 . 'SET e.is_read = ? '
 			 . 'SET e.is_read = ? '
 			 . 'WHERE '
 			 . 'WHERE '
 			 . ($id == '' ? '' : 'et.id_tag = ? AND ')
 			 . ($id == '' ? '' : 'et.id_tag = ? AND ')
@@ -596,9 +531,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 
 
-		$stm = $this->bd->prepare($sql . $search);
+		$stm = $this->pdo->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 ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error markReadTag: ' . $info[2]);
 			Minz_Log::error('SQL error markReadTag: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -609,48 +544,86 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $affected;
 		return $affected;
 	}
 	}
 
 
-	public function cleanOldEntries($id_feed, $date_min, $keep = 15) {	//Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
-		$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
-		     . 'WHERE id_feed=:id_feed AND id<=:id_max '
-		     . '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 id NOT IN (SELECT id_entry FROM `' . $this->prefix . 'entrytag`) '	//Do not purge tagged entries
-		     . '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);
+	public function cleanOldEntries($id_feed, $options = []) { //Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
+		$sql = 'DELETE FROM `_entry` WHERE id_feed = :id_feed1';	//No alias for MySQL / MariaDB
+		$params = [];
+		$params[':id_feed1'] = $id_feed;
 
 
-		if ($stm) {
-			$id_max = intval($date_min) . '000000';
-			$stm->bindParam(':id_feed', $id_feed, PDO::PARAM_INT);
-			$stm->bindParam(':id_max', $id_max, PDO::PARAM_STR);
-			$stm->bindParam(':keep', $keep, PDO::PARAM_INT);
+		//==Exclusions==
+		if (!empty($options['keep_favourites'])) {
+			$sql .= ' AND is_favorite = 0';
+		}
+		if (!empty($options['keep_unreads'])) {
+			$sql .= ' AND is_read = 1';
+		}
+		if (!empty($options['keep_labels'])) {
+			$sql .= ' AND NOT EXISTS (SELECT 1 FROM `_entrytag` WHERE id_entry = id)';
+		}
+		if (!empty($options['keep_min']) && $options['keep_min'] > 0) {
+			//Double SELECT for MySQL workaround ERROR 1093 (HY000)
+			$sql .= ' AND `lastSeen` < (SELECT `lastSeen`'
+			      . ' FROM (SELECT e2.`lastSeen` FROM `_entry` e2 WHERE e2.id_feed = :id_feed2'
+			      . ' ORDER BY e2.`lastSeen` DESC LIMIT 1 OFFSET :keep_min) last_seen2)';
+			$params[':id_feed2'] = $id_feed;
+			$params[':keep_min'] = (int)$options['keep_min'];
+		}
+		//Keep at least the articles seen at the last refresh
+		$sql .= ' AND `lastSeen` < (SELECT maxlastseen'
+		      . ' FROM (SELECT MAX(e3.`lastSeen`) AS maxlastseen FROM `_entry` e3 WHERE e3.id_feed = :id_feed3) last_seen3)';
+		$params[':id_feed3'] = $id_feed;
+
+		//==Inclusions==
+		$sql .= ' AND (1=0';
+		if (!empty($options['keep_period'])) {
+			$sql .= ' OR `lastSeen` < :max_last_seen';
+			$now = new DateTime('now');
+			$now->sub(new DateInterval($options['keep_period']));
+			$params[':max_last_seen'] = $now->format('U');
 		}
 		}
+		if (!empty($options['keep_max']) && $options['keep_max'] > 0) {
+			$sql .= ' OR `lastSeen` <= (SELECT `lastSeen`'
+			      . ' FROM (SELECT e4.`lastSeen` FROM `_entry` e4 WHERE e4.id_feed = :id_feed4'
+			      . ' ORDER BY e4.`lastSeen` DESC LIMIT 1 OFFSET :keep_max) last_seen4)';
+			$params[':id_feed4'] = $id_feed;
+			$params[':keep_max'] = (int)$options['keep_max'];
+		}
+		$sql .= ')';
+
+		$stm = $this->pdo->prepare($sql);
 
 
-		if ($stm && $stm->execute()) {
+		if ($stm && $stm->execute($params)) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
-				return $this->cleanOldEntries($id_feed, $date_min, $keep);
+				return $this->cleanOldEntries($id_feed, $options);
 			}
 			}
-			Minz_Log::error('SQL error cleanOldEntries: ' . $info[2]);
+			Minz_Log::error(__method__ . ' error:' . json_encode($info));
 			return false;
 			return false;
 		}
 		}
 	}
 	}
 
 
+	public function selectAll() {
+		$sql = 'SELECT id, guid, title, author, '
+			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
+			. ', link, date, `lastSeen`, ' . $this->sqlHexEncode('hash') . ' AS hash, is_read, is_favorite, id_feed, tags '
+			. 'FROM `_entry`';
+		$stm = $this->pdo->query($sql);
+		while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
+			yield $row;
+		}
+	}
+
 	public function searchByGuid($id_feed, $guid) {
 	public function searchByGuid($id_feed, $guid) {
 		// un guid est unique pour un flux donné
 		// un guid est unique pour un flux donné
 		$sql = 'SELECT id, guid, title, author, '
 		$sql = 'SELECT id, guid, title, author, '
 			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
 			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
 			. ', link, date, is_read, is_favorite, id_feed, tags '
 			. ', link, date, is_read, is_favorite, id_feed, tags '
-			. 'FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid=?';
-		$stm = $this->bd->prepare($sql);
-
-		$values = array(
-			$id_feed,
-			$guid,
-		);
-
-		$stm->execute($values);
+			. 'FROM `_entry` WHERE id_feed=:id_feed AND guid=:guid';
+		$stm = $this->pdo->prepare($sql);
+		$stm->bindParam(':id_feed', $id_feed, PDO::PARAM_INT);
+		$stm->bindParam(':guid', $guid);
+		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$entries = self::daoToEntries($res);
 		$entries = self::daoToEntries($res);
 		return isset($entries[0]) ? $entries[0] : null;
 		return isset($entries[0]) ? $entries[0] : null;
@@ -660,22 +633,21 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$sql = 'SELECT id, guid, title, author, '
 		$sql = 'SELECT id, guid, title, author, '
 			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
 			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
 			. ', link, date, is_read, is_favorite, id_feed, tags '
 			. ', link, date, is_read, is_favorite, id_feed, tags '
-			. 'FROM `' . $this->prefix . 'entry` WHERE id=?';
-		$stm = $this->bd->prepare($sql);
-
-		$values = array($id);
-
-		$stm->execute($values);
+			. 'FROM `_entry` WHERE id=:id';
+		$stm = $this->pdo->prepare($sql);
+		$stm->bindParam(':id', $id, PDO::PARAM_INT);
+		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$entries = self::daoToEntries($res);
 		$entries = self::daoToEntries($res);
 		return isset($entries[0]) ? $entries[0] : null;
 		return isset($entries[0]) ? $entries[0] : null;
 	}
 	}
 
 
 	public function searchIdByGuid($id_feed, $guid) {
 	public function searchIdByGuid($id_feed, $guid) {
-		$sql = 'SELECT id FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid=?';
-		$stm = $this->bd->prepare($sql);
-		$values = array($id_feed, $guid);
-		$stm->execute($values);
+		$sql = 'SELECT id FROM `_entry` WHERE id_feed=:id_feed AND guid=:guid';
+		$stm = $this->pdo->prepare($sql);
+		$stm->bindParam(':id_feed', $id_feed, PDO::PARAM_INT);
+		$stm->bindParam(':guid', $guid);
+		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		return isset($res[0]) ? $res[0] : null;
 		return isset($res[0]) ? $res[0] : null;
 	}
 	}
@@ -859,7 +831,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$where .= '1=1 ';
 			$where .= '1=1 ';
 			break;
 			break;
 		case 'ST':	//Starred or tagged
 		case 'ST':	//Starred or tagged
-			$where .= 'e.is_favorite=1 OR EXISTS (SELECT et2.id_tag FROM `' . $this->prefix . 'entrytag` et2 WHERE et2.id_entry = e.id) ';
+			$where .= 'e.is_favorite=1 OR EXISTS (SELECT et2.id_tag FROM `_entrytag` et2 WHERE et2.id_entry = e.id) ';
 			break;
 			break;
 		default:
 		default:
 			throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
 			throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
@@ -870,9 +842,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return array(array_merge($values, $searchValues),
 		return array(array_merge($values, $searchValues),
 			'SELECT '
 			'SELECT '
 			. ($type === 'T' ? 'DISTINCT ' : '')
 			. ($type === 'T' ? 'DISTINCT ' : '')
-			. 'e.id FROM `' . $this->prefix . 'entry` e '
-			. 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
-			. ($type === 't' || $type === 'T' ? 'INNER JOIN `' . $this->prefix . 'entrytag` et ON et.id_entry = e.id ' : '')
+			. 'e.id FROM `_entry` e '
+			. 'INNER JOIN `_feed` f ON e.id_feed = f.id '
+			. ($type === 't' || $type === 'T' ? 'INNER JOIN `_entrytag` et ON et.id_entry = e.id ' : '')
 			. 'WHERE ' . $where
 			. 'WHERE ' . $where
 			. $search
 			. $search
 			. 'ORDER BY e.id ' . $order
 			. 'ORDER BY e.id ' . $order
@@ -885,17 +857,17 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$sql = 'SELECT e0.id, e0.guid, e0.title, e0.author, '
 		$sql = 'SELECT e0.id, e0.guid, e0.title, e0.author, '
 			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
 			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
 			. ', e0.link, e0.date, e0.is_read, e0.is_favorite, e0.id_feed, e0.tags '
 			. ', e0.link, e0.date, e0.is_read, e0.is_favorite, e0.id_feed, e0.tags '
-			. 'FROM `' . $this->prefix . 'entry` e0 '
+			. 'FROM `_entry` e0 '
 			. 'INNER JOIN ('
 			. 'INNER JOIN ('
 			. $sql
 			. $sql
 			. ') e2 ON e2.id=e0.id '
 			. ') e2 ON e2.id=e0.id '
 			. 'ORDER BY e0.id ' . $order;
 			. 'ORDER BY e0.id ' . $order;
 
 
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm;
 			return $stm;
 		} else {
 		} else {
-			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error listWhereRaw: ' . $info[2]);
 			Minz_Log::error('SQL error listWhereRaw: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -918,11 +890,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$sql = 'SELECT id, guid, title, author, '
 		$sql = 'SELECT id, guid, title, author, '
 			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
 			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
 			. ', link, date, is_read, is_favorite, id_feed, tags '
 			. ', link, date, is_read, is_favorite, id_feed, tags '
-			. 'FROM `' . $this->prefix . 'entry` '
+			. 'FROM `_entry` '
 			. 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?) '
 			. 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?) '
 			. 'ORDER BY id ' . $order;
 			. 'ORDER BY id ' . $order;
 
 
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 		$stm->execute($ids);
 		$stm->execute($ids);
 		return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
 		return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 	}
@@ -930,7 +902,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null) {	//For API
 	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null) {	//For API
 		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
 		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
 
 
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 		$stm->execute($values);
 		$stm->execute($values);
 
 
 		return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
@@ -941,8 +913,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			return array();
 			return array();
 		}
 		}
 		$guids = array_unique($guids);
 		$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);
+		$sql = 'SELECT guid, ' . $this->sqlHexEncode('hash') . ' AS hex_hash FROM `_entry` WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
+		$stm = $this->pdo->prepare($sql);
 		$values = array($id_feed);
 		$values = array($id_feed);
 		$values = array_merge($values, $guids);
 		$values = array_merge($values, $guids);
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
@@ -953,7 +925,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			}
 			}
 			return $result;
 			return $result;
 		} else {
 		} else {
-			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
 				return $this->listHashForFeedGuids($id_feed, $guids);
 				return $this->listHashForFeedGuids($id_feed, $guids);
 			}
 			}
@@ -967,8 +939,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		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). '?)';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'UPDATE `_entry` SET `lastSeen`=? WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
+		$stm = $this->pdo->prepare($sql);
 		if ($mtime <= 0) {
 		if ($mtime <= 0) {
 			$mtime = time();
 			$mtime = time();
 		}
 		}
@@ -977,7 +949,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
 				return $this->updateLastSeen($id_feed, $guids);
 				return $this->updateLastSeen($id_feed, $guids);
 			}
 			}
@@ -988,65 +960,70 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function countUnreadRead() {
 	public function countUnreadRead() {
-		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE f.priority > 0'
-			. ' UNION SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE f.priority > 0 AND e.is_read=0';
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$sql = 'SELECT COUNT(e.id) AS count FROM `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id WHERE f.priority > 0'
+			. ' UNION SELECT COUNT(e.id) AS count FROM `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id WHERE f.priority > 0 AND e.is_read=0';
+		$stm = $this->pdo->query($sql);
+		if ($stm === false) {
+			return false;
+		}
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		rsort($res);
 		rsort($res);
 		$all = empty($res[0]) ? 0 : $res[0];
 		$all = empty($res[0]) ? 0 : $res[0];
 		$unread = empty($res[1]) ? 0 : $res[1];
 		$unread = empty($res[1]) ? 0 : $res[1];
 		return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
 		return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
 	}
 	}
+
 	public function count($minPriority = null) {
 	public function count($minPriority = null) {
-		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e';
+		$sql = 'SELECT COUNT(e.id) AS count FROM `_entry` e';
 		if ($minPriority !== null) {
 		if ($minPriority !== null) {
-			$sql .= ' INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id';
+			$sql .= ' INNER JOIN `_feed` f ON e.id_feed=f.id';
 			$sql .= ' WHERE f.priority > ' . intval($minPriority);
 			$sql .= ' WHERE f.priority > ' . intval($minPriority);
 		}
 		}
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
+		if ($stm == false) {
+			return false;
+		}
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		return isset($res[0]) ? $res[0] : 0;
 		return isset($res[0]) ? $res[0] : 0;
 	}
 	}
+
 	public function countNotRead($minPriority = null) {
 	public function countNotRead($minPriority = null) {
-		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e';
+		$sql = 'SELECT COUNT(e.id) AS count FROM `_entry` e';
 		if ($minPriority !== null) {
 		if ($minPriority !== null) {
-			$sql .= ' INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id';
+			$sql .= ' INNER JOIN `_feed` f ON e.id_feed=f.id';
 		}
 		}
 		$sql .= ' WHERE e.is_read=0';
 		$sql .= ' WHERE e.is_read=0';
 		if ($minPriority !== null) {
 		if ($minPriority !== null) {
 			$sql .= ' AND f.priority > ' . intval($minPriority);
 			$sql .= ' AND f.priority > ' . intval($minPriority);
 		}
 		}
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		return $res[0];
 		return $res[0];
 	}
 	}
 
 
 	public function countUnreadReadFavorites() {
 	public function countUnreadReadFavorites() {
-		$sql = <<<SQL
-  SELECT c
-    FROM (
-         SELECT COUNT(e1.id) AS c
-              , 1 AS o
-           FROM `{$this->prefix}entry` AS e1
-           JOIN `{$this->prefix}feed` AS f1 ON e1.id_feed = f1.id
-          WHERE e1.is_favorite = 1
-            AND f1.priority >= :priority_normal
-         UNION
-         SELECT COUNT(e2.id) AS c
-              , 2 AS o
-           FROM `{$this->prefix}entry` AS e2
-           JOIN `{$this->prefix}feed` AS f2 ON e2.id_feed = f2.id
-          WHERE e2.is_favorite = 1
-            AND e2.is_read = 0
-            AND f2.priority >= :priority_normal
-         ) u
+		$sql = <<<'SQL'
+SELECT c FROM (
+	SELECT COUNT(e1.id) AS c, 1 AS o
+		 FROM `_entry` AS e1
+		 JOIN `_feed` AS f1 ON e1.id_feed = f1.id
+		WHERE e1.is_favorite = 1
+		  AND f1.priority >= :priority_normal1
+	UNION
+	SELECT COUNT(e2.id) AS c, 2 AS o
+		 FROM `_entry` AS e2
+		 JOIN `_feed` AS f2 ON e2.id_feed = f2.id
+		WHERE e2.is_favorite = 1
+		  AND e2.is_read = 0
+		  AND f2.priority >= :priority_normal2
+	) u
 ORDER BY o
 ORDER BY o
 SQL;
 SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute(array(':priority_normal' => FreshRSS_Feed::PRIORITY_NORMAL));
+		$stm = $this->pdo->prepare($sql);
+		//Binding a value more than once is not standard and does not work with native prepared statements (e.g. MySQL) https://bugs.php.net/bug.php?id=40417
+		$stm->bindValue(':priority_normal1', FreshRSS_Feed::PRIORITY_NORMAL, PDO::PARAM_INT);
+		$stm->bindValue(':priority_normal2', FreshRSS_Feed::PRIORITY_NORMAL, PDO::PARAM_INT);
+		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		rsort($res);
 		rsort($res);
 		$all = empty($res[0]) ? 0 : $res[0];
 		$all = empty($res[0]) ? 0 : $res[0];

+ 17 - 11
app/Models/EntryDAOPGSQL.php

@@ -2,6 +2,10 @@
 
 
 class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
 class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
 
 
+	public function hasNativeHex() {
+		return true;
+	}
+
 	public function sqlHexDecode($x) {
 	public function sqlHexDecode($x) {
 		return 'decode(' . $x . ", 'hex')";
 		return 'decode(' . $x . ", 'hex')";
 	}
 	}
@@ -31,25 +35,27 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
 	public function commitNewEntries() {
 	public function commitNewEntries() {
 		$sql = 'DO $$
 		$sql = 'DO $$
 DECLARE
 DECLARE
-maxrank bigint := (SELECT MAX(id) FROM `' . $this->prefix . 'entrytmp`);
-rank bigint := (SELECT maxrank - COUNT(*) FROM `' . $this->prefix . 'entrytmp`);
+maxrank bigint := (SELECT MAX(id) FROM `_entrytmp`);
+rank bigint := (SELECT maxrank - COUNT(*) FROM `_entrytmp`);
 BEGIN
 BEGIN
-	INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
-		(SELECT rank + row_number() OVER(ORDER BY date) AS id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
-			FROM `' . $this->prefix . 'entrytmp` AS etmp
+	INSERT INTO `_entry`
+		(id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
+		(SELECT rank + row_number() OVER(ORDER BY date) AS id, guid, title, author, content,
+			link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
+			FROM `_entrytmp` AS etmp
 			WHERE NOT EXISTS (
 			WHERE NOT EXISTS (
-				SELECT 1 FROM `' . $this->prefix . 'entry` AS ereal
+				SELECT 1 FROM `_entry` AS ereal
 				WHERE (etmp.id = ereal.id) OR (etmp.id_feed = ereal.id_feed AND etmp.guid = ereal.guid))
 				WHERE (etmp.id = ereal.id) OR (etmp.id_feed = ereal.id_feed AND etmp.guid = ereal.guid))
 			ORDER BY date);
 			ORDER BY date);
-	DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= maxrank;
+	DELETE FROM `_entrytmp` WHERE id <= maxrank;
 END $$;';
 END $$;';
-		$hadTransaction = $this->bd->inTransaction();
+		$hadTransaction = $this->pdo->inTransaction();
 		if (!$hadTransaction) {
 		if (!$hadTransaction) {
-			$this->bd->beginTransaction();
+			$this->pdo->beginTransaction();
 		}
 		}
-		$result = $this->bd->exec($sql) !== false;
+		$result = $this->pdo->exec($sql) !== false;
 		if (!$hadTransaction) {
 		if (!$hadTransaction) {
-			$this->bd->commit();
+			$this->pdo->commit();
 		}
 		}
 		return $result;
 		return $result;
 	}
 	}

+ 46 - 46
app/Models/EntryDAOSQLite.php

@@ -2,32 +2,32 @@
 
 
 class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 
 
+	public function isCompressed() {
+		return false;
+	}
+
+	public function hasNativeHex() {
+		return false;
+	}
+
 	public function sqlHexDecode($x) {
 	public function sqlHexDecode($x) {
 		return $x;
 		return $x;
 	}
 	}
 
 
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
-		if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='tag'")) {
+		if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='tag'")) {
 			$showCreate = $tableInfo->fetchColumn();
 			$showCreate = $tableInfo->fetchColumn();
 			if (stripos($showCreate, 'tag') === false) {
 			if (stripos($showCreate, 'tag') === false) {
 				$tagDAO = FreshRSS_Factory::createTagDao();
 				$tagDAO = FreshRSS_Factory::createTagDao();
 				return $tagDAO->createTagTable();	//v1.12.0
 				return $tagDAO->createTagTable();	//v1.12.0
 			}
 			}
 		}
 		}
-		if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entrytmp'")) {
+		if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='entrytmp'")) {
 			$showCreate = $tableInfo->fetchColumn();
 			$showCreate = $tableInfo->fetchColumn();
 			if (stripos($showCreate, 'entrytmp') === false) {
 			if (stripos($showCreate, 'entrytmp') === false) {
 				return $this->createEntryTempTable();	//v1.7.0
 				return $this->createEntryTempTable();	//v1.7.0
 			}
 			}
 		}
 		}
-		if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) {
-			$showCreate = $tableInfo->fetchColumn();
-			foreach (array('lastSeen', 'hash') as $column) {
-				if (stripos($showCreate, $column) === false) {
-					return $this->addColumn($column);
-				}
-			}
-		}
 		return false;
 		return false;
 	}
 	}
 
 
@@ -36,27 +36,27 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 DROP TABLE IF EXISTS `tmp`;
 DROP TABLE IF EXISTS `tmp`;
 CREATE TEMP TABLE `tmp` AS
 CREATE TEMP TABLE `tmp` AS
 	SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
 	SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
-	FROM `' . $this->prefix . 'entrytmp`
+	FROM `_entrytmp`
 	ORDER BY date;
 	ORDER BY date;
-INSERT OR IGNORE INTO `' . $this->prefix . 'entry`
+INSERT OR IGNORE INTO `_entry`
 	(id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
 	(id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
 	SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS id,
 	SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS id,
 	guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
 	guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
 	FROM `tmp`
 	FROM `tmp`
 	ORDER BY date;
 	ORDER BY date;
-DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= (SELECT MAX(id) FROM `tmp`);
+DELETE FROM `_entrytmp` WHERE id <= (SELECT MAX(id) FROM `tmp`);
 DROP TABLE IF EXISTS `tmp`;
 DROP TABLE IF EXISTS `tmp`;
 ';
 ';
-		$hadTransaction = $this->bd->inTransaction();
+		$hadTransaction = $this->pdo->inTransaction();
 		if (!$hadTransaction) {
 		if (!$hadTransaction) {
-			$this->bd->beginTransaction();
+			$this->pdo->beginTransaction();
 		}
 		}
-		$result = $this->bd->exec($sql) !== false;
+		$result = $this->pdo->exec($sql) !== false;
 		if (!$result) {
 		if (!$result) {
-			Minz_Log::error('SQL error commitNewEntries: ' . json_encode($this->bd->errorInfo()));
+			Minz_Log::error('SQL error commitNewEntries: ' . json_encode($this->pdo->errorInfo()));
 		}
 		}
 		if (!$hadTransaction) {
 		if (!$hadTransaction) {
-			$this->bd->commit();
+			$this->pdo->commit();
 		}
 		}
 		return $result;
 		return $result;
 	}
 	}
@@ -66,10 +66,10 @@ DROP TABLE IF EXISTS `tmp`;
 	}
 	}
 
 
 	protected function updateCacheUnreads($catId = false, $feedId = false) {
 	protected function updateCacheUnreads($catId = false, $feedId = false) {
-		$sql = 'UPDATE `' . $this->prefix . 'feed` '
+		$sql = 'UPDATE `_feed` '
 		 . 'SET `cache_nbUnreads`=('
 		 . 'SET `cache_nbUnreads`=('
-		 .	'SELECT COUNT(*) AS nbUnreads FROM `' . $this->prefix . 'entry` e '
-		 .	'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0)';
+		 .	'SELECT COUNT(*) AS nbUnreads FROM `_entry` e '
+		 .	'WHERE e.id_feed=`_feed`.id AND e.is_read=0)';
 		$hasWhere = false;
 		$hasWhere = false;
 		$values = array();
 		$values = array();
 		if ($feedId !== false) {
 		if ($feedId !== false) {
@@ -84,11 +84,11 @@ DROP TABLE IF EXISTS `tmp`;
 			$sql .= ' category=?';
 			$sql .= ' category=?';
 			$values[] = $catId;
 			$values[] = $catId;
 		}
 		}
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return true;
 			return true;
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error updateCacheUnreads: ' . $info[2]);
 			Minz_Log::error('SQL error updateCacheUnreads: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -118,30 +118,30 @@ DROP TABLE IF EXISTS `tmp`;
 				return $affected;
 				return $affected;
 			}
 			}
 		} else {
 		} else {
-			$this->bd->beginTransaction();
-			$sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read=? WHERE id=? AND is_read=?';
+			$this->pdo->beginTransaction();
+			$sql = 'UPDATE `_entry` SET is_read=? WHERE id=? AND 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->pdo->prepare($sql);
 			if (!($stm && $stm->execute($values))) {
 			if (!($stm && $stm->execute($values))) {
-				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 				Minz_Log::error('SQL error markRead 1: ' . $info[2]);
 				Minz_Log::error('SQL error markRead 1: ' . $info[2]);
-				$this->bd->rollBack();
+				$this->pdo->rollBack();
 				return false;
 				return false;
 			}
 			}
 			$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 '
-				 . 'WHERE id=(SELECT e.id_feed FROM `' . $this->prefix . 'entry` e WHERE e.id=?)';
+				$sql = 'UPDATE `_feed` SET `cache_nbUnreads`=`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
+				 . 'WHERE id=(SELECT e.id_feed FROM `_entry` e WHERE e.id=?)';
 				$values = array($ids);
 				$values = array($ids);
-				$stm = $this->bd->prepare($sql);
+				$stm = $this->pdo->prepare($sql);
 				if (!($stm && $stm->execute($values))) {
 				if (!($stm && $stm->execute($values))) {
-					$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+					$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 					Minz_Log::error('SQL error markRead 2: ' . $info[2]);
 					Minz_Log::error('SQL error markRead 2: ' . $info[2]);
-					$this->bd->rollBack();
+					$this->pdo->rollBack();
 					return false;
 					return false;
 				}
 				}
 			}
 			}
-			$this->bd->commit();
+			$this->pdo->commit();
 			return $affected;
 			return $affected;
 		}
 		}
 	}
 	}
@@ -174,19 +174,19 @@ DROP TABLE IF EXISTS `tmp`;
 			Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
 			Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
 		}
 		}
 
 
-		$sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read = ? WHERE is_read <> ? AND id <= ?';
+		$sql = 'UPDATE `_entry` SET is_read = ? WHERE is_read <> ? AND id <= ?';
 		if ($onlyFavorites) {
 		if ($onlyFavorites) {
 			$sql .= ' AND is_favorite=1';
 			$sql .= ' AND is_favorite=1';
 		} elseif ($priorityMin >= 0) {
 		} elseif ($priorityMin >= 0) {
-			$sql .= ' AND id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.priority > ' . intval($priorityMin) . ')';
+			$sql .= ' AND id_feed IN (SELECT f.id FROM `_feed` f WHERE f.priority > ' . intval($priorityMin) . ')';
 		}
 		}
 		$values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax);
 		$values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax);
 
 
 		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 
 
-		$stm = $this->bd->prepare($sql . $search);
+		$stm = $this->pdo->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 ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error markReadEntries: ' . $info[2]);
 			Minz_Log::error('SQL error markReadEntries: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -215,17 +215,17 @@ DROP TABLE IF EXISTS `tmp`;
 			Minz_Log::debug('Calling markReadCat(0) is deprecated!');
 			Minz_Log::debug('Calling markReadCat(0) is deprecated!');
 		}
 		}
 
 
-		$sql = 'UPDATE `' . $this->prefix . 'entry` '
+		$sql = 'UPDATE `_entry` '
 			 . 'SET is_read = ? '
 			 . 'SET is_read = ? '
 			 . 'WHERE is_read <> ? AND id <= ? AND '
 			 . 'WHERE is_read <> ? AND id <= ? AND '
-			 . 'id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.category=?)';
+			 . 'id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category=?)';
 		$values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax, $id);
 		$values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax, $id);
 
 
 		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 
 
-		$stm = $this->bd->prepare($sql . $search);
+		$stm = $this->pdo->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 ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error markReadCat: ' . $info[2]);
 			Minz_Log::error('SQL error markReadCat: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -249,10 +249,10 @@ DROP TABLE IF EXISTS `tmp`;
 			Minz_Log::debug('Calling markReadTag(0) is deprecated!');
 			Minz_Log::debug('Calling markReadTag(0) is deprecated!');
 		}
 		}
 
 
-		$sql = 'UPDATE `' . $this->prefix . 'entry` e '
+		$sql = 'UPDATE `_entry` e '
 			 . 'SET e.is_read = ? '
 			 . 'SET e.is_read = ? '
 			 . 'WHERE e.is_read <> ? AND e.id <= ? AND '
 			 . 'WHERE e.is_read <> ? AND e.id <= ? AND '
-			 . 'e.id IN (SELECT et.id_entry FROM `' . $this->prefix . 'entrytag` et '
+			 . 'e.id IN (SELECT et.id_entry FROM `_entrytag` et '
 			 . ($id == '' ? '' : 'WHERE et.id = ?')
 			 . ($id == '' ? '' : 'WHERE et.id = ?')
 			 . ')';
 			 . ')';
 		$values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax);
 		$values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax);
@@ -262,9 +262,9 @@ DROP TABLE IF EXISTS `tmp`;
 
 
 		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 
 
-		$stm = $this->bd->prepare($sql . $search);
+		$stm = $this->pdo->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 ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error markReadTag: ' . $info[2]);
 			Minz_Log::error('SQL error markReadTag: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}

+ 11 - 1
app/Models/Factory.php

@@ -2,8 +2,18 @@
 
 
 class FreshRSS_Factory {
 class FreshRSS_Factory {
 
 
+	public static function createUserDao($username = null) {
+		return new FreshRSS_UserDAO($username);
+	}
+
 	public static function createCategoryDao($username = null) {
 	public static function createCategoryDao($username = null) {
-		return new FreshRSS_CategoryDAO($username);
+		$conf = Minz_Configuration::get('system');
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_CategoryDAOSQLite($username);
+			default:
+				return new FreshRSS_CategoryDAO($username);
+		}
 	}
 	}
 
 
 	public static function createFeedDao($username = null) {
 	public static function createFeedDao($username = null) {

+ 31 - 24
app/Models/Feed.php

@@ -7,8 +7,8 @@ class FreshRSS_Feed extends Minz_Model {
 
 
 	const TTL_DEFAULT = 0;
 	const TTL_DEFAULT = 0;
 
 
-	const KEEP_HISTORY_DEFAULT = -2;
-	const KEEP_HISTORY_INFINITE = -1;
+	const ARCHIVING_RETENTION_COUNT_LIMIT = 10000;
+	const ARCHIVING_RETENTION_PERIOD = 'P3M';
 
 
 	private $id = 0;
 	private $id = 0;
 	private $url;
 	private $url;
@@ -24,9 +24,8 @@ class FreshRSS_Feed extends Minz_Model {
 	private $pathEntries = '';
 	private $pathEntries = '';
 	private $httpAuth = '';
 	private $httpAuth = '';
 	private $error = false;
 	private $error = false;
-	private $keep_history = self::KEEP_HISTORY_DEFAULT;
 	private $ttl = self::TTL_DEFAULT;
 	private $ttl = self::TTL_DEFAULT;
-	private $attributes = array();
+	private $attributes = [];
 	private $mute = false;
 	private $mute = false;
 	private $hash = null;
 	private $hash = null;
 	private $lockPath = '';
 	private $lockPath = '';
@@ -110,9 +109,6 @@ class FreshRSS_Feed extends Minz_Model {
 	public function inError() {
 	public function inError() {
 		return $this->error;
 		return $this->error;
 	}
 	}
-	public function keepHistory() {
-		return $this->keep_history;
-	}
 	public function ttl() {
 	public function ttl() {
 		return $this->ttl;
 		return $this->ttl;
 	}
 	}
@@ -153,18 +149,17 @@ class FreshRSS_Feed extends Minz_Model {
 		return $this->nbNotRead;
 		return $this->nbNotRead;
 	}
 	}
 	public function faviconPrepare() {
 	public function faviconPrepare() {
-		global $favicons_dir;
 		require_once(LIB_PATH . '/favicons.php');
 		require_once(LIB_PATH . '/favicons.php');
 		$url = $this->website;
 		$url = $this->website;
 		if ($url == '') {
 		if ($url == '') {
 			$url = $this->url;
 			$url = $this->url;
 		}
 		}
-		$txt = $favicons_dir . $this->hash() . '.txt';
+		$txt = FAVICONS_DIR . $this->hash() . '.txt';
 		if (!file_exists($txt)) {
 		if (!file_exists($txt)) {
 			file_put_contents($txt, $url);
 			file_put_contents($txt, $url);
 		}
 		}
 		if (FreshRSS_Context::$isCli) {
 		if (FreshRSS_Context::$isCli) {
-			$ico = $favicons_dir . $this->hash() . '.ico';
+			$ico = FAVICONS_DIR . $this->hash() . '.ico';
 			$ico_mtime = @filemtime($ico);
 			$ico_mtime = @filemtime($ico);
 			$txt_mtime = @filemtime($txt);
 			$txt_mtime = @filemtime($txt);
 			if ($txt_mtime != false &&
 			if ($txt_mtime != false &&
@@ -231,12 +226,6 @@ class FreshRSS_Feed extends Minz_Model {
 	public function _error($value) {
 	public function _error($value) {
 		$this->error = (bool)$value;
 		$this->error = (bool)$value;
 	}
 	}
-	public function _keepHistory($value) {
-		$value = intval($value);
-		$value = min($value, 1000000);
-		$value = max($value, self::KEEP_HISTORY_DEFAULT);
-		$this->keep_history = $value;
-	}
 	public function _ttl($value) {
 	public function _ttl($value) {
 		$value = intval($value);
 		$value = intval($value);
 		$value = min($value, 100000000);
 		$value = min($value, 100000000);
@@ -470,6 +459,28 @@ class FreshRSS_Feed extends Minz_Model {
 		$this->entries = $entries;
 		$this->entries = $entries;
 	}
 	}
 
 
+	public function cleanOldEntries() {	//Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
+		$archiving = $this->attributes('archiving');
+		if ($archiving == null) {
+			$catDAO = FreshRSS_Factory::createCategoryDao();
+			$category = $catDAO->searchById($this->category());
+			$archiving = $category == null ? null : $category->attributes('archiving');
+			if ($archiving == null) {
+				$archiving = FreshRSS_Context::$user_conf->archiving;
+			}
+		}
+		if (is_array($archiving)) {
+			$entryDAO = FreshRSS_Factory::createEntryDao();
+			$nb = $entryDAO->cleanOldEntries($this->id(), $archiving);
+			if ($nb > 0) {
+				$needFeedCacheRefresh = true;
+				Minz_Log::debug($nb . ' entries cleaned in feed [' . $this->url(false) . '] with: ' . json_encode($archiving));
+			}
+			return $nb;
+		}
+		return false;
+	}
+
 	protected function cacheFilename() {
 	protected function cacheFilename() {
 		return CACHE_PATH . '/' . md5($this->url) . '.spc';
 		return CACHE_PATH . '/' . md5($this->url) . '.spc';
 	}
 	}
@@ -701,7 +712,7 @@ class FreshRSS_Feed extends Minz_Model {
 				file_put_contents($hubFilename, json_encode($hubJson));
 				file_put_contents($hubFilename, json_encode($hubJson));
 			}
 			}
 			$ch = curl_init();
 			$ch = curl_init();
-			curl_setopt_array($ch, array(
+			curl_setopt_array($ch, [
 					CURLOPT_URL => $hubJson['hub'],
 					CURLOPT_URL => $hubJson['hub'],
 					CURLOPT_RETURNTRANSFER => true,
 					CURLOPT_RETURNTRANSFER => true,
 					CURLOPT_POSTFIELDS => http_build_query(array(
 					CURLOPT_POSTFIELDS => http_build_query(array(
@@ -712,13 +723,9 @@ class FreshRSS_Feed extends Minz_Model {
 						)),
 						)),
 					CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
 					CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
 					CURLOPT_MAXREDIRS => 10,
 					CURLOPT_MAXREDIRS => 10,
-				));
-			if (version_compare(PHP_VERSION, '5.6.0') >= 0 || ini_get('open_basedir') == '') {
-				curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);	//Keep option separated for open_basedir PHP bug 65646
-			}
-			if (defined('CURLOPT_ENCODING')) {
-				curl_setopt($ch, CURLOPT_ENCODING, '');	//Enable all encodings
-			}
+					CURLOPT_FOLLOWLOCATION => true,
+					CURLOPT_ENCODING => '',	//Enable all encodings
+				]);
 			$response = curl_exec($ch);
 			$response = curl_exec($ch);
 			$info = curl_getinfo($ch);
 			$info = curl_getinfo($ch);
 
 

+ 103 - 105
app/Models/FeedDAO.php

@@ -3,14 +3,13 @@
 class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 	protected function addColumn($name) {
 	protected function addColumn($name) {
-		Minz_Log::warning('FreshRSS_FeedDAO::addColumn: ' . $name);
+		Minz_Log::warning(__method__ . ': ' . $name);
 		try {
 		try {
 			if ($name === 'attributes') {	//v1.11.0
 			if ($name === 'attributes') {	//v1.11.0
-				$stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN attributes TEXT');
-				return $stm && $stm->execute();
+				return $this->pdo->exec('ALTER TABLE `_feed` ADD COLUMN attributes TEXT') !== false;
 			}
 			}
 		} catch (Exception $e) {
 		} catch (Exception $e) {
-			Minz_Log::error('FreshRSS_FeedDAO::addColumn error: ' . $e->getMessage());
+			Minz_Log::error(__method__ . ' error: ' . $e->getMessage());
 		}
 		}
 		return false;
 		return false;
 	}
 	}
@@ -18,7 +17,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
 		if (isset($errorInfo[0])) {
 		if (isset($errorInfo[0])) {
 			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
 			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
-				foreach (array('attributes') as $column) {
+				foreach (['attributes'] as $column) {
 					if (stripos($errorInfo[2], $column) !== false) {
 					if (stripos($errorInfo[2], $column) !== false) {
 						return $this->addColumn($column);
 						return $this->addColumn($column);
 					}
 					}
@@ -30,7 +29,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 	public function addFeed($valuesTmp) {
 	public function addFeed($valuesTmp) {
 		$sql = '
 		$sql = '
-			INSERT INTO `' . $this->prefix . 'feed`
+			INSERT INTO `_feed`
 				(
 				(
 					url,
 					url,
 					category,
 					category,
@@ -39,18 +38,24 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 					description,
 					description,
 					`lastUpdate`,
 					`lastUpdate`,
 					priority,
 					priority,
+					`pathEntries`,
 					`httpAuth`,
 					`httpAuth`,
 					error,
 					error,
-					keep_history,
 					ttl,
 					ttl,
 					attributes
 					attributes
 				)
 				)
 				VALUES
 				VALUES
-				(?, ?, ?, ?, ?, ?, 10, ?, 0, ?, ?, ?)';
-		$stm = $this->bd->prepare($sql);
+				(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
+		$stm = $this->pdo->prepare($sql);
 
 
 		$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
 		$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
 		$valuesTmp['website'] = safe_ascii($valuesTmp['website']);
 		$valuesTmp['website'] = safe_ascii($valuesTmp['website']);
+		if (!isset($valuesTmp['pathEntries'])) {
+			$valuesTmp['pathEntries'] = '';
+		}
+		if (!isset($valuesTmp['attributes'])) {
+			$valuesTmp['attributes'] = [];
+		}
 
 
 		$values = array(
 		$values = array(
 			substr($valuesTmp['url'], 0, 511),
 			substr($valuesTmp['url'], 0, 511),
@@ -59,16 +64,18 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			substr($valuesTmp['website'], 0, 255),
 			substr($valuesTmp['website'], 0, 255),
 			mb_strcut($valuesTmp['description'], 0, 1023, 'UTF-8'),
 			mb_strcut($valuesTmp['description'], 0, 1023, 'UTF-8'),
 			$valuesTmp['lastUpdate'],
 			$valuesTmp['lastUpdate'],
+			isset($valuesTmp['priority']) ? intval($valuesTmp['priority']) : FreshRSS_Feed::PRIORITY_MAIN_STREAM,
+			mb_strcut($valuesTmp['pathEntries'], 0, 511, 'UTF-8'),
 			base64_encode($valuesTmp['httpAuth']),
 			base64_encode($valuesTmp['httpAuth']),
-			FreshRSS_Feed::KEEP_HISTORY_DEFAULT,
+			isset($valuesTmp['error']) ? intval($valuesTmp['error']) : 0,
 			isset($valuesTmp['ttl']) ? intval($valuesTmp['ttl']) : FreshRSS_Feed::TTL_DEFAULT,
 			isset($valuesTmp['ttl']) ? intval($valuesTmp['ttl']) : FreshRSS_Feed::TTL_DEFAULT,
-			isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
+			is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
 		);
 		);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
-			return $this->bd->lastInsertId('"' . $this->prefix . 'feed_id_seq"');
+			return $this->pdo->lastInsertId('`_feed_id_seq`');
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
 				return $this->addFeed($valuesTmp);
 				return $this->addFeed($valuesTmp);
 			}
 			}
@@ -129,13 +136,13 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			if ($key === 'httpAuth') {
 			if ($key === 'httpAuth') {
 				$valuesTmp[$key] = base64_encode($v);
 				$valuesTmp[$key] = base64_encode($v);
 			} elseif ($key === 'attributes') {
 			} elseif ($key === 'attributes') {
-				$valuesTmp[$key] = json_encode($v);
+				$valuesTmp[$key] = is_string($valuesTmp[$key]) ? $valuesTmp[$key] : json_encode($valuesTmp[$key], JSON_UNESCAPED_SLASHES);
 			}
 			}
 		}
 		}
 		$set = substr($set, 0, -2);
 		$set = substr($set, 0, -2);
 
 
-		$sql = 'UPDATE `' . $this->prefix . 'feed` SET ' . $set . ' WHERE id=?';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'UPDATE `_feed` SET ' . $set . ' WHERE id=?';
+		$stm = $this->pdo->prepare($sql);
 
 
 		foreach ($valuesTmp as $v) {
 		foreach ($valuesTmp as $v) {
 			$values[] = $v;
 			$values[] = $v;
@@ -145,7 +152,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
 				return $this->updateFeed($id, $valuesTmp);
 				return $this->updateFeed($id, $valuesTmp);
 			}
 			}
@@ -166,7 +173,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function updateLastUpdate($id, $inError = false, $mtime = 0) {	//See also updateCachedValue()
 	public function updateLastUpdate($id, $inError = false, $mtime = 0) {	//See also updateCachedValue()
-		$sql = 'UPDATE `' . $this->prefix . 'feed` '
+		$sql = 'UPDATE `_feed` '
 		     . 'SET `lastUpdate`=?, error=? '
 		     . 'SET `lastUpdate`=?, error=? '
 		     . 'WHERE id=?';
 		     . 'WHERE id=?';
 		$values = array(
 		$values = array(
@@ -174,12 +181,12 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$inError ? 1 : 0,
 			$inError ? 1 : 0,
 			$id,
 			$id,
 		);
 		);
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error updateLastUpdate: ' . $info[2]);
 			Minz_Log::error('SQL error updateLastUpdate: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -192,8 +199,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$newCat = $catDAO->getDefault();
 			$newCat = $catDAO->getDefault();
 		}
 		}
 
 
-		$sql = 'UPDATE `' . $this->prefix . 'feed` SET category=? WHERE category=?';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'UPDATE `_feed` SET category=? WHERE category=?';
+		$stm = $this->pdo->prepare($sql);
 
 
 		$values = array(
 		$values = array(
 			$newCat->id(),
 			$newCat->id(),
@@ -203,44 +210,54 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error changeCategory: ' . $info[2]);
 			Minz_Log::error('SQL error changeCategory: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
 	}
 	}
 
 
 	public function deleteFeed($id) {
 	public function deleteFeed($id) {
-		$sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE id=?';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'DELETE FROM `_feed` WHERE id=?';
+		$stm = $this->pdo->prepare($sql);
 
 
 		$values = array($id);
 		$values = array($id);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error deleteFeed: ' . $info[2]);
 			Minz_Log::error('SQL error deleteFeed: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
 	}
 	}
 	public function deleteFeedByCategory($id) {
 	public function deleteFeedByCategory($id) {
-		$sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE category=?';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'DELETE FROM `_feed` WHERE category=?';
+		$stm = $this->pdo->prepare($sql);
 
 
 		$values = array($id);
 		$values = array($id);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error deleteFeedByCategory: ' . $info[2]);
 			Minz_Log::error('SQL error deleteFeedByCategory: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
 	}
 	}
 
 
+	public function selectAll() {
+		$sql = 'SELECT id, url, category, name, website, description, `lastUpdate`, priority, '
+		     . '`pathEntries`, `httpAuth`, error, ttl, attributes '
+		     . 'FROM `_feed`';
+		$stm = $this->pdo->query($sql);
+		while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
+			yield $row;
+		}
+	}
+
 	public function searchById($id) {
 	public function searchById($id) {
-		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE id=?';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'SELECT * FROM `_feed` WHERE id=?';
+		$stm = $this->pdo->prepare($sql);
 
 
 		$values = array($id);
 		$values = array($id);
 
 
@@ -255,8 +272,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		}
 	}
 	}
 	public function searchByUrl($url) {
 	public function searchByUrl($url) {
-		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE url=?';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'SELECT * FROM `_feed` WHERE url=?';
+		$stm = $this->pdo->prepare($sql);
 
 
 		$values = array($url);
 		$values = array($url);
 
 
@@ -272,25 +289,21 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function listFeedsIds() {
 	public function listFeedsIds() {
-		$sql = 'SELECT id FROM `' . $this->prefix . 'feed`';
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$sql = 'SELECT id FROM `_feed`';
+		$stm = $this->pdo->query($sql);
 		return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 	}
 	}
 
 
 	public function listFeeds() {
 	public function listFeeds() {
-		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` ORDER BY name';
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
-
+		$sql = 'SELECT * FROM `_feed` ORDER BY name';
+		$stm = $this->pdo->query($sql);
 		return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
 		return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 	}
 
 
 	public function arrayFeedCategoryNames() {	//For API
 	public function arrayFeedCategoryNames() {	//For API
-		$sql = 'SELECT f.id, f.name, c.name as c_name FROM `' . $this->prefix . 'feed` f '
-		     . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category';
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$sql = 'SELECT f.id, f.name, c.name as c_name FROM `_feed` f '
+		     . 'INNER JOIN `_category` c ON c.id = f.category';
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$feedCategoryNames = array();
 		$feedCategoryNames = array();
 		foreach ($res as $line) {
 		foreach ($res as $line) {
@@ -307,17 +320,18 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 */
 	 */
 	public function listFeedsOrderUpdate($defaultCacheDuration = 3600, $limit = 0) {
 	public function listFeedsOrderUpdate($defaultCacheDuration = 3600, $limit = 0) {
 		$this->updateTTL();
 		$this->updateTTL();
-		$sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl, attributes '
-		     . 'FROM `' . $this->prefix . 'feed` '
+		$sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes '
+		     . 'FROM `_feed` '
 		     . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT
 		     . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT
-		     . ' AND `lastUpdate` < (' . (time() + 60) . '-(CASE WHEN ttl=' . FreshRSS_Feed::TTL_DEFAULT . ' THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ')
+		     . ' AND `lastUpdate` < (' . (time() + 60)
+			 . '-(CASE WHEN ttl=' . FreshRSS_Feed::TTL_DEFAULT . ' THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ')
 		     . 'ORDER BY `lastUpdate` '
 		     . 'ORDER BY `lastUpdate` '
 		     . ($limit < 1 ? '' : 'LIMIT ' . intval($limit));
 		     . ($limit < 1 ? '' : 'LIMIT ' . intval($limit));
-		$stm = $this->bd->prepare($sql);
-		if ($stm && $stm->execute()) {
+		$stm = $this->pdo->query($sql);
+		if ($stm !== false) {
 			return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
 			return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
 		} else {
 		} else {
-			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
 				return $this->listFeedsOrderUpdate($defaultCacheDuration);
 				return $this->listFeedsOrderUpdate($defaultCacheDuration);
 			}
 			}
@@ -327,8 +341,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function listByCategory($cat) {
 	public function listByCategory($cat) {
-		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE category=? ORDER BY name';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'SELECT * FROM `_feed` WHERE category=? ORDER BY name';
+		$stm = $this->pdo->prepare($sql);
 
 
 		$values = array($cat);
 		$values = array($cat);
 
 
@@ -338,8 +352,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function countEntries($id) {
 	public function countEntries($id) {
-		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=?';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'SELECT COUNT(*) AS count FROM `_entry` WHERE id_feed=?';
+		$stm = $this->pdo->prepare($sql);
 		$values = array($id);
 		$values = array($id);
 		$stm->execute($values);
 		$stm->execute($values);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
@@ -348,8 +362,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function countNotRead($id) {
 	public function countNotRead($id) {
-		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND is_read=0';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'SELECT COUNT(*) AS count FROM `_entry` WHERE id_feed=? AND is_read=0';
+		$stm = $this->pdo->prepare($sql);
 		$values = array($id);
 		$values = array($id);
 		$stm->execute($values);
 		$stm->execute($values);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
@@ -357,62 +371,51 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $res[0]['count'];
 		return $res[0]['count'];
 	}
 	}
 
 
-	public function updateCachedValue($id) {	//For multiple feeds, call updateCachedValues()
-		$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) '
-		     . 'WHERE id=?';
-		$values = array($id);
-		$stm = $this->bd->prepare($sql);
-
-		if ($stm && $stm->execute($values)) {
-			return $stm->rowCount();
-		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::error('SQL error updateCachedValue: ' . $info[2]);
-			return false;
+	public function updateCachedValues($id = null) {
+		//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
+		$sql = 'UPDATE `_feed` '
+		     . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `_entry` e1 WHERE e1.id_feed=`_feed`.id),'
+		     . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `_entry` e2 WHERE e2.id_feed=`_feed`.id AND e2.is_read=0)'
+		     . ($id != null ? ' WHERE id=:id' : '');
+		$stm = $this->pdo->prepare($sql);
+		if ($id != null) {
+			$stm->bindParam(':id', $id, PDO::PARAM_INT);
 		}
 		}
-	}
 
 
-	public function updateCachedValues() {	//For one single feed, call updateCachedValue($id)
-		$sql = 'UPDATE `' . $this->prefix . 'feed` '
-		     . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
-		     . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
-		$stm = $this->bd->prepare($sql);
 		if ($stm && $stm->execute()) {
 		if ($stm && $stm->execute()) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::error('SQL error updateCachedValues: ' . $info[2]);
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
+			Minz_Log::error('SQL error updateCachedValue: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
 	}
 	}
 
 
 	public function truncate($id) {
 	public function truncate($id) {
-		$sql = 'DELETE FROM `' . $this->prefix . 'entry` WHERE id_feed=?';
-		$stm = $this->bd->prepare($sql);
-		$values = array($id);
-		$this->bd->beginTransaction();
-		if (!($stm && $stm->execute($values))) {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+		$sql = 'DELETE FROM `_entry` WHERE id_feed=:id';
+		$stm = $this->pdo->prepare($sql);
+		$stm->bindParam(':id', $id, PDO::PARAM_INT);
+		$this->pdo->beginTransaction();
+		if (!($stm && $stm->execute())) {
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error truncate: ' . $info[2]);
 			Minz_Log::error('SQL error truncate: ' . $info[2]);
-			$this->bd->rollBack();
+			$this->pdo->rollBack();
 			return false;
 			return false;
 		}
 		}
 		$affected = $stm->rowCount();
 		$affected = $stm->rowCount();
 
 
-		$sql = 'UPDATE `' . $this->prefix . 'feed` '
-			 . 'SET `cache_nbEntries`=0, `cache_nbUnreads`=0 WHERE id=?';
-		$values = array($id);
-		$stm = $this->bd->prepare($sql);
-		if (!($stm && $stm->execute($values))) {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+		$sql = 'UPDATE `_feed` '
+			 . 'SET `cache_nbEntries`=0, `cache_nbUnreads`=0 WHERE id=:id';
+		$stm = $this->pdo->prepare($sql);
+		$stm->bindParam(':id', $id, PDO::PARAM_INT);
+		if (!($stm && $stm->execute())) {
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error truncate: ' . $info[2]);
 			Minz_Log::error('SQL error truncate: ' . $info[2]);
-			$this->bd->rollBack();
+			$this->pdo->rollBack();
 			return false;
 			return false;
 		}
 		}
 
 
-		$this->bd->commit();
+		$this->pdo->commit();
 		return $affected;
 		return $affected;
 	}
 	}
 
 
@@ -446,7 +449,6 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$myFeed->_pathEntries(isset($dao['pathEntries']) ? $dao['pathEntries'] : '');
 			$myFeed->_pathEntries(isset($dao['pathEntries']) ? $dao['pathEntries'] : '');
 			$myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode($dao['httpAuth']) : '');
 			$myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode($dao['httpAuth']) : '');
 			$myFeed->_error(isset($dao['error']) ? $dao['error'] : 0);
 			$myFeed->_error(isset($dao['error']) ? $dao['error'] : 0);
-			$myFeed->_keepHistory(isset($dao['keep_history']) ? $dao['keep_history'] : FreshRSS_Feed::KEEP_HISTORY_DEFAULT);
 			$myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : FreshRSS_Feed::TTL_DEFAULT);
 			$myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : FreshRSS_Feed::TTL_DEFAULT);
 			$myFeed->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : '');
 			$myFeed->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : '');
 			$myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0);
 			$myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0);
@@ -461,20 +463,16 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function updateTTL() {
 	public function updateTTL() {
-		$sql = <<<SQL
-UPDATE `{$this->prefix}feed`
-   SET ttl = :new_value
- WHERE ttl = :old_value
-SQL;
-		$stm = $this->bd->prepare($sql);
+		$sql = 'UPDATE `_feed` SET ttl=:new_value WHERE ttl=:old_value';
+		$stm = $this->pdo->prepare($sql);
 		if (!($stm && $stm->execute(array(':new_value' => FreshRSS_Feed::TTL_DEFAULT, ':old_value' => -2)))) {
 		if (!($stm && $stm->execute(array(':new_value' => FreshRSS_Feed::TTL_DEFAULT, ':old_value' => -2)))) {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL warning updateTTL 1: ' . $info[2] . ' ' . $sql);
 			Minz_Log::error('SQL warning updateTTL 1: ' . $info[2] . ' ' . $sql);
 
 
-			$sql2 = 'ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN ttl INT NOT NULL DEFAULT ' . FreshRSS_Feed::TTL_DEFAULT;	//v0.7.3
-			$stm = $this->bd->prepare($sql2);
-			if (!($stm && $stm->execute())) {
-				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$sql2 = 'ALTER TABLE `_feed` ADD COLUMN ttl INT NOT NULL DEFAULT ' . FreshRSS_Feed::TTL_DEFAULT;	//v0.7.3
+			$stm = $this->pdo->query($sql2);
+			if ($stm === false) {
+				$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 				Minz_Log::error('SQL error updateTTL 2: ' . $info[2] . ' ' . $sql2);
 				Minz_Log::error('SQL error updateTTL 2: ' . $info[2] . ' ' . $sql2);
 			}
 			}
 		} else {
 		} else {

+ 2 - 2
app/Models/FeedDAOSQLite.php

@@ -3,9 +3,9 @@
 class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
 class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
 
 
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
-		if ($tableInfo = $this->bd->query("PRAGMA table_info('feed')")) {
+		if ($tableInfo = $this->pdo->query("PRAGMA table_info('feed')")) {
 			$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
 			$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
-			foreach (array('attributes') as $column) {
+			foreach (['attributes'] as $column) {
 				if (!in_array($column, $columns)) {
 				if (!in_array($column, $columns)) {
 					return $this->addColumn($column);
 					return $this->addColumn($column);
 				}
 				}

+ 16 - 31
app/Models/StatsDAO.php

@@ -45,13 +45,11 @@ SELECT COUNT(1) AS total,
 COUNT(1) - SUM(e.is_read) AS count_unreads,
 COUNT(1) - SUM(e.is_read) AS count_unreads,
 SUM(e.is_read) AS count_reads,
 SUM(e.is_read) AS count_reads,
 SUM(e.is_favorite) AS count_favorites
 SUM(e.is_favorite) AS count_favorites
-FROM `{$this->prefix}entry` AS e
-, `{$this->prefix}feed` AS f
+FROM `_entry` AS e, `_feed` AS f
 WHERE e.id_feed = f.id
 WHERE e.id_feed = f.id
 {$filter}
 {$filter}
 SQL;
 SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 
 		return $res[0];
 		return $res[0];
@@ -73,13 +71,12 @@ SQL;
 		$sql = <<<SQL
 		$sql = <<<SQL
 SELECT {$sqlDay} AS day,
 SELECT {$sqlDay} AS day,
 COUNT(*) as count
 COUNT(*) as count
-FROM `{$this->prefix}entry`
+FROM `_entry`
 WHERE date >= {$oldest} AND date < {$midnight}
 WHERE date >= {$oldest} AND date < {$midnight}
 GROUP BY day
 GROUP BY day
 ORDER BY day ASC
 ORDER BY day ASC
 SQL;
 SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 
 		foreach ($res as $value) {
 		foreach ($res as $value) {
@@ -143,14 +140,13 @@ 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 `_entry` AS e
 {$restrict}
 {$restrict}
 GROUP BY period
 GROUP BY period
 ORDER BY period ASC
 ORDER BY period ASC
 SQL;
 SQL;
 
 
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 
 
 		$repartition = array();
 		$repartition = array();
@@ -207,11 +203,10 @@ 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 `_entry` AS e
 {$restrict}
 {$restrict}
 SQL;
 SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetch(PDO::FETCH_NAMED);
 		$res = $stm->fetch(PDO::FETCH_NAMED);
 		$date_min = new \DateTime();
 		$date_min = new \DateTime();
 		$date_min->setTimestamp($res['date_min']);
 		$date_min->setTimestamp($res['date_min']);
@@ -251,14 +246,12 @@ 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 `_category` AS c, `_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
 SQL;
 SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 
 		return $res;
 		return $res;
@@ -274,16 +267,13 @@ 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 `_category` AS c, `_feed` AS f, `_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
 ORDER BY data DESC
 ORDER BY data DESC
 SQL;
 SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 
 		return $res;
 		return $res;
@@ -300,17 +290,14 @@ 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 `_category` AS c, `_feed` AS f, `_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
 ORDER BY count DESC
 ORDER BY count DESC
 LIMIT 10
 LIMIT 10
 SQL;
 SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		return $stm->fetchAll(PDO::FETCH_ASSOC);
 		return $stm->fetchAll(PDO::FETCH_ASSOC);
 	}
 	}
 
 
@@ -325,14 +312,12 @@ 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 `_feed` AS f, `_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
 SQL;
 SQL;
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		return $stm->fetchAll(PDO::FETCH_ASSOC);
 		return $stm->fetchAll(PDO::FETCH_ASSOC);
 	}
 	}
 
 

+ 2 - 3
app/Models/StatsDAOPGSQL.php

@@ -47,14 +47,13 @@ class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
 		$sql = <<<SQL
 		$sql = <<<SQL
 SELECT extract( {$period} from to_timestamp(e.date)) AS period
 SELECT extract( {$period} from to_timestamp(e.date)) AS period
 , COUNT(1) AS count
 , COUNT(1) AS count
-FROM "{$this->prefix}entry" AS e
+FROM `_entry` AS e
 {$restrict}
 {$restrict}
 GROUP BY period
 GROUP BY period
 ORDER BY period ASC
 ORDER BY period ASC
 SQL;
 SQL;
 
 
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 
 
 		foreach ($res as $value) {
 		foreach ($res as $value) {

+ 2 - 3
app/Models/StatsDAOSQLite.php

@@ -15,14 +15,13 @@ class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
 		$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 `_entry` AS e
 {$restrict}
 {$restrict}
 GROUP BY period
 GROUP BY period
 ORDER BY period ASC
 ORDER BY period ASC
 SQL;
 SQL;
 
 
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
+		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 		$res = $stm->fetchAll(PDO::FETCH_NAMED);
 
 
 		$repartition = array();
 		$repartition = array();

+ 1 - 1
app/Models/Tag.php

@@ -3,7 +3,7 @@
 class FreshRSS_Tag extends Minz_Model {
 class FreshRSS_Tag extends Minz_Model {
 	private $id = 0;
 	private $id = 0;
 	private $name;
 	private $name;
-	private $attributes = array();
+	private $attributes = [];
 	private $nbEntries = -1;
 	private $nbEntries = -1;
 	private $nbUnread = -1;
 	private $nbUnread = -1;
 
 

+ 74 - 65
app/Models/TagDAO.php

@@ -8,37 +8,24 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 	public function createTagTable() {
 	public function createTagTable() {
 		$ok = false;
 		$ok = false;
-		$hadTransaction = $this->bd->inTransaction();
+		$hadTransaction = $this->pdo->inTransaction();
 		if ($hadTransaction) {
 		if ($hadTransaction) {
-			$this->bd->commit();
+			$this->pdo->commit();
 		}
 		}
 		try {
 		try {
-			$db = FreshRSS_Context::$system_conf->db;
-			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+			require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
 
 
 			Minz_Log::warning('SQL ALTER GUID case sensitivity...');
 			Minz_Log::warning('SQL ALTER GUID case sensitivity...');
 			$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
 			$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
 			$databaseDAO->ensureCaseInsensitiveGuids();
 			$databaseDAO->ensureCaseInsensitiveGuids();
 
 
 			Minz_Log::warning('SQL CREATE TABLE tag...');
 			Minz_Log::warning('SQL CREATE TABLE tag...');
-			if (defined('SQL_CREATE_TABLE_TAGS')) {
-				$sql = sprintf(SQL_CREATE_TABLE_TAGS, $this->prefix);
-				$stm = $this->bd->prepare($sql);
-				$ok = $stm && $stm->execute();
-			} else {
-				global $SQL_CREATE_TABLE_TAGS;
-				$ok = !empty($SQL_CREATE_TABLE_TAGS);
-				foreach ($SQL_CREATE_TABLE_TAGS as $instruction) {
-					$sql = sprintf($instruction, $this->prefix);
-					$stm = $this->bd->prepare($sql);
-					$ok &= $stm && $stm->execute();
-				}
-			}
+			$ok = $this->pdo->exec($SQL_CREATE_TABLE_TAGS) !== false;
 		} catch (Exception $e) {
 		} catch (Exception $e) {
 			Minz_Log::error('FreshRSS_EntryDAO::createTagTable error: ' . $e->getMessage());
 			Minz_Log::error('FreshRSS_EntryDAO::createTagTable error: ' . $e->getMessage());
 		}
 		}
 		if ($hadTransaction) {
 		if ($hadTransaction) {
-			$this->bd->beginTransaction();
+			$this->pdo->beginTransaction();
 		}
 		}
 		return $ok;
 		return $ok;
 	}
 	}
@@ -55,22 +42,25 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function addTag($valuesTmp) {
 	public function addTag($valuesTmp) {
-		$sql = 'INSERT INTO `' . $this->prefix . 'tag`(name, attributes) '
-		     . 'SELECT * FROM (SELECT TRIM(?), TRIM(?)) t2 '	//TRIM() to provide a type hint as text for PostgreSQL
-		     . 'WHERE NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'category` WHERE name = TRIM(?))';	//No category of the same name
-		$stm = $this->bd->prepare($sql);
+		$sql = 'INSERT INTO `_tag`(name, attributes) '
+		     . 'SELECT * FROM (SELECT TRIM(?) as name, TRIM(?) as attributes) t2 '	//TRIM() gives a text type hint to PostgreSQL
+		     . 'WHERE NOT EXISTS (SELECT 1 FROM `_category` WHERE name = TRIM(?))';	//No category of the same name
+		$stm = $this->pdo->prepare($sql);
 
 
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8');
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8');
+		if (!isset($valuesTmp['attributes'])) {
+			$valuesTmp['attributes'] = [];
+		}
 		$values = array(
 		$values = array(
 			$valuesTmp['name'],
 			$valuesTmp['name'],
-			isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
+			is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
 			$valuesTmp['name'],
 			$valuesTmp['name'],
 		);
 		);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
-			return $this->bd->lastInsertId('"' . $this->prefix . 'tag_id_seq"');
+			return $this->pdo->lastInsertId('`_tag_id_seq`');
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error addTag: ' . $info[2]);
 			Minz_Log::error('SQL error addTag: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -89,14 +79,17 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function updateTag($id, $valuesTmp) {
 	public function updateTag($id, $valuesTmp) {
-		$sql = 'UPDATE `' . $this->prefix . 'tag` SET name=?, attributes=? WHERE id=? '
-		     . 'AND NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'category` WHERE name = ?)';	//No category of the same name
-		$stm = $this->bd->prepare($sql);
+		$sql = 'UPDATE `_tag` SET name=?, attributes=? WHERE id=? '
+		     . 'AND NOT EXISTS (SELECT 1 FROM `_category` WHERE name = ?)';	//No category of the same name
+		$stm = $this->pdo->prepare($sql);
 
 
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8');
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8');
+		if (!isset($valuesTmp['attributes'])) {
+			$valuesTmp['attributes'] = [];
+		}
 		$values = array(
 		$values = array(
 			$valuesTmp['name'],
 			$valuesTmp['name'],
-			isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
+			is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
 			$id,
 			$id,
 			$valuesTmp['name'],
 			$valuesTmp['name'],
 		);
 		);
@@ -104,7 +97,7 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error updateTag: ' . $info[2]);
 			Minz_Log::error('SQL error updateTag: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -125,23 +118,39 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if ($id <= 0) {
 		if ($id <= 0) {
 			return false;
 			return false;
 		}
 		}
-		$sql = 'DELETE FROM `' . $this->prefix . 'tag` WHERE id=?';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'DELETE FROM `_tag` WHERE id=?';
+		$stm = $this->pdo->prepare($sql);
 
 
 		$values = array($id);
 		$values = array($id);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error deleteTag: ' . $info[2]);
 			Minz_Log::error('SQL error deleteTag: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
 	}
 	}
 
 
+	public function selectAll() {
+		$sql = 'SELECT id, name, attributes FROM `_tag`';
+		$stm = $this->pdo->query($sql);
+		while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
+			yield $row;
+		}
+	}
+
+	public function selectEntryTag() {
+		$sql = 'SELECT id_tag, id_entry FROM `_entrytag`';
+		$stm = $this->pdo->query($sql);
+		while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
+			yield $row;
+		}
+	}
+
 	public function searchById($id) {
 	public function searchById($id) {
-		$sql = 'SELECT * FROM `' . $this->prefix . 'tag` WHERE id=?';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'SELECT * FROM `_tag` WHERE id=?';
+		$stm = $this->pdo->prepare($sql);
 		$values = array($id);
 		$values = array($id);
 		$stm->execute($values);
 		$stm->execute($values);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
@@ -150,8 +159,8 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function searchByName($name) {
 	public function searchByName($name) {
-		$sql = 'SELECT * FROM `' . $this->prefix . 'tag` WHERE name=?';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'SELECT * FROM `_tag` WHERE name=?';
+		$stm = $this->pdo->prepare($sql);
 		$values = array($name);
 		$values = array($name);
 		$stm->execute($values);
 		$stm->execute($values);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
@@ -162,20 +171,20 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	public function listTags($precounts = false) {
 	public function listTags($precounts = false) {
 		if ($precounts) {
 		if ($precounts) {
 			$sql = 'SELECT t.id, t.name, count(e.id) AS unreads '
 			$sql = 'SELECT t.id, t.name, count(e.id) AS unreads '
-				 . 'FROM `' . $this->prefix . 'tag` t '
-				 . 'LEFT OUTER JOIN `' . $this->prefix . 'entrytag` et ON et.id_tag = t.id '
-				 . 'LEFT OUTER JOIN `' . $this->prefix . 'entry` e ON et.id_entry = e.id AND e.is_read = 0 '
+				 . 'FROM `_tag` t '
+				 . 'LEFT OUTER JOIN `_entrytag` et ON et.id_tag = t.id '
+				 . 'LEFT OUTER JOIN `_entry` e ON et.id_entry = e.id AND e.is_read = 0 '
 				 . 'GROUP BY t.id '
 				 . 'GROUP BY t.id '
 				 . 'ORDER BY t.name';
 				 . 'ORDER BY t.name';
 		} else {
 		} else {
-			$sql = 'SELECT * FROM `' . $this->prefix . 'tag` ORDER BY name';
+			$sql = 'SELECT * FROM `_tag` ORDER BY name';
 		}
 		}
 
 
-		$stm = $this->bd->prepare($sql);
-		if ($stm && $stm->execute()) {
+		$stm = $this->pdo->query($sql);
+		if ($stm !== false) {
 			return self::daoToTag($stm->fetchAll(PDO::FETCH_ASSOC));
 			return self::daoToTag($stm->fetchAll(PDO::FETCH_ASSOC));
 		} else {
 		} else {
-			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
 				return $this->listTags($precounts);
 				return $this->listTags($precounts);
 			}
 			}
@@ -185,13 +194,13 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function count() {
 	public function count() {
-		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'tag`';
-		$stm = $this->bd->prepare($sql);
-		if ($stm && $stm->execute()) {
+		$sql = 'SELECT COUNT(*) AS count FROM `_tag`';
+		$stm = $this->pdo->query($sql);
+		if ($stm !== false) {
 			$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 			$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 			return $res[0]['count'];
 			return $res[0]['count'];
 		} else {
 		} else {
-			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
 				return $this->count();
 				return $this->count();
 			}
 			}
@@ -201,8 +210,8 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function countEntries($id) {
 	public function countEntries($id) {
-		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entrytag` WHERE id_tag=?';
-		$stm = $this->bd->prepare($sql);
+		$sql = 'SELECT COUNT(*) AS count FROM `_entrytag` WHERE id_tag=?';
+		$stm = $this->pdo->prepare($sql);
 		$values = array($id);
 		$values = array($id);
 		$stm->execute($values);
 		$stm->execute($values);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
@@ -210,10 +219,10 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function countNotRead($id) {
 	public function countNotRead($id) {
-		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entrytag` et '
-			 . 'INNER JOIN `' . $this->prefix . 'entry` e ON et.id_entry=e.id '
+		$sql = 'SELECT COUNT(*) AS count FROM `_entrytag` et '
+			 . 'INNER JOIN `_entry` e ON et.id_entry=e.id '
 			 . 'WHERE et.id_tag=? AND e.is_read=0';
 			 . 'WHERE et.id_tag=? AND e.is_read=0';
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 		$values = array($id);
 		$values = array($id);
 		$stm->execute($values);
 		$stm->execute($values);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
@@ -222,17 +231,17 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 	public function tagEntry($id_tag, $id_entry, $checked = true) {
 	public function tagEntry($id_tag, $id_entry, $checked = true) {
 		if ($checked) {
 		if ($checked) {
-			$sql = 'INSERT ' . $this->sqlIgnore() . ' INTO `' . $this->prefix . 'entrytag`(id_tag, id_entry) VALUES(?, ?)';
+			$sql = 'INSERT ' . $this->sqlIgnore() . ' INTO `_entrytag`(id_tag, id_entry) VALUES(?, ?)';
 		} else {
 		} else {
-			$sql = 'DELETE FROM `' . $this->prefix . 'entrytag` WHERE id_tag=? AND id_entry=?';
+			$sql = 'DELETE FROM `_entrytag` WHERE id_tag=? AND id_entry=?';
 		}
 		}
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 		$values = array($id_tag, $id_entry);
 		$values = array($id_tag, $id_entry);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return true;
 			return true;
 		} else {
 		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error tagEntry: ' . $info[2]);
 			Minz_Log::error('SQL error tagEntry: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
@@ -240,11 +249,11 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 	public function getTagsForEntry($id_entry) {
 	public function getTagsForEntry($id_entry) {
 		$sql = 'SELECT t.id, t.name, et.id_entry IS NOT NULL as checked '
 		$sql = 'SELECT t.id, t.name, et.id_entry IS NOT NULL as checked '
-			 . 'FROM `' . $this->prefix . 'tag` t '
-			 . 'LEFT OUTER JOIN `' . $this->prefix . 'entrytag` et ON et.id_tag = t.id AND et.id_entry=? '
+			 . 'FROM `_tag` t '
+			 . 'LEFT OUTER JOIN `_entrytag` et ON et.id_tag = t.id AND et.id_entry=? '
 			 . 'ORDER BY t.name';
 			 . 'ORDER BY t.name';
 
 
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 		$values = array($id_entry);
 		$values = array($id_entry);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
@@ -255,7 +264,7 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			}
 			}
 			return $lines;
 			return $lines;
 		} else {
 		} else {
-			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
 				return $this->getTagsForEntry($id_entry);
 				return $this->getTagsForEntry($id_entry);
 			}
 			}
@@ -266,8 +275,8 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 	public function getTagsForEntries($entries) {
 	public function getTagsForEntries($entries) {
 		$sql = 'SELECT et.id_entry, et.id_tag, t.name '
 		$sql = 'SELECT et.id_entry, et.id_tag, t.name '
-			 . 'FROM `' . $this->prefix . 'tag` t '
-			 . 'INNER JOIN `' . $this->prefix . 'entrytag` et ON et.id_tag = t.id';
+			 . 'FROM `_tag` t '
+			 . 'INNER JOIN `_entrytag` et ON et.id_tag = t.id';
 
 
 		$values = array();
 		$values = array();
 		if (is_array($entries) && count($entries) > 0) {
 		if (is_array($entries) && count($entries) > 0) {
@@ -286,12 +295,12 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 				}
 				}
 			}
 			}
 		}
 		}
-		$stm = $this->bd->prepare($sql);
+		$stm = $this->pdo->prepare($sql);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
 			return $stm->fetchAll(PDO::FETCH_ASSOC);
 			return $stm->fetchAll(PDO::FETCH_ASSOC);
 		} else {
 		} else {
-			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
 				return $this->getTagsForEntries($entries);
 				return $this->getTagsForEntries($entries);
 			}
 			}

+ 1 - 1
app/Models/TagDAOSQLite.php

@@ -7,7 +7,7 @@ class FreshRSS_TagDAOSQLite extends FreshRSS_TagDAO {
 	}
 	}
 
 
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
-		if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='tag'")) {
+		if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='tag'")) {
 			$showCreate = $tableInfo->fetchColumn();
 			$showCreate = $tableInfo->fetchColumn();
 			if (stripos($showCreate, 'tag') === false) {
 			if (stripos($showCreate, 'tag') === false) {
 				return $this->createTagTable();	//v1.12.0
 				return $this->createTagTable();	//v1.12.0

+ 29 - 60
app/Models/UserDAO.php

@@ -1,83 +1,52 @@
 <?php
 <?php
 
 
 class FreshRSS_UserDAO extends Minz_ModelPdo {
 class FreshRSS_UserDAO extends Minz_ModelPdo {
-	public function createUser($username, $new_user_language, $insertDefaultFeeds = true) {
-		$db = FreshRSS_Context::$system_conf->db;
-		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
-
-		$userPDO = new Minz_ModelPdo($username);
-
-		$currentLanguage = Minz_Translate::language();
+	public function createUser($insertDefaultFeeds = false) {
+		require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
 
 
 		try {
 		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 . SQL_CREATE_TABLE_ENTRYTMP . SQL_CREATE_TABLE_TAGS, $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, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS;
-				if (is_array($SQL_CREATE_TABLES)) {
-					$instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS);
-					$ok = !empty($instructions);
-					foreach ($instructions as $instruction) {
-						$sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category'));
-						$stm = $userPDO->bd->prepare($sql);
-						$ok &= ($stm && $stm->execute());
-					}
-				}
-			}
+			$sql = $SQL_CREATE_TABLES . $SQL_CREATE_TABLE_ENTRYTMP . $SQL_CREATE_TABLE_TAGS;
+			$ok = $this->pdo->exec($sql) !== false;	//Note: Only exec() can take multiple statements safely.
 			if ($ok && $insertDefaultFeeds) {
 			if ($ok && $insertDefaultFeeds) {
-				if (defined('SQL_INSERT_FEEDS')) {	//E.g. MySQL
-					$sql = sprintf(SQL_INSERT_FEEDS, $bd_prefix_user);
-					$stm = $userPDO->bd->prepare($sql);
-					$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());
-						}
-					}
+				$default_feeds = FreshRSS_Context::$system_conf->default_feeds;
+				$stm = $this->pdo->prepare($SQL_INSERT_FEED);
+				foreach ($default_feeds as $feed) {
+					$parameters = [
+						':url' => $feed['url'],
+						':name' => $feed['name'],
+						':website' => $feed['website'],
+						':description' => $feed['description'],
+					];
+					$ok &= ($stm && $stm->execute($parameters));
 				}
 				}
 			}
 			}
 		} catch (Exception $e) {
 		} catch (Exception $e) {
-			Minz_Log::error('Error while creating user: ' . $e->getMessage());
+			Minz_Log::error('Error while creating database for 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();
-			Minz_Log::error('SQL error: ' . $info[2]);
+			$info = empty($stm) ? $this->pdo->errorInfo() : $stm->errorInfo();
+			Minz_Log::error(__METHOD__ . ' error: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
 	}
 	}
 
 
-	public function deleteUser($username) {
-		$db = FreshRSS_Context::$system_conf->db;
-		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+	public function deleteUser() {
+		if (defined('STDERR')) {
+			fwrite(STDERR, 'Deleting SQL data for user “' . $this->current_user . "”…\n");
+		}
+
+		require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
+		$ok = $this->pdo->exec($SQL_DROP_TABLES) !== false;
 
 
-		if ($db['type'] === 'sqlite') {
-			return unlink(USERS_PATH . '/' . $username . '/db.sqlite');
+		if ($ok) {
+			return true;
 		} else {
 		} else {
-			$userPDO = new Minz_ModelPdo($username);
-
-			$sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_');
-			$stm = $userPDO->bd->prepare($sql);
-			if ($stm && $stm->execute()) {
-				return true;
-			} else {
-				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-				Minz_Log::error('SQL error : ' . $info[2]);
-				return false;
-			}
+			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
+			Minz_Log::error(__METHOD__ . ' error: ' . $info[2]);
+			return false;
 		}
 		}
 	}
 	}
 
 

+ 44 - 60
app/SQL/install.sql.mysql.php

@@ -1,20 +1,23 @@
 <?php
 <?php
-define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS `%1$s` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+$SQL_CREATE_DB = <<<'SQL'
+CREATE DATABASE IF NOT EXISTS `%1$s` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+SQL;
 
 
-define('SQL_CREATE_TABLES', '
-CREATE TABLE IF NOT EXISTS `%1$scategory` (
+$SQL_CREATE_TABLES = <<<'SQL'
+CREATE TABLE IF NOT EXISTS `_category` (
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
-	`name` VARCHAR(' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') NOT NULL,	-- Max index length for Unicode is 191 characters (767 bytes)
+	`name` VARCHAR(191) NOT NULL,	-- Max index length for Unicode is 191 characters (767 bytes) FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE
+	`attributes` TEXT,	-- v1.15.0
 	PRIMARY KEY (`id`),
 	PRIMARY KEY (`id`),
 	UNIQUE KEY (`name`)	-- v0.7
 	UNIQUE KEY (`name`)	-- v0.7
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ENGINE = INNODB;
 ENGINE = INNODB;
 
 
-CREATE TABLE IF NOT EXISTS `%1$sfeed` (
+CREATE TABLE IF NOT EXISTS `_feed` (
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
 	`url` VARCHAR(511) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
 	`url` VARCHAR(511) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
 	`category` SMALLINT DEFAULT 0,	-- v0.7
 	`category` SMALLINT DEFAULT 0,	-- v0.7
-	`name` VARCHAR(' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') NOT NULL,
+	`name` VARCHAR(191) NOT NULL,
 	`website` VARCHAR(255) CHARACTER SET latin1 COLLATE latin1_bin,
 	`website` VARCHAR(255) CHARACTER SET latin1 COLLATE latin1_bin,
 	`description` TEXT,
 	`description` TEXT,
 	`lastUpdate` INT(11) DEFAULT 0,	-- Until year 2038
 	`lastUpdate` INT(11) DEFAULT 0,	-- Until year 2038
@@ -22,26 +25,24 @@ CREATE TABLE IF NOT EXISTS `%1$sfeed` (
 	`pathEntries` VARCHAR(511) DEFAULT NULL,
 	`pathEntries` VARCHAR(511) DEFAULT NULL,
 	`httpAuth` VARCHAR(511) DEFAULT NULL,
 	`httpAuth` VARCHAR(511) DEFAULT NULL,
 	`error` BOOLEAN DEFAULT 0,
 	`error` BOOLEAN DEFAULT 0,
-	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,	-- v0.7
 	`ttl` INT NOT NULL DEFAULT 0,	-- v0.7.3
 	`ttl` INT NOT NULL DEFAULT 0,	-- v0.7.3
 	`attributes` TEXT,	-- v1.11.0
 	`attributes` TEXT,	-- v1.11.0
 	`cache_nbEntries` INT DEFAULT 0,	-- v0.7
 	`cache_nbEntries` INT DEFAULT 0,	-- v0.7
 	`cache_nbUnreads` INT DEFAULT 0,	-- v0.7
 	`cache_nbUnreads` INT DEFAULT 0,	-- v0.7
 	PRIMARY KEY (`id`),
 	PRIMARY KEY (`id`),
-	FOREIGN KEY (`category`) 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 KEY (`url`),	-- v0.7
 	UNIQUE KEY (`url`),	-- v0.7
 	INDEX (`name`),	-- v0.7
 	INDEX (`name`),	-- v0.7
-	INDEX (`priority`),	-- v0.7
-	INDEX (`keep_history`)	-- v0.7
+	INDEX (`priority`)	-- v0.7
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ENGINE = INNODB;
 ENGINE = INNODB;
 
 
-CREATE TABLE IF NOT EXISTS `%1$sentry` (
+CREATE TABLE IF NOT EXISTS `_entry` (
 	`id` BIGINT NOT NULL,	-- v0.7
 	`id` BIGINT NOT NULL,	-- v0.7
 	`guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,	-- Maximum for UNIQUE is 767B
 	`guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,	-- Maximum for UNIQUE is 767B
 	`title` VARCHAR(255) NOT NULL,
 	`title` VARCHAR(255) NOT NULL,
 	`author` VARCHAR(255),
 	`author` VARCHAR(255),
-	`content_bin` BLOB,	-- v0.7
+	`content_bin` MEDIUMBLOB,	-- v0.7
 	`link` VARCHAR(1023) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
 	`link` VARCHAR(1023) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
 	`date` INT(11),	-- Until year 2038
 	`date` INT(11),	-- Until year 2038
 	`lastSeen` INT(11) DEFAULT 0,	-- v1.1.1, Until year 2038
 	`lastSeen` INT(11) DEFAULT 0,	-- v1.1.1, Until year 2038
@@ -51,25 +52,29 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` (
 	`id_feed` SMALLINT,	-- v0.7
 	`id_feed` SMALLINT,	-- v0.7
 	`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 KEY (`id_feed`,`guid`),	-- v0.7
 	UNIQUE KEY (`id_feed`,`guid`),	-- v0.7
 	INDEX (`is_favorite`),	-- v0.7
 	INDEX (`is_favorite`),	-- v0.7
 	INDEX (`is_read`),	-- v0.7
 	INDEX (`is_read`),	-- v0.7
-	INDEX `entry_lastSeen_index` (`lastSeen`)	-- v1.1.1
-	-- INDEX `entry_feed_read_index` (`id_feed`,`is_read`)	-- v1.7 Located futher down
+	INDEX `entry_lastSeen_index` (`lastSeen`),	-- v1.1.1
+	INDEX `entry_feed_read_index` (`id_feed`,`is_read`)	-- v1.7
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ENGINE = INNODB;
 ENGINE = INNODB;
 
 
-INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
-');
+INSERT IGNORE INTO `_category` (id, name) VALUES(1, "Uncategorized");
+SQL;
 
 
-define('SQL_CREATE_TABLE_ENTRYTMP', '
-CREATE TABLE IF NOT EXISTS `%1$sentrytmp` (	-- v1.7
+$SQL_CREATE_INDEX_ENTRY_1 = <<<'SQL'
+CREATE INDEX `entry_feed_read_index` ON `_entry` (`id_feed`,`is_read`);	-- v1.7
+SQL;
+
+$SQL_CREATE_TABLE_ENTRYTMP = <<<'SQL'
+CREATE TABLE IF NOT EXISTS `_entrytmp` (	-- v1.7
 	`id` BIGINT NOT NULL,
 	`id` BIGINT NOT NULL,
 	`guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
 	`guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
 	`title` VARCHAR(255) NOT NULL,
 	`title` VARCHAR(255) NOT NULL,
 	`author` VARCHAR(255),
 	`author` VARCHAR(255),
-	`content_bin` BLOB,
+	`content_bin` MEDIUMBLOB,
 	`link` VARCHAR(1023) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
 	`link` VARCHAR(1023) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
 	`date` INT(11),
 	`date` INT(11),
 	`lastSeen` INT(11) DEFAULT 0,
 	`lastSeen` INT(11) DEFAULT 0,
@@ -79,17 +84,15 @@ CREATE TABLE IF NOT EXISTS `%1$sentrytmp` (	-- v1.7
 	`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 KEY (`id_feed`,`guid`),
 	UNIQUE KEY (`id_feed`,`guid`),
 	INDEX (`date`)
 	INDEX (`date`)
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ENGINE = INNODB;
 ENGINE = INNODB;
+SQL;
 
 
-CREATE INDEX `entry_feed_read_index` ON `%1$sentry`(`id_feed`,`is_read`);	-- v1.7 Located here to be auto-added
-');
-
-define('SQL_CREATE_TABLE_TAGS', '
-CREATE TABLE IF NOT EXISTS `%1$stag` (	-- v1.12
+$SQL_CREATE_TABLE_TAGS = <<<'SQL'
+CREATE TABLE IF NOT EXISTS `_tag` (	-- v1.12
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,
 	`name` VARCHAR(63) NOT NULL,
 	`name` VARCHAR(63) NOT NULL,
 	`attributes` TEXT,
 	`attributes` TEXT,
@@ -98,46 +101,27 @@ CREATE TABLE IF NOT EXISTS `%1$stag` (	-- v1.12
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ENGINE = INNODB;
 ENGINE = INNODB;
 
 
-CREATE TABLE IF NOT EXISTS `%1$sentrytag` (	-- v1.12
+CREATE TABLE IF NOT EXISTS `_entrytag` (	-- v1.12
 	`id_tag` SMALLINT,
 	`id_tag` SMALLINT,
 	`id_entry` BIGINT,
 	`id_entry` BIGINT,
 	PRIMARY KEY (`id_tag`,`id_entry`),
 	PRIMARY KEY (`id_tag`,`id_entry`),
-	FOREIGN KEY (`id_tag`) REFERENCES `%1$stag`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
-	FOREIGN KEY (`id_entry`) REFERENCES `%1$sentry`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+	FOREIGN KEY (`id_tag`) REFERENCES `_tag`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+	FOREIGN KEY (`id_entry`) REFERENCES `_entry`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	INDEX (`id_entry`)
 	INDEX (`id_entry`)
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ENGINE = INNODB;
 ENGINE = INNODB;
-');
-
-define('SQL_INSERT_FEEDS', '
-INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "https://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);
-');
-
-define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `%1$sentrytag`, `%1$stag`, `%1$sentrytmp`, `%1$sentry`, `%1$sfeed`, `%1$scategory`');
-
-define('SQL_UPDATE_UTF8MB4', '
-ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;	-- v1.5.0
-
-ALTER TABLE `%1$scategory` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-UPDATE `%1$scategory` SET name=SUBSTRING(name,1,' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') WHERE LENGTH(name) > ' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ';
-ALTER TABLE `%1$scategory` MODIFY `name` VARCHAR(' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
-OPTIMIZE TABLE `%1$scategory`;
+SQL;
 
 
-ALTER TABLE `%1$sfeed` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-UPDATE `%1$sfeed` SET name=SUBSTRING(name,1,' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') WHERE LENGTH(name) > ' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ';
-ALTER TABLE `%1$sfeed` MODIFY `name` VARCHAR(' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
-ALTER TABLE `%1$sfeed` MODIFY `description` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-OPTIMIZE TABLE `%1$sfeed`;
+$SQL_INSERT_FEED = <<<'SQL'
+INSERT IGNORE INTO `_feed` (url, category, name, website, description, ttl)
+	VALUES(:url, 1, :name, :website, :description, 86400);
+SQL;
 
 
-ALTER TABLE `%1$sentry` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-ALTER TABLE `%1$sentry` MODIFY `title` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
-ALTER TABLE `%1$sentry` MODIFY `author` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-ALTER TABLE `%1$sentry` MODIFY `tags` VARCHAR(1023) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-OPTIMIZE TABLE `%1$sentry`;
-');
+$SQL_DROP_TABLES = <<<'SQL'
+DROP TABLE IF EXISTS `_entrytag`, `_tag`, `_entrytmp`, `_entry`, `_feed`, `_category`;
+SQL;
 
 
-define('SQL_UPDATE_GUID_LATIN1_BIN', '	-- v1.12
-ALTER TABLE `%1$sentrytmp` MODIFY `guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL;
-ALTER TABLE `%1$sentry` MODIFY `guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL;
-');
+$SQL_UPDATE_GUID_LATIN1_BIN = <<<'SQL'
+ALTER TABLE `_entrytmp` MODIFY `guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL;	-- v1.12
+ALTER TABLE `_entry` MODIFY `guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL;
+SQL;

+ 50 - 51
app/SQL/install.sql.pgsql.php

@@ -1,14 +1,16 @@
 <?php
 <?php
-define('SQL_CREATE_DB', 'CREATE DATABASE "%1$s" ENCODING \'UTF8\';');
+$SQL_CREATE_DB = <<<'SQL'
+CREATE DATABASE "%1$s" ENCODING 'UTF8';
+SQL;
 
 
-global $SQL_CREATE_TABLES;
-$SQL_CREATE_TABLES = array(
-'CREATE TABLE IF NOT EXISTS "%1$scategory" (
+$SQL_CREATE_TABLES = <<<'SQL'
+CREATE TABLE IF NOT EXISTS `_category` (
 	"id" SERIAL PRIMARY KEY,
 	"id" SERIAL PRIMARY KEY,
-	"name" VARCHAR(255) UNIQUE NOT NULL
-);',
+	"name" VARCHAR(255) UNIQUE NOT NULL,
+	"attributes" TEXT	-- v1.15.0
+);
 
 
-'CREATE TABLE IF NOT EXISTS "%1$sfeed" (
+CREATE TABLE IF NOT EXISTS `_feed` (
 	"id" SERIAL PRIMARY KEY,
 	"id" SERIAL PRIMARY KEY,
 	"url" VARCHAR(511) UNIQUE NOT NULL,
 	"url" VARCHAR(511) UNIQUE NOT NULL,
 	"category" SMALLINT DEFAULT 0,
 	"category" SMALLINT DEFAULT 0,
@@ -20,18 +22,16 @@ $SQL_CREATE_TABLES = array(
 	"pathEntries" VARCHAR(511) DEFAULT NULL,
 	"pathEntries" VARCHAR(511) DEFAULT NULL,
 	"httpAuth" VARCHAR(511) DEFAULT NULL,
 	"httpAuth" VARCHAR(511) DEFAULT NULL,
 	"error" SMALLINT DEFAULT 0,
 	"error" SMALLINT DEFAULT 0,
-	"keep_history" INT NOT NULL DEFAULT -2,
 	"ttl" INT NOT NULL DEFAULT 0,
 	"ttl" INT NOT NULL DEFAULT 0,
 	"attributes" TEXT,	-- v1.11.0
 	"attributes" TEXT,	-- v1.11.0
 	"cache_nbEntries" INT DEFAULT 0,
 	"cache_nbEntries" INT DEFAULT 0,
 	"cache_nbUnreads" 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");',
+	FOREIGN KEY ("category") REFERENCES `_category` ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);
+CREATE INDEX IF NOT EXISTS `_name_index` ON `_feed` ("name");
+CREATE INDEX IF NOT EXISTS `_priority_index` ON `_feed` ("priority");
 
 
-'CREATE TABLE IF NOT EXISTS "%1$sentry" (
+CREATE TABLE IF NOT EXISTS `_entry` (
 	"id" BIGINT NOT NULL PRIMARY KEY,
 	"id" BIGINT NOT NULL PRIMARY KEY,
 	"guid" VARCHAR(760) NOT NULL,
 	"guid" VARCHAR(760) NOT NULL,
 	"title" VARCHAR(255) NOT NULL,
 	"title" VARCHAR(255) NOT NULL,
@@ -45,22 +45,26 @@ $SQL_CREATE_TABLES = array(
 	"is_favorite" SMALLINT NOT NULL DEFAULT 0,
 	"is_favorite" SMALLINT NOT NULL DEFAULT 0,
 	"id_feed" SMALLINT,
 	"id_feed" SMALLINT,
 	"tags" VARCHAR(1023),
 	"tags" VARCHAR(1023),
-	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 "%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" (id, name)
-	SELECT 1, \'%2$s\'
-	WHERE NOT EXISTS (SELECT id FROM "%1$scategory" WHERE id = 1)
-	RETURNING nextval(\'"%1$scategory_id_seq"\');',
 );
 );
+CREATE INDEX IF NOT EXISTS `_is_favorite_index` ON `_entry` ("is_favorite");
+CREATE INDEX IF NOT EXISTS `_is_read_index` ON `_entry` ("is_read");
+CREATE INDEX IF NOT EXISTS `_entry_lastSeen_index` ON `_entry` ("lastSeen");
+CREATE INDEX IF NOT EXISTS `_entry_feed_read_index` ON `_entry` ("id_feed","is_read");	-- v1.7
+
+INSERT INTO `_category` (id, name)
+	SELECT 1, 'Uncategorized'
+	WHERE NOT EXISTS (SELECT id FROM `_category` WHERE id = 1)
+	RETURNING nextval('`_category_id_seq`');
+SQL;
 
 
-global $SQL_CREATE_TABLE_ENTRYTMP;
-$SQL_CREATE_TABLE_ENTRYTMP = array(
-'CREATE TABLE IF NOT EXISTS "%1$sentrytmp" (	-- v1.7
+$SQL_CREATE_INDEX_ENTRY_1 = <<<'SQL'
+CREATE INDEX IF NOT EXISTS `_entry_feed_read_index` ON `_entry` ("id_feed","is_read");	-- v1.7
+SQL;
+
+$SQL_CREATE_TABLE_ENTRYTMP = <<<'SQL'
+CREATE TABLE IF NOT EXISTS `_entrytmp` (	-- v1.7
 	"id" BIGINT NOT NULL PRIMARY KEY,
 	"id" BIGINT NOT NULL PRIMARY KEY,
 	"guid" VARCHAR(760) NOT NULL,
 	"guid" VARCHAR(760) NOT NULL,
 	"title" VARCHAR(255) NOT NULL,
 	"title" VARCHAR(255) NOT NULL,
@@ -74,39 +78,34 @@ $SQL_CREATE_TABLE_ENTRYTMP = array(
 	"is_favorite" SMALLINT NOT NULL DEFAULT 0,
 	"is_favorite" SMALLINT NOT NULL DEFAULT 0,
 	"id_feed" SMALLINT,
 	"id_feed" SMALLINT,
 	"tags" VARCHAR(1023),
 	"tags" VARCHAR(1023),
-	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 "%1$sentrytmp_date_index" ON "%1$sentrytmp" ("date");',
-
-'CREATE INDEX "%1$sentry_feed_read_index" ON "%1$sentry" ("id_feed","is_read");',	//v1.7
 );
 );
+CREATE INDEX IF NOT EXISTS `_entrytmp_date_index` ON `_entrytmp` ("date");
+SQL;
 
 
-global $SQL_CREATE_TABLE_TAGS;
-$SQL_CREATE_TABLE_TAGS = array(
-'CREATE TABLE IF NOT EXISTS "%1$stag" (	-- v1.12
+$SQL_CREATE_TABLE_TAGS = <<<'SQL'
+CREATE TABLE IF NOT EXISTS `_tag` (	-- v1.12
 	"id" SERIAL PRIMARY KEY,
 	"id" SERIAL PRIMARY KEY,
 	"name" VARCHAR(63) UNIQUE NOT NULL,
 	"name" VARCHAR(63) UNIQUE NOT NULL,
 	"attributes" TEXT
 	"attributes" TEXT
-);',
-'CREATE TABLE IF NOT EXISTS "%1$sentrytag" (
+);
+CREATE TABLE IF NOT EXISTS `_entrytag` (
 	"id_tag" SMALLINT,
 	"id_tag" SMALLINT,
 	"id_entry" BIGINT,
 	"id_entry" BIGINT,
 	PRIMARY KEY ("id_tag","id_entry"),
 	PRIMARY KEY ("id_tag","id_entry"),
-	FOREIGN KEY ("id_tag") REFERENCES "%1$stag" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
-	FOREIGN KEY ("id_entry") REFERENCES "%1$sentry" ("id") ON DELETE CASCADE ON UPDATE CASCADE
-);',
-'CREATE INDEX "%1$sentrytag_id_entry_index" ON "%1$sentrytag" ("id_entry");',
+	FOREIGN KEY ("id_tag") REFERENCES `_tag` ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+	FOREIGN KEY ("id_entry") REFERENCES `_entry` ("id") ON DELETE CASCADE ON UPDATE CASCADE
 );
 );
+CREATE INDEX IF NOT EXISTS `_entrytag_id_entry_index` ON `_entrytag` ("id_entry");
+SQL;
 
 
-global $SQL_INSERT_FEEDS;
-$SQL_INSERT_FEEDS = array(
-'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl)
-	SELECT \'https://freshrss.org/feeds/all.atom.xml\', 1, \'FreshRSS.org\', \'https://freshrss.org/\', \'FreshRSS, a free, self-hostable aggregator…\', 86400
-	WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'https://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\');',
-);
+$SQL_INSERT_FEED = <<<'SQL'
+INSERT INTO `_feed` (url, category, name, website, description, ttl)
+	SELECT :url::VARCHAR, 1, :name, :website, :description, 86400
+		WHERE NOT EXISTS (SELECT id FROM `_feed` WHERE url = :url);
+SQL;
 
 
-define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentrytag", "%1$stag", "%1$sentrytmp", "%1$sentry", "%1$sfeed", "%1$scategory"');
+$SQL_DROP_TABLES = <<<'SQL'
+DROP TABLE IF EXISTS `_entrytag`, `_tag`, `_entrytmp`, `_entry`, `_feed`, `_category`;
+SQL;

+ 47 - 40
app/SQL/install.sql.sqlite.php

@@ -1,13 +1,17 @@
 <?php
 <?php
-global $SQL_CREATE_TABLES;
-$SQL_CREATE_TABLES = array(
-'CREATE TABLE IF NOT EXISTS `category` (
+$SQL_CREATE_DB = <<<'SQL'
+SELECT 1;	-- Do nothing for SQLite
+SQL;
+
+$SQL_CREATE_TABLES = <<<'SQL'
+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,
+	`attributes` TEXT,	-- v1.15.0
 	UNIQUE (`name`)
 	UNIQUE (`name`)
-);',
+);
 
 
-'CREATE TABLE IF NOT EXISTS `feed` (
+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,
 	`category` SMALLINT DEFAULT 0,
 	`category` SMALLINT DEFAULT 0,
@@ -19,19 +23,17 @@ $SQL_CREATE_TABLES = array(
 	`pathEntries` VARCHAR(511) DEFAULT NULL,
 	`pathEntries` VARCHAR(511) DEFAULT NULL,
 	`httpAuth` VARCHAR(511) DEFAULT NULL,
 	`httpAuth` VARCHAR(511) DEFAULT NULL,
 	`error` BOOLEAN DEFAULT 0,
 	`error` BOOLEAN DEFAULT 0,
-	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,
 	`ttl` INT NOT NULL DEFAULT 0,
 	`ttl` INT NOT NULL DEFAULT 0,
 	`attributes` TEXT,	-- v1.11.0
 	`attributes` TEXT,	-- v1.11.0
 	`cache_nbEntries` INT DEFAULT 0,
 	`cache_nbEntries` INT DEFAULT 0,
 	`cache_nbUnreads` INT DEFAULT 0,
 	`cache_nbUnreads` INT DEFAULT 0,
 	FOREIGN KEY (`category`) REFERENCES `category`(`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 `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 INDEX IF NOT EXISTS feed_name_index ON `feed`(`name`);
+CREATE INDEX IF NOT EXISTS feed_priority_index ON `feed`(`priority`);
 
 
-'CREATE TABLE IF NOT EXISTS `entry` (
+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,
@@ -48,17 +50,21 @@ $SQL_CREATE_TABLES = array(
 	PRIMARY KEY (`id`),
 	PRIMARY KEY (`id`),
 	FOREIGN KEY (`id_feed`) REFERENCES `feed`(`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 `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 `category` (id, name) VALUES(1, "%2$s");',
 );
 );
+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
+CREATE INDEX IF NOT EXISTS entry_feed_read_index ON `entry`(`id_feed`,`is_read`);	-- v1.7
+
+INSERT OR IGNORE INTO `category` (id, name) VALUES(1, "Uncategorized");
+SQL;
 
 
-global $SQL_CREATE_TABLE_ENTRYTMP;
-$SQL_CREATE_TABLE_ENTRYTMP = array(
-'CREATE TABLE IF NOT EXISTS `entrytmp` (	-- v1.7
+$SQL_CREATE_INDEX_ENTRY_1 = <<<'SQL'
+CREATE INDEX IF NOT EXISTS entry_feed_read_index ON `entry`(`id_feed`,`is_read`);	-- v1.7
+SQL;
+
+$SQL_CREATE_TABLE_ENTRYTMP = <<<'SQL'
+CREATE TABLE IF NOT EXISTS `entrytmp` (	-- v1.7
 	`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,
@@ -75,36 +81,37 @@ $SQL_CREATE_TABLE_ENTRYTMP = array(
 	PRIMARY KEY (`id`),
 	PRIMARY KEY (`id`),
 	FOREIGN KEY (`id_feed`) REFERENCES `feed`(`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 entrytmp_date_index ON `entrytmp`(`date`);',
-
-'CREATE INDEX IF NOT EXISTS `entry_feed_read_index` ON `entry`(`id_feed`,`is_read`);',	//v1.7
 );
 );
+CREATE INDEX IF NOT EXISTS entrytmp_date_index ON `entrytmp`(`date`);
+SQL;
 
 
-global $SQL_CREATE_TABLE_TAGS;
-$SQL_CREATE_TABLE_TAGS = array(
-'CREATE TABLE IF NOT EXISTS `tag` (	-- v1.12
+$SQL_CREATE_TABLE_TAGS = <<<'SQL'
+CREATE TABLE IF NOT EXISTS `tag` (	-- v1.12
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 	`name` VARCHAR(63) NOT NULL,
 	`name` VARCHAR(63) NOT NULL,
 	`attributes` TEXT,
 	`attributes` TEXT,
 	UNIQUE (`name`)
 	UNIQUE (`name`)
-);',
-'CREATE TABLE IF NOT EXISTS `entrytag` (
+);
+CREATE TABLE IF NOT EXISTS `entrytag` (
 	`id_tag` SMALLINT,
 	`id_tag` SMALLINT,
-	`id_entry` SMALLINT,
+	`id_entry` BIGINT,
 	PRIMARY KEY (`id_tag`,`id_entry`),
 	PRIMARY KEY (`id_tag`,`id_entry`),
 	FOREIGN KEY (`id_tag`) REFERENCES `tag` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	FOREIGN KEY (`id_tag`) REFERENCES `tag` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	FOREIGN KEY (`id_entry`) REFERENCES `entry` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
 	FOREIGN KEY (`id_entry`) REFERENCES `entry` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
-);',
-'CREATE INDEX entrytag_id_entry_index ON `entrytag` (`id_entry`);',
 );
 );
+CREATE INDEX IF NOT EXISTS entrytag_id_entry_index ON `entrytag` (`id_entry`);
+SQL;
 
 
-global $SQL_INSERT_FEEDS;
-$SQL_INSERT_FEEDS = array(
-'INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl)
-	VALUES ("https://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "https://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);',
-);
+$SQL_INSERT_FEED = <<<'SQL'
+INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl)
+	VALUES(:url, 1, :name, :website, :description, 86400);
+SQL;
 
 
-define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `entrytag`, `tag`, `entrytmp`, `entry`, `feed`, `category`');
+$SQL_DROP_TABLES = <<<'SQL'
+DROP TABLE IF EXISTS `entrytag`;
+DROP TABLE IF EXISTS `tag`;
+DROP TABLE IF EXISTS `entrytmp`;
+DROP TABLE IF EXISTS `entry`;
+DROP TABLE IF EXISTS `feed`;
+DROP TABLE IF EXISTS `category`;
+SQL;

+ 1 - 0
app/i18n/cz/admin.php

@@ -163,6 +163,7 @@ return array(
 			'help' => 'in seconds', //TODO - Translation
 			'help' => 'in seconds', //TODO - Translation
 			'number' => 'Duration to keep logged in', //TODO - Translation
 			'number' => 'Duration to keep logged in', //TODO - Translation
 		),
 		),
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Instance name', //TODO - Translation
 		'instance-name' => 'Instance name', //TODO - Translation
 		'max-categories' => 'Categories per user limit', //TODO - Translation
 		'max-categories' => 'Categories per user limit', //TODO - Translation
 		'max-feeds' => 'Feeds per user limit', //TODO - Translation
 		'max-feeds' => 'Feeds per user limit', //TODO - Translation

+ 12 - 3
app/i18n/cz/conf.php

@@ -3,13 +3,21 @@
 return array(
 return array(
 	'archiving' => array(
 	'archiving' => array(
 		'_' => 'Archivace',
 		'_' => 'Archivace',
-		'advanced' => 'Pokročilé',
 		'delete_after' => 'Smazat články starší než',
 		'delete_after' => 'Smazat články starší než',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Více možností je dostupných v nastavení jednotlivých kanálů',
 		'help' => 'Více možností je dostupných v nastavení jednotlivých kanálů',
-		'keep_history_by_feed' => 'Zachovat tento minimální počet článků v každém kanálu',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Zachovat tento minimální počet článků v každém kanálu',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Optimalizovat databázi',
 		'optimize' => 'Optimalizovat databázi',
 		'optimize_help' => 'Občasná údržba zmenší velikost databáze',
 		'optimize_help' => 'Občasná údržba zmenší velikost databáze',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Vyčistit nyní',
 		'purge_now' => 'Vyčistit nyní',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Archivace',
 		'title' => 'Archivace',
 		'ttl' => 'Neaktualizovat častěji než',
 		'ttl' => 'Neaktualizovat častěji než',
 	),
 	),
@@ -21,6 +29,7 @@ return array(
 			'publication_date' => 'Datum vydání',
 			'publication_date' => 'Datum vydání',
 			'related_tags' => 'Související tagy',	//TODO - Translation
 			'related_tags' => 'Související tagy',	//TODO - Translation
 			'sharing' => 'Sdílení',
 			'sharing' => 'Sdílení',
+			'display_authors' => 'Authors',  //TODO - Translation
 			'top_line' => 'Horní řádek',
 			'top_line' => 'Horní řádek',
 		),
 		),
 		'language' => 'Jazyk',
 		'language' => 'Jazyk',
@@ -45,6 +54,7 @@ return array(
 			'_' => 'Smazání účtu',
 			'_' => 'Smazání účtu',
 			'warn' => 'Váš účet bude smazán spolu se všemi souvisejícími daty',
 			'warn' => 'Váš účet bude smazán spolu se všemi souvisejícími daty',
 		),
 		),
+		'email' => 'Email',
 		'password_api' => 'Password API<br /><small>(tzn. pro mobilní aplikace)</small>',
 		'password_api' => 'Password API<br /><small>(tzn. pro mobilní aplikace)</small>',
 		'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
 		'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
 		'password_format' => 'Alespoň 7 znaků',
 		'password_format' => 'Alespoň 7 znaků',
@@ -133,7 +143,6 @@ return array(
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'more_information' => 'Více informací',
 		'more_information' => 'Více informací',
 		'print' => 'Tisk',
 		'print' => 'Tisk',
 		'remove' => 'Remove sharing method',	//TODO - Translation
 		'remove' => 'Remove sharing method',	//TODO - Translation

+ 10 - 1
app/i18n/cz/gen.php

@@ -3,6 +3,7 @@
 return array(
 return array(
 	'action' => array(
 	'action' => array(
 		'actualize' => 'Aktualizovat',
 		'actualize' => 'Aktualizovat',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← Zpět na seznam RSS kanálů',
 		'back_to_rss_feeds' => '← Zpět na seznam RSS kanálů',
 		'cancel' => 'Zrušit',
 		'cancel' => 'Zrušit',
 		'create' => 'Vytvořit',
 		'create' => 'Vytvořit',
@@ -22,6 +23,7 @@ return array(
 		'update' => 'Update',	//TODO - Translation
 		'update' => 'Update',	//TODO - Translation
 	),
 	),
 	'auth' => array(
 	'auth' => array(
+		'accept_tos' => 'I accept the <a href="%s">Terms of Service</a>.', // TODO - Translation
 		'email' => 'Email',
 		'email' => 'Email',
 		'keep_logged_in' => 'Zapamatovat přihlášení <small>(%s dny)</small>',
 		'keep_logged_in' => 'Zapamatovat přihlášení <small>(%s dny)</small>',
 		'login' => 'Login',
 		'login' => 'Login',
@@ -160,15 +162,22 @@ return array(
 		'nothing_to_load' => 'Žádné nové články',
 		'nothing_to_load' => 'Žádné nové články',
 		'previous' => 'Předchozí',
 		'previous' => 'Předchozí',
 	),
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'gnusocial' => 'GNU social',
 		'gnusocial' => 'GNU social',
 		'jdh' => 'Journal du hacker',
 		'jdh' => 'Journal du hacker',
 		'Known' => 'Known based sites',
 		'Known' => 'Known based sites',
+		'lemmy' => 'Lemmy',
 		'linkedin' => 'LinkedIn',
 		'linkedin' => 'LinkedIn',
 		'mastodon' => 'Mastodon',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
 		'movim' => 'Movim',

+ 4 - 1
app/i18n/cz/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Hlášení chyb',
 		'bugs_reports' => 'Hlášení chyb',
 		'credits' => 'Poděkování',
 		'credits' => 'Poděkování',
 		'credits_content' => 'Některé designové prvky pocházejí z <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, FreshRSS ale tuto platformu nevyužívá. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Ikony</a> pocházejí z <a href="https://www.gnome.org/">GNOME projektu</a>. Font <em>Open Sans</em> vytvořil <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS je založen na PHP framework <a href="https://github.com/marienfressinaud/MINZ">Minz</a>.',
 		'credits_content' => 'Některé designové prvky pocházejí z <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, FreshRSS ale tuto platformu nevyužívá. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Ikony</a> pocházejí z <a href="https://www.gnome.org/">GNOME projektu</a>. Font <em>Open Sans</em> vytvořil <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS je založen na PHP framework <a href="https://github.com/marienfressinaud/MINZ">Minz</a>.',
-		'freshrss_description' => 'FreshRSS je čtečka RSS kanálů určená k provozu na vlastním serveru, podobná <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> nebo <a href="http://leed.idleman.fr/">Leed</a>. Je to nenáročný a jednoduchý, zároveň ale mocný a konfigurovatelný nástroj.',
+		'freshrss_description' => 'FreshRSS je čtečka RSS kanálů určená k provozu na vlastním serveru, podobná <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> nebo <a href="https://github.com/LeedRSS/Leed">Leed</a>. Je to nenáročný a jednoduchý, zároveň ale mocný a konfigurovatelný nástroj.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">na Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">na Github</a>',
 		'license' => 'Licence',
 		'license' => 'Licence',
 		'project_website' => 'Stránka projektu',
 		'project_website' => 'Stránka projektu',
@@ -15,6 +15,9 @@ return array(
 		'version' => 'Verze',
 		'version' => 'Verze',
 		'website' => 'Webové stránka',
 		'website' => 'Webové stránka',
 	),
 	),
+	'tos' => array(
+		'title' => 'Terms of Service', // TODO - Translation
+	),
 	'feed' => array(
 	'feed' => array(
 		'add' => 'Můžete přidat kanály.',
 		'add' => 'Můžete přidat kanály.',
 		'empty' => 'Žádné články k zobrazení.',
 		'empty' => 'Žádné články k zobrazení.',

+ 5 - 1
app/i18n/cz/sub.php

@@ -13,9 +13,12 @@ return array(
 	'category' => array(
 	'category' => array(
 		'_' => 'Kategorie',
 		'_' => 'Kategorie',
 		'add' => 'Přidat kategorii',
 		'add' => 'Přidat kategorii',
+		'archiving' => 'Archivace',
 		'empty' => 'Vyprázdit kategorii',
 		'empty' => 'Vyprázdit kategorii',
 		'information' => 'Informace',
 		'information' => 'Informace',
 		'new' => 'Nová kategorie',
 		'new' => 'Nová kategorie',
+		'position' => 'Display position',	//TODO - Translation
+		'position_help' => 'To control category sort order',	//TODO - Translation
 		'title' => 'Název',
 		'title' => 'Název',
 	),
 	),
 	'feed' => array(
 	'feed' => array(
@@ -40,7 +43,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		),
 		'information' => 'Informace',
 		'information' => 'Informace',
-		'keep_history' => 'Zachovat tento minimální počet článků',
+		'keep_min' => 'Zachovat tento minimální počet článků',
 		'moved_category_deleted' => 'Po smazání kategorie budou v ní obsažené kanály automaticky přesunuty do <em>%s</em>.',
 		'moved_category_deleted' => 'Po smazání kategorie budou v ní obsažené kanály automaticky přesunuty do <em>%s</em>.',
 		'mute' => 'mute',	//TODO - Translation
 		'mute' => 'mute',	//TODO - Translation
 		'no_selected' => 'Nejsou označeny žádné kanály.',
 		'no_selected' => 'Nejsou označeny žádné kanály.',
@@ -72,6 +75,7 @@ return array(
 	),
 	),
 	'firefox' => array(
 	'firefox' => array(
 		'documentation' => 'Follow the steps described <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">here</a> to add FreshRSS to Firefox feed reader list.',// TODO
 		'documentation' => 'Follow the steps described <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">here</a> to add FreshRSS to Firefox feed reader list.',// TODO
+		'obsolete_63' => 'From version 63 and onwards, Firefox has removed the ability to add your own subscription services that are not standalone programs.',	//TODO - Translation
 		'title' => 'Firefox feed reader',	//TODO - Translation
 		'title' => 'Firefox feed reader',	//TODO - Translation
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(

+ 37 - 0
app/i18n/cz/user.php

@@ -0,0 +1,37 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'tos' => array(
+		'feedback' => array(
+			'invalid' => 'You must accept the Terms of Service to be able to register.', // TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/de/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 	'system' => array(
 		'_' => 'Systemeinstellungen',
 		'_' => 'Systemeinstellungen',
 		'auto-update-url' => 'Auto-update URL',
 		'auto-update-url' => 'Auto-update URL',
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Dein Reader Name',
 		'instance-name' => 'Dein Reader Name',
 		'max-categories' => 'Anzahl erlaubter Kategorien pro Benutzer',
 		'max-categories' => 'Anzahl erlaubter Kategorien pro Benutzer',
 		'max-feeds' => 'Anzahl erlaubter Feeds pro Benutzer',
 		'max-feeds' => 'Anzahl erlaubter Feeds pro Benutzer',

+ 12 - 3
app/i18n/de/conf.php

@@ -3,13 +3,21 @@
 return array(
 return array(
 	'archiving' => array(
 	'archiving' => array(
 		'_' => 'Archivierung',
 		'_' => 'Archivierung',
-		'advanced' => 'Erweitert',
 		'delete_after' => 'Entferne Artikel nach',
 		'delete_after' => 'Entferne Artikel nach',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Weitere Optionen sind in den Einstellungen der individuellen Feeds verfügbar.',
 		'help' => 'Weitere Optionen sind in den Einstellungen der individuellen Feeds verfügbar.',
-		'keep_history_by_feed' => 'Minimale Anzahl an Artikeln, die pro Feed behalten werden',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Minimale Anzahl an Artikeln, die pro Feed behalten werden',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Datenbank optimieren',
 		'optimize' => 'Datenbank optimieren',
 		'optimize_help' => 'Sollte gelegentlich durchgeführt werden, um die Größe der Datenbank zu reduzieren.',
 		'optimize_help' => 'Sollte gelegentlich durchgeführt werden, um die Größe der Datenbank zu reduzieren.',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Jetzt bereinigen',
 		'purge_now' => 'Jetzt bereinigen',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Archivierung',
 		'title' => 'Archivierung',
 		'ttl' => 'Aktualisiere automatisch nicht öfter als',
 		'ttl' => 'Aktualisiere automatisch nicht öfter als',
 	),
 	),
@@ -21,6 +29,7 @@ return array(
 			'publication_date' => 'Datum der Veröffentlichung',
 			'publication_date' => 'Datum der Veröffentlichung',
 			'related_tags' => 'Verwandte Tags',
 			'related_tags' => 'Verwandte Tags',
 			'sharing' => 'Teilen',
 			'sharing' => 'Teilen',
+			'display_authors' => 'Authors',  //TODO - Translation
 			'top_line' => 'Kopfzeile',
 			'top_line' => 'Kopfzeile',
 		),
 		),
 		'language' => 'Sprache',
 		'language' => 'Sprache',
@@ -45,6 +54,7 @@ return array(
 			'_' => 'Accountlöschung',
 			'_' => 'Accountlöschung',
 			'warn' => 'Dein Account und alle damit bezogenen Daten werden gelöscht.',
 			'warn' => 'Dein Account und alle damit bezogenen Daten werden gelöscht.',
 		),
 		),
+		'email' => 'E-Mail-Adresse',
 		'password_api' => 'Passwort-API<br /><small>(z. B. für mobile Anwendungen)</small>',
 		'password_api' => 'Passwort-API<br /><small>(z. B. für mobile Anwendungen)</small>',
 		'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',
 		'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',
 		'password_format' => 'mindestens 7 Zeichen',
 		'password_format' => 'mindestens 7 Zeichen',
@@ -133,7 +143,6 @@ return array(
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'E-Mail',
 		'email' => 'E-Mail',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'more_information' => 'Weitere Informationen',
 		'more_information' => 'Weitere Informationen',
 		'print' => 'Drucken',
 		'print' => 'Drucken',
 		'remove' => 'Entferne Teilen-Dienst',
 		'remove' => 'Entferne Teilen-Dienst',

+ 10 - 1
app/i18n/de/gen.php

@@ -3,6 +3,7 @@
 return array(
 return array(
 	'action' => array(
 	'action' => array(
 		'actualize' => 'Aktualisieren',
 		'actualize' => 'Aktualisieren',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← Zurück zu Ihren RSS-Feeds gehen',
 		'back_to_rss_feeds' => '← Zurück zu Ihren RSS-Feeds gehen',
 		'cancel' => 'Abbrechen',
 		'cancel' => 'Abbrechen',
 		'create' => 'Erstellen',
 		'create' => 'Erstellen',
@@ -22,6 +23,7 @@ return array(
 		'update' => 'Aktualisieren',
 		'update' => 'Aktualisieren',
 	),
 	),
 	'auth' => array(
 	'auth' => array(
+		'accept_tos' => 'I accept the <a href="%s">Terms of Service</a>.', // TODO - Translation
 		'email' => 'E-Mail-Adresse',
 		'email' => 'E-Mail-Adresse',
 		'keep_logged_in' => 'Eingeloggt bleiben <small>(%s Tage)</small>',
 		'keep_logged_in' => 'Eingeloggt bleiben <small>(%s Tage)</small>',
 		'login' => 'Anmelden',
 		'login' => 'Anmelden',
@@ -160,15 +162,22 @@ return array(
 		'nothing_to_load' => 'Es gibt keine weiteren Artikel',
 		'nothing_to_load' => 'Es gibt keine weiteren Artikel',
 		'previous' => 'Vorherige',
 		'previous' => 'Vorherige',
 	),
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'E-Mail',
 		'email' => 'E-Mail',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'gnusocial' => 'GNU social',
 		'gnusocial' => 'GNU social',
 		'jdh' => 'Journal du hacker',
 		'jdh' => 'Journal du hacker',
 		'Known' => 'Known-Seite (https://withknown.com)',
 		'Known' => 'Known-Seite (https://withknown.com)',
+		'lemmy' => 'Lemmy',
 		'linkedin' => 'LinkedIn',
 		'linkedin' => 'LinkedIn',
 		'mastodon' => 'Mastodon',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
 		'movim' => 'Movim',

+ 4 - 1
app/i18n/de/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Fehlerberichte',
 		'bugs_reports' => 'Fehlerberichte',
 		'credits' => 'Credits',
 		'credits' => 'Credits',
 		'credits_content' => 'Einige Designelemente stammen von <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, obwohl FreshRSS dieses Framework nicht nutzt. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> stammen vom <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> Font wurde von <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> erstellt. FreshRSS basiert auf <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, einem PHP-Framework.',
 		'credits_content' => 'Einige Designelemente stammen von <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, obwohl FreshRSS dieses Framework nicht nutzt. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> stammen vom <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> Font wurde von <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> erstellt. FreshRSS basiert auf <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, einem PHP-Framework.',
-		'freshrss_description' => 'FreshRSS ist ein RSS-Feedsaggregator zum selbst hosten wie zum Beispiel <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> oder <a href="http://leed.idleman.fr/">Leed</a>. Er ist leicht und einfach zu handhaben und gleichzeitig ein leistungsstarkes und konfigurierbares Werkzeug.',
+		'freshrss_description' => 'FreshRSS ist ein RSS-Feedsaggregator zum selbst hosten wie zum Beispiel <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> oder <a href="https://github.com/LeedRSS/Leed">Leed</a>. Er ist leicht und einfach zu handhaben und gleichzeitig ein leistungsstarkes und konfigurierbares Werkzeug.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
 		'license' => 'Lizenz',
 		'license' => 'Lizenz',
 		'project_website' => 'Projekt-Webseite',
 		'project_website' => 'Projekt-Webseite',
@@ -15,6 +15,9 @@ return array(
 		'version' => 'Version',
 		'version' => 'Version',
 		'website' => 'Webseite',
 		'website' => 'Webseite',
 	),
 	),
+	'tos' => array(
+		'title' => 'Terms of Service', // TODO - Translation
+	),
 	'feed' => array(
 	'feed' => array(
 		'add' => 'Sie können Feeds hinzufügen.',
 		'add' => 'Sie können Feeds hinzufügen.',
 		'empty' => 'Es gibt keinen Artikel zum Anzeigen.',
 		'empty' => 'Es gibt keinen Artikel zum Anzeigen.',

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

@@ -13,9 +13,12 @@ return array(
 	'category' => array(
 	'category' => array(
 		'_' => 'Kategorie',
 		'_' => 'Kategorie',
 		'add' => 'Eine Kategorie hinzufügen',
 		'add' => 'Eine Kategorie hinzufügen',
+		'archiving' => 'Archivierung',
 		'empty' => 'Leere Kategorie',
 		'empty' => 'Leere Kategorie',
 		'information' => 'Information',
 		'information' => 'Information',
 		'new' => 'Neue Kategorie',
 		'new' => 'Neue Kategorie',
+		'position' => 'Display position',	//TODO - Translation
+		'position_help' => 'To control category sort order',	//TODO - Translation
 		'title' => 'Titel',
 		'title' => 'Titel',
 	),
 	),
 	'feed' => array(
 	'feed' => array(
@@ -40,7 +43,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		),
 		'information' => 'Information',
 		'information' => 'Information',
-		'keep_history' => 'Minimale Anzahl an Artikeln, die behalten wird',
+		'keep_min' => 'Minimale Anzahl an Artikeln, die behalten wird',
 		'moved_category_deleted' => 'Wenn Sie eine Kategorie entfernen, werden deren Feeds automatisch in die Kategorie <em>%s</em> eingefügt.',
 		'moved_category_deleted' => 'Wenn Sie eine Kategorie entfernen, werden deren Feeds automatisch in die Kategorie <em>%s</em> eingefügt.',
 		'mute' => 'Stumm schalten',
 		'mute' => 'Stumm schalten',
 		'no_selected' => 'Kein Feed ausgewählt.',
 		'no_selected' => 'Kein Feed ausgewählt.',
@@ -72,6 +75,7 @@ return array(
 	),
 	),
 	'firefox' => array(
 	'firefox' => array(
 		'documentation' => 'Folge den <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">hier</a> beschriebenen Schritten um FreshRSS zu Deiner Firefox RSS-Reader Liste hinzuzufügen.',
 		'documentation' => 'Folge den <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">hier</a> beschriebenen Schritten um FreshRSS zu Deiner Firefox RSS-Reader Liste hinzuzufügen.',
+		'obsolete_63' => 'From version 63 and onwards, Firefox has removed the ability to add your own subscription services that are not standalone programs.',	//TODO - Translation
 		'title' => 'Firefox RSS-Reader',
 		'title' => 'Firefox RSS-Reader',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(

+ 37 - 0
app/i18n/de/user.php

@@ -0,0 +1,37 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'tos' => array(
+		'feedback' => array(
+			'invalid' => 'You must accept the Terms of Service to be able to register.', // TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/en/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 	'system' => array(
 		'_' => 'System configuration',
 		'_' => 'System configuration',
 		'auto-update-url' => 'Auto-update server URL',
 		'auto-update-url' => 'Auto-update server URL',
+		'force_email_validation' => 'Force email addresses validation',
 		'instance-name' => 'Instance name',
 		'instance-name' => 'Instance name',
 		'max-categories' => 'Categories per user limit',
 		'max-categories' => 'Categories per user limit',
 		'max-feeds' => 'Feeds per user limit',
 		'max-feeds' => 'Feeds per user limit',

+ 12 - 3
app/i18n/en/conf.php

@@ -3,13 +3,21 @@
 return array(
 return array(
 	'archiving' => array(
 	'archiving' => array(
 		'_' => 'Archiving',
 		'_' => 'Archiving',
-		'advanced' => 'Advanced',
 		'delete_after' => 'Remove articles after',
 		'delete_after' => 'Remove articles after',
+		'exception' => 'Purge exception',
 		'help' => 'More options are available in the individual feed settings',
 		'help' => 'More options are available in the individual feed settings',
-		'keep_history_by_feed' => 'Minimum number of articles to keep by feed',
+		'keep_favourites' => 'Never delete favourites',
+		'keep_min_by_feed' => 'Minimum number of articles to keep by feed',
+		'keep_labels' => 'Never delete labels',
+		'keep_unreads' => 'Never delete unreads',
+		'maintenance' => 'Maintenance',
 		'optimize' => 'Optimise database',
 		'optimize' => 'Optimise database',
 		'optimize_help' => 'Do occasionally to reduce the size of the database',
 		'optimize_help' => 'Do occasionally to reduce the size of the database',
+		'policy' => 'Purge policy',
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',
 		'purge_now' => 'Purge now',
 		'purge_now' => 'Purge now',
+		'keep_max' => 'Maximum number of articles to keep',
+		'keep_period' => 'Maximum age of articles to keep',
 		'title' => 'Archiving',
 		'title' => 'Archiving',
 		'ttl' => 'Do not automatically refresh more often than',
 		'ttl' => 'Do not automatically refresh more often than',
 	),
 	),
@@ -21,6 +29,7 @@ return array(
 			'publication_date' => 'Date of publication',
 			'publication_date' => 'Date of publication',
 			'related_tags' => 'Article tags',
 			'related_tags' => 'Article tags',
 			'sharing' => 'Sharing',
 			'sharing' => 'Sharing',
+			'display_authors' => 'Authors',
 			'top_line' => 'Top line',
 			'top_line' => 'Top line',
 		),
 		),
 		'language' => 'Language',
 		'language' => 'Language',
@@ -45,6 +54,7 @@ return array(
 			'_' => 'Account deletion',
 			'_' => 'Account deletion',
 			'warn' => 'Your account and all related data will be deleted.',
 			'warn' => 'Your account and all related data will be deleted.',
 		),
 		),
+		'email' => 'Email address',
 		'password_api' => 'API password<br /><small>(e.g., for mobile apps)</small>',
 		'password_api' => 'API password<br /><small>(e.g., for mobile apps)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
 		'password_format' => 'At least 7 characters',
 		'password_format' => 'At least 7 characters',
@@ -133,7 +143,6 @@ return array(
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'more_information' => 'More information',
 		'more_information' => 'More information',
 		'print' => 'Print',
 		'print' => 'Print',
 		'remove' => 'Remove sharing method',
 		'remove' => 'Remove sharing method',

+ 11 - 1
app/i18n/en/gen.php

@@ -3,6 +3,7 @@
 return array(
 return array(
 	'action' => array(
 	'action' => array(
 		'actualize' => 'Actualize',
 		'actualize' => 'Actualize',
+		'back' => '← Go back',
 		'back_to_rss_feeds' => '← Go back to your RSS feeds',
 		'back_to_rss_feeds' => '← Go back to your RSS feeds',
 		'cancel' => 'Cancel',
 		'cancel' => 'Cancel',
 		'create' => 'Create',
 		'create' => 'Create',
@@ -22,6 +23,7 @@ return array(
 		'update' => 'Update',
 		'update' => 'Update',
 	),
 	),
 	'auth' => array(
 	'auth' => array(
+		'accept_tos' => 'I accept the <a href="%s">Terms of Service</a>.',
 		'email' => 'Email address',
 		'email' => 'Email address',
 		'keep_logged_in' => 'Keep me logged in <small>(%s days)</small>',
 		'keep_logged_in' => 'Keep me logged in <small>(%s days)</small>',
 		'login' => 'Login',
 		'login' => 'Login',
@@ -127,6 +129,7 @@ return array(
 		'oc' => 'Occitan',
 		'oc' => 'Occitan',
 		'pt-br' => 'Português (Brasil)',
 		'pt-br' => 'Português (Brasil)',
 		'ru' => 'Русский',
 		'ru' => 'Русский',
+		'sk' => 'Slovenčina',
 		'tr' => 'Türkçe',
 		'tr' => 'Türkçe',
 		'zh-cn' => '简体中文',
 		'zh-cn' => '简体中文',
 	),
 	),
@@ -160,15 +163,22 @@ return array(
 		'nothing_to_load' => 'There are no more articles',
 		'nothing_to_load' => 'There are no more articles',
 		'previous' => 'Previous',
 		'previous' => 'Previous',
 	),
 	),
+	'period' => array(
+		'days' => 'days',
+		'hours' => 'hours',
+		'months' => 'months',
+		'weeks' => 'weeks',
+		'years' => 'years',
+	),
 	'share' => array(
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'gnusocial' => 'GNU social',
 		'gnusocial' => 'GNU social',
 		'jdh' => 'Journal du hacker',
 		'jdh' => 'Journal du hacker',
 		'Known' => 'Known based sites',
 		'Known' => 'Known based sites',
+		'lemmy' => 'Lemmy',
 		'linkedin' => 'LinkedIn',
 		'linkedin' => 'LinkedIn',
 		'mastodon' => 'Mastodon',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
 		'movim' => 'Movim',

+ 4 - 1
app/i18n/en/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Bugs reports',
 		'bugs_reports' => 'Bugs reports',
 		'credits' => 'Credits',
 		'credits' => 'Credits',
 		'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn’t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police has been created by <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
 		'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn’t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police has been created by <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
-		'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="http://leed.idleman.fr/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.',
+		'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="https://github.com/LeedRSS/Leed">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
 		'license' => 'License',
 		'license' => 'License',
 		'project_website' => 'Project website',
 		'project_website' => 'Project website',
@@ -15,6 +15,9 @@ return array(
 		'version' => 'Version',
 		'version' => 'Version',
 		'website' => 'Website',
 		'website' => 'Website',
 	),
 	),
+	'tos' => array(
+		'title' => 'Terms of Service',
+	),
 	'feed' => array(
 	'feed' => array(
 		'add' => 'You may add some feeds.',
 		'add' => 'You may add some feeds.',
 		'empty' => 'There is no article to show.',
 		'empty' => 'There is no article to show.',

+ 5 - 1
app/i18n/en/sub.php

@@ -13,9 +13,12 @@ return array(
 	'category' => array(
 	'category' => array(
 		'_' => 'Category',
 		'_' => 'Category',
 		'add' => 'Add a category',
 		'add' => 'Add a category',
+		'archiving' => 'Archiving',
 		'empty' => 'Empty category',
 		'empty' => 'Empty category',
 		'information' => 'Information',
 		'information' => 'Information',
 		'new' => 'New category',
 		'new' => 'New category',
+		'position' => 'Display position',
+		'position_help' => 'To control category sort order',
 		'title' => 'Title',
 		'title' => 'Title',
 	),
 	),
 	'feed' => array(
 	'feed' => array(
@@ -40,7 +43,7 @@ return array(
 			'help' => 'Write one search filter per line.',
 			'help' => 'Write one search filter per line.',
 		),
 		),
 		'information' => 'Information',
 		'information' => 'Information',
-		'keep_history' => 'Minimum number of articles to keep',
+		'keep_min' => 'Minimum number of articles to keep',
 		'moved_category_deleted' => 'When you delete a category, its feeds are automatically classified under <em>%s</em>.',
 		'moved_category_deleted' => 'When you delete a category, its feeds are automatically classified under <em>%s</em>.',
 		'mute' => 'mute',
 		'mute' => 'mute',
 		'no_selected' => 'No feed selected.',
 		'no_selected' => 'No feed selected.',
@@ -72,6 +75,7 @@ return array(
 	),
 	),
 	'firefox' => array(
 	'firefox' => array(
 		'documentation' => 'Follow the steps described <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">here</a> to add FreshRSS to Firefox feed reader list.',
 		'documentation' => 'Follow the steps described <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">here</a> to add FreshRSS to Firefox feed reader list.',
+		'obsolete_63' => 'From version 63 and onwards, Firefox has removed the ability to add your own subscription services that are not standalone programs.',
 		'title' => 'Firefox feed reader',
 		'title' => 'Firefox feed reader',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(

+ 37 - 0
app/i18n/en/user.php

@@ -0,0 +1,37 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.',
+			'required' => 'The email address is required.',
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.',
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.',
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.',
+				'email_sent' => 'An email has been sent to your address.',
+				'error' => 'The email address failed to be validated.',
+				'ok' => 'The email address has been validated.',
+				'unneccessary' => 'The email address was already validated.',
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.',
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.',
+			'resend_email' => 'Resend the email',
+			'title' => 'Email address validation',
+		),
+	),
+	'tos' => array(
+		'feedback' => array(
+			'invalid' => 'You must accept the Terms of Service to be able to register.',
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account',
+			'welcome' => 'Welcome %s,',
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:',
+		),
+	),
+);

+ 1 - 0
app/i18n/es/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 	'system' => array(
 		'_' => 'Configuración del sistema',
 		'_' => 'Configuración del sistema',
 		'auto-update-url' => 'URL de auto-actualización',
 		'auto-update-url' => 'URL de auto-actualización',
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Nombre de la fuente',
 		'instance-name' => 'Nombre de la fuente',
 		'max-categories' => 'Límite de categorías por usuario',
 		'max-categories' => 'Límite de categorías por usuario',
 		'max-feeds' => 'Límite de fuentes por usuario',
 		'max-feeds' => 'Límite de fuentes por usuario',

+ 12 - 3
app/i18n/es/conf.php

@@ -3,13 +3,21 @@
 return array(
 return array(
 	'archiving' => array(
 	'archiving' => array(
 		'_' => 'Archivo',
 		'_' => 'Archivo',
-		'advanced' => 'Avanzado',
 		'delete_after' => 'Eliminar artículos tras',
 		'delete_after' => 'Eliminar artículos tras',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Hay más opciones disponibles en los ajustes de la fuente',
 		'help' => 'Hay más opciones disponibles en los ajustes de la fuente',
-		'keep_history_by_feed' => 'Número mínimo de artículos a conservar por fuente',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Número mínimo de artículos a conservar por fuente',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Optimizar la base de datos',
 		'optimize' => 'Optimizar la base de datos',
 		'optimize_help' => 'Ejecuta la optimización de vez en cuando para reducir el tamaño de la base de datos',
 		'optimize_help' => 'Ejecuta la optimización de vez en cuando para reducir el tamaño de la base de datos',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Limpiar ahora',
 		'purge_now' => 'Limpiar ahora',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Archivo',
 		'title' => 'Archivo',
 		'ttl' => 'No actualizar automáticamente más de',
 		'ttl' => 'No actualizar automáticamente más de',
 	),
 	),
@@ -21,6 +29,7 @@ return array(
 			'publication_date' => 'Fecha de publicación',
 			'publication_date' => 'Fecha de publicación',
 			'related_tags' => 'Etiquetas relacionadas',
 			'related_tags' => 'Etiquetas relacionadas',
 			'sharing' => 'Compartir',
 			'sharing' => 'Compartir',
+			'display_authors' => 'Authors',  //TODO - Translation
 			'top_line' => 'Línea superior',
 			'top_line' => 'Línea superior',
 		),
 		),
 		'language' => 'Idioma',
 		'language' => 'Idioma',
@@ -45,6 +54,7 @@ return array(
 			'_' => 'Borrar cuenta',
 			'_' => 'Borrar cuenta',
 			'warn' => 'Tu cuenta y todos los datos asociados serán eliminados.',
 			'warn' => 'Tu cuenta y todos los datos asociados serán eliminados.',
 		),
 		),
+		'email' => 'Correo electrónico',
 		'password_api' => 'Contraseña API <br /><small>(para apps móviles, por ej.)</small>',
 		'password_api' => 'Contraseña API <br /><small>(para apps móviles, por ej.)</small>',
 		'password_form' => 'Contraseña<br /><small>(para el método de identificación por formulario web)</small>',
 		'password_form' => 'Contraseña<br /><small>(para el método de identificación por formulario web)</small>',
 		'password_format' => 'Mínimo de 7 caracteres',
 		'password_format' => 'Mínimo de 7 caracteres',
@@ -133,7 +143,6 @@ return array(
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'more_information' => 'Más información',
 		'more_information' => 'Más información',
 		'print' => 'Print',
 		'print' => 'Print',
 		'remove' => 'Remove sharing method',	//TODO - Translation
 		'remove' => 'Remove sharing method',	//TODO - Translation

+ 10 - 1
app/i18n/es/gen.php

@@ -3,6 +3,7 @@
 return array(
 return array(
 	'action' => array(
 	'action' => array(
 		'actualize' => 'Actualizar',
 		'actualize' => 'Actualizar',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← regresar a tus fuentes RSS',
 		'back_to_rss_feeds' => '← regresar a tus fuentes RSS',
 		'cancel' => 'Cancelar',
 		'cancel' => 'Cancelar',
 		'create' => 'Crear',
 		'create' => 'Crear',
@@ -22,6 +23,7 @@ return array(
 		'update' => 'Update',	//TODO - Translation
 		'update' => 'Update',	//TODO - Translation
 	),
 	),
 	'auth' => array(
 	'auth' => array(
+		'accept_tos' => 'I accept the <a href="%s">Terms of Service</a>.', // TODO - Translation
 		'email' => 'Correo electrónico',
 		'email' => 'Correo electrónico',
 		'keep_logged_in' => 'Mantenerme identificado <small>(%s días)</small>',
 		'keep_logged_in' => 'Mantenerme identificado <small>(%s días)</small>',
 		'login' => 'Conectar',
 		'login' => 'Conectar',
@@ -160,15 +162,22 @@ return array(
 		'nothing_to_load' => 'No hay más artículos',
 		'nothing_to_load' => 'No hay más artículos',
 		'previous' => 'Anterior',
 		'previous' => 'Anterior',
 	),
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'gnusocial' => 'GNU social',
 		'gnusocial' => 'GNU social',
 		'jdh' => 'Journal du hacker',
 		'jdh' => 'Journal du hacker',
 		'Known' => 'Known based sites',
 		'Known' => 'Known based sites',
+		'lemmy' => 'Lemmy',
 		'linkedin' => 'LinkedIn',
 		'linkedin' => 'LinkedIn',
 		'mastodon' => 'Mastodon',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
 		'movim' => 'Movim',

+ 4 - 1
app/i18n/es/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Informe de fallos',
 		'bugs_reports' => 'Informe de fallos',
 		'credits' => 'Créditos',
 		'credits' => 'Créditos',
 		'credits_content' => 'Aunque FreshRSS no usa ese entorno, algunos elementos del diseño están obtenidos de <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>. Los <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Iconos</a> han sido obtenidos del <a href="https://www.gnome.org/">proyecto GNOME</a>. La fuente <em>Open Sans</em> es una creación de <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS usa el entorno PHP <a href="https://github.com/marienfressinaud/MINZ">Minz</a>.',
 		'credits_content' => 'Aunque FreshRSS no usa ese entorno, algunos elementos del diseño están obtenidos de <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>. Los <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Iconos</a> han sido obtenidos del <a href="https://www.gnome.org/">proyecto GNOME</a>. La fuente <em>Open Sans</em> es una creación de <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS usa el entorno PHP <a href="https://github.com/marienfressinaud/MINZ">Minz</a>.',
-		'freshrss_description' => 'FreshRSS es un agregador de fuentes RSS de alojamiento privado al estilo de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> o <a href="http://leed.idleman.fr/">Leed</a>. Es una herramienta potente, pero ligera y fácil de usar y configurar.',
+		'freshrss_description' => 'FreshRSS es un agregador de fuentes RSS de alojamiento privado al estilo de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> o <a href="https://github.com/LeedRSS/Leed">Leed</a>. Es una herramienta potente, pero ligera y fácil de usar y configurar.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">en Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">en Github</a>',
 		'license' => 'Licencia',
 		'license' => 'Licencia',
 		'project_website' => 'Web del proyecto',
 		'project_website' => 'Web del proyecto',
@@ -15,6 +15,9 @@ return array(
 		'version' => 'Versión',
 		'version' => 'Versión',
 		'website' => 'Web',
 		'website' => 'Web',
 	),
 	),
+	'tos' => array(
+		'title' => 'Terms of Service', // TODO - Translation
+	),
 	'feed' => array(
 	'feed' => array(
 		'add' => 'Puedes añadir fuentes.',
 		'add' => 'Puedes añadir fuentes.',
 		'empty' => 'No hay artículos a mostrar.',
 		'empty' => 'No hay artículos a mostrar.',

+ 5 - 1
app/i18n/es/sub.php

@@ -13,9 +13,12 @@ return array(
 	'category' => array(
 	'category' => array(
 		'_' => 'Categoría',
 		'_' => 'Categoría',
 		'add' => 'Añadir a la categoría',
 		'add' => 'Añadir a la categoría',
+		'archiving' => 'Archivo',
 		'empty' => 'Vaciar categoría',
 		'empty' => 'Vaciar categoría',
 		'information' => 'Información',
 		'information' => 'Información',
 		'new' => 'Nueva categoría',
 		'new' => 'Nueva categoría',
+		'position' => 'Display position',	//TODO - Translation
+		'position_help' => 'To control category sort order',	//TODO - Translation
 		'title' => 'Título',
 		'title' => 'Título',
 	),
 	),
 	'feed' => array(
 	'feed' => array(
@@ -40,7 +43,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		),
 		'information' => 'Información',
 		'information' => 'Información',
-		'keep_history' => 'Número mínimo de artículos a conservar',
+		'keep_min' => 'Número mínimo de artículos a conservar',
 		'moved_category_deleted' => 'Al borrar una categoría todas sus fuentes pasan automáticamente a la categoría <em>%s</em>.',
 		'moved_category_deleted' => 'Al borrar una categoría todas sus fuentes pasan automáticamente a la categoría <em>%s</em>.',
 		'mute' => 'mute',	//TODO - Translation
 		'mute' => 'mute',	//TODO - Translation
 		'no_selected' => 'No hay funentes seleccionadas.',
 		'no_selected' => 'No hay funentes seleccionadas.',
@@ -72,6 +75,7 @@ return array(
 	),
 	),
 	'firefox' => array(
 	'firefox' => array(
 		'documentation' => 'Follow the steps described <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">here</a> to add FreshRSS to Firefox feed reader list.',	//TODO - Translation
 		'documentation' => 'Follow the steps described <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">here</a> to add FreshRSS to Firefox feed reader list.',	//TODO - Translation
+		'obsolete_63' => 'From version 63 and onwards, Firefox has removed the ability to add your own subscription services that are not standalone programs.',	//TODO - Translation
 		'title' => 'Firefox feed reader',	//TODO - Translation
 		'title' => 'Firefox feed reader',	//TODO - Translation
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(

+ 37 - 0
app/i18n/es/user.php

@@ -0,0 +1,37 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'tos' => array(
+		'feedback' => array(
+			'invalid' => 'You must accept the Terms of Service to be able to register.', // TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/fr/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 	'system' => array(
 		'_' => 'Configuration du système',
 		'_' => 'Configuration du système',
 		'auto-update-url' => 'URL du service de mise à jour',
 		'auto-update-url' => 'URL du service de mise à jour',
+		'force_email_validation' => 'Forcer la validation des adresses email',
 		'instance-name' => 'Nom de l’instance',
 		'instance-name' => 'Nom de l’instance',
 		'max-categories' => 'Limite de catégories par utilisateur',
 		'max-categories' => 'Limite de catégories par utilisateur',
 		'max-feeds' => 'Limite de flux par utilisateur',
 		'max-feeds' => 'Limite de flux par utilisateur',

+ 12 - 3
app/i18n/fr/conf.php

@@ -3,13 +3,21 @@
 return array(
 return array(
 	'archiving' => array(
 	'archiving' => array(
 		'_' => 'Archivage',
 		'_' => 'Archivage',
-		'advanced' => 'Avancé',
 		'delete_after' => 'Supprimer les articles après',
 		'delete_after' => 'Supprimer les articles après',
+		'exception' => 'Exception de nettoyage',
 		'help' => 'D’autres options sont disponibles dans la configuration individuelle des flux.',
 		'help' => 'D’autres options sont disponibles dans la configuration individuelle des flux.',
-		'keep_history_by_feed' => 'Nombre minimum d’articles à conserver par flux',
+		'keep_favourites' => 'Ne jamais supprimer les articles favoris',
+		'keep_min_by_feed' => 'Nombre minimum d’articles à conserver par flux',
+		'keep_labels' => 'Ne jamais supprimer les articles étiquetés',
+		'keep_unreads' => 'Ne jamais supprimer les articles non lus',
+		'maintenance' => 'Maintenance',
 		'optimize' => 'Optimiser la base de données',
 		'optimize' => 'Optimiser la base de données',
 		'optimize_help' => 'À faire de temps en temps pour réduire la taille de la BDD',
 		'optimize_help' => 'À faire de temps en temps pour réduire la taille de la BDD',
+		'policy' => 'Politique de nettoyage',
+		'policy_warning' => 'Si aucune politique de nettoyage n’est sélectionnée, tous les articles seront conservés.',
 		'purge_now' => 'Purger maintenant',
 		'purge_now' => 'Purger maintenant',
+		'keep_max' => 'Nombre maximum d’articles à conserver',
+		'keep_period' => 'Âge maximum des articles à conserver',
 		'title' => 'Archivage',
 		'title' => 'Archivage',
 		'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',
 		'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',
 	),
 	),
@@ -21,6 +29,7 @@ return array(
 			'publication_date' => 'Date de publication',
 			'publication_date' => 'Date de publication',
 			'related_tags' => 'Tags de l’article',
 			'related_tags' => 'Tags de l’article',
 			'sharing' => 'Partage',
 			'sharing' => 'Partage',
+			'display_authors' => 'Authors',  //TODO - Translation
 			'top_line' => 'Ligne du haut',
 			'top_line' => 'Ligne du haut',
 		),
 		),
 		'language' => 'Langue',
 		'language' => 'Langue',
@@ -45,6 +54,7 @@ return array(
 			'_' => 'Suppression du compte',
 			'_' => 'Suppression du compte',
 			'warn' => 'Le compte et toutes les données associées vont être supprimées.',
 			'warn' => 'Le compte et toutes les données associées vont être supprimées.',
 		),
 		),
+		'email' => 'Adresse email',
 		'password_api' => 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</small>',
 		'password_api' => 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</small>',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
 		'password_format' => '7 caractères minimum',
 		'password_format' => '7 caractères minimum',
@@ -133,7 +143,6 @@ return array(
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Courriel',
 		'email' => 'Courriel',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'more_information' => 'Plus d’informations',
 		'more_information' => 'Plus d’informations',
 		'print' => 'Print',
 		'print' => 'Print',
 		'remove' => 'Supprimer la méthode de partage',
 		'remove' => 'Supprimer la méthode de partage',

+ 10 - 1
app/i18n/fr/gen.php

@@ -3,6 +3,7 @@
 return array(
 return array(
 	'action' => array(
 	'action' => array(
 		'actualize' => 'Actualiser',
 		'actualize' => 'Actualiser',
+		'back' => '← Retour',
 		'back_to_rss_feeds' => '← Retour à vos flux RSS',
 		'back_to_rss_feeds' => '← Retour à vos flux RSS',
 		'cancel' => 'Annuler',
 		'cancel' => 'Annuler',
 		'create' => 'Créer',
 		'create' => 'Créer',
@@ -22,6 +23,7 @@ return array(
 		'update' => 'Mettre à jour',
 		'update' => 'Mettre à jour',
 	),
 	),
 	'auth' => array(
 	'auth' => array(
+		'accept_tos' => 'Accepter les <a href="%s">Conditions Générales d’Utilisation</a>.',
 		'email' => 'Adresse courriel',
 		'email' => 'Adresse courriel',
 		'keep_logged_in' => 'Rester connecté <small>(%s jours)</small>',
 		'keep_logged_in' => 'Rester connecté <small>(%s jours)</small>',
 		'login' => 'Connexion',
 		'login' => 'Connexion',
@@ -160,15 +162,22 @@ return array(
 		'nothing_to_load' => 'Fin des articles',
 		'nothing_to_load' => 'Fin des articles',
 		'previous' => 'Précédent',
 		'previous' => 'Précédent',
 	),
 	),
+	'period' => array(
+		'days' => 'jours',
+		'hours' => 'heures',
+		'months' => 'mois',
+		'weeks' => 'semaines',
+		'years' => 'années',
+	),
 	'share' => array(
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Courriel',
 		'email' => 'Courriel',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'gnusocial' => 'GNU social',
 		'gnusocial' => 'GNU social',
 		'jdh' => 'Journal du hacker',
 		'jdh' => 'Journal du hacker',
 		'Known' => 'Sites basés sur Known',
 		'Known' => 'Sites basés sur Known',
+		'lemmy' => 'Lemmy',
 		'linkedin' => 'LinkedIn',
 		'linkedin' => 'LinkedIn',
 		'mastodon' => 'Mastodon',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
 		'movim' => 'Movim',

+ 4 - 1
app/i18n/fr/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Rapports de bugs',
 		'bugs_reports' => 'Rapports de bugs',
 		'credits' => 'Crédits',
 		'credits' => 'Crédits',
 		'credits_content' => 'Des éléments de design sont issus du <a href="http://twitter.github.io/bootstrap/">projet Bootstrap</a> bien que FreshRSS n’utilise pas ce framework. Les <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icônes</a> sont issues du <a href="https://www.gnome.org/">projet GNOME</a>. La police <em>Open Sans</em> utilisée a été créée par <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS repose sur <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
 		'credits_content' => 'Des éléments de design sont issus du <a href="http://twitter.github.io/bootstrap/">projet Bootstrap</a> bien que FreshRSS n’utilise pas ce framework. Les <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icônes</a> sont issues du <a href="https://www.gnome.org/">projet GNOME</a>. La police <em>Open Sans</em> utilisée a été créée par <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS repose sur <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
-		'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://leed.idleman.fr/">Leed</a>. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.',
+		'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="https://github.com/LeedRSS/Leed">Leed</a>. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">sur Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">sur Github</a>',
 		'license' => 'Licence',
 		'license' => 'Licence',
 		'project_website' => 'Site du projet',
 		'project_website' => 'Site du projet',
@@ -15,6 +15,9 @@ return array(
 		'version' => 'Version',
 		'version' => 'Version',
 		'website' => 'Site Internet',
 		'website' => 'Site Internet',
 	),
 	),
+	'tos' => array(
+		'title' => 'Conditions Générales d’Utilisation',
+	),
 	'feed' => array(
 	'feed' => array(
 		'add' => 'Vous pouvez ajouter des flux.',
 		'add' => 'Vous pouvez ajouter des flux.',
 		'empty' => 'Il n’y a aucun article à afficher.',
 		'empty' => 'Il n’y a aucun article à afficher.',

+ 5 - 1
app/i18n/fr/sub.php

@@ -13,9 +13,12 @@ return array(
 	'category' => array(
 	'category' => array(
 		'_' => 'Catégorie',
 		'_' => 'Catégorie',
 		'add' => 'Ajouter une catégorie',
 		'add' => 'Ajouter une catégorie',
+		'archiving' => 'Archivage',
 		'empty' => 'Catégorie vide',
 		'empty' => 'Catégorie vide',
 		'information' => 'Informations',
 		'information' => 'Informations',
 		'new' => 'Nouvelle catégorie',
 		'new' => 'Nouvelle catégorie',
+		'position' => 'Position d’affichage',
+		'position_help' => 'Pour contrôler l’ordre de tri des catégories',
 		'title' => 'Titre',
 		'title' => 'Titre',
 	),
 	),
 	'feed' => array(
 	'feed' => array(
@@ -40,7 +43,7 @@ return array(
 			'help' => 'Écrivez une recherche par ligne.',
 			'help' => 'Écrivez une recherche par ligne.',
 		),
 		),
 		'information' => 'Informations',
 		'information' => 'Informations',
-		'keep_history' => 'Nombre minimum d’articles à conserver',
+		'keep_min' => 'Nombre minimum d’articles à conserver',
 		'moved_category_deleted' => 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans <em>%s</em>.',
 		'moved_category_deleted' => 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans <em>%s</em>.',
 		'mute' => 'muet',
 		'mute' => 'muet',
 		'no_selected' => 'Aucun flux sélectionné.',
 		'no_selected' => 'Aucun flux sélectionné.',
@@ -72,6 +75,7 @@ return array(
 	),
 	),
 	'firefox' => array(
 	'firefox' => array(
 		'documentation' => 'Suivre les étapes décrites <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">ici</a> pour ajouter FreshRSS à la liste des lecteurs de flux dans Firefox.',
 		'documentation' => 'Suivre les étapes décrites <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">ici</a> pour ajouter FreshRSS à la liste des lecteurs de flux dans Firefox.',
+		'obsolete_63' => 'À partir de la version 63, Firefox ne supporte plus l’ajout de services d’abonnements.',
 		'title' => 'Lecteur de flux dans Firefox',
 		'title' => 'Lecteur de flux dans Firefox',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(

+ 37 - 0
app/i18n/fr/user.php

@@ -0,0 +1,37 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'L’adresse email est invalide.',
+			'required' => 'L’adresse email est requise.',
+		),
+		'validation' => array(
+			'change_email' => 'Vous pouvez changer votre adresse email <a href="%s">dans votre profil</a>.',
+			'email_sent_to' => 'Nous venons d’envoyer un email à <strong>%s</strong>, veuillez suivre ses indications pour valider votre adresse.',
+			'feedback' => array(
+				'email_failed' => 'Nous n’avons pas pu vous envoyer d’email à cause d’une mauvaise configuration du serveur.',
+				'email_sent' => 'Un email a été envoyé à votre adresse.',
+				'error' => 'L’adresse email n’a pas pu être validée.',
+				'ok' => 'L’adresse email a été validée.',
+				'unnecessary' => 'L’adresse email a déjà été validée.',
+				'wrong_token' => 'L’adresse email n’a pas pu être validée à cause d’un mauvais token.',
+			),
+			'need_to' => 'Vous devez valider votre adresse email avant de pouvoir utiliser %s.',
+			'resend_email' => 'Renvoyer l’email',
+			'title' => 'Validation de l’adresse email',
+		),
+	),
+	'tos' => array(
+		'feedback' => array(
+			'invalid' => 'Vous devez accepter les conditions générales d’utilisation pour pouvoir vous inscrire.',
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'Vous devez valider votre compte',
+			'welcome' => 'Bienvenue %s,',
+			'body' => 'Vous venez de vous inscrire sur %s mais vous devez encore valider votre adresse email. Pour cela, veuillez cliquer sur ce lien :',
+		),
+	),
+);

+ 1 - 0
app/i18n/he/admin.php

@@ -163,6 +163,7 @@ return array(
 			'help' => 'in seconds', //TODO - Translation
 			'help' => 'in seconds', //TODO - Translation
 			'number' => 'Duration to keep logged in', //TODO - Translation
 			'number' => 'Duration to keep logged in', //TODO - Translation
 		),
 		),
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Instance name', //TODO - Translation
 		'instance-name' => 'Instance name', //TODO - Translation
 		'max-categories' => 'Categories per user limit', //TODO - Translation
 		'max-categories' => 'Categories per user limit', //TODO - Translation
 		'max-feeds' => 'Feeds per user limit', //TODO - Translation
 		'max-feeds' => 'Feeds per user limit', //TODO - Translation

+ 12 - 3
app/i18n/he/conf.php

@@ -3,13 +3,21 @@
 return array(
 return array(
 	'archiving' => array(
 	'archiving' => array(
 		'_' => 'ארכוב',
 		'_' => 'ארכוב',
-		'advanced' => 'מתקדם',
 		'delete_after' => 'מחיקת מאמרים לאחר',
 		'delete_after' => 'מחיקת מאמרים לאחר',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'אפשרויות נוספות זמינות בזרמים ספציפיים',
 		'help' => 'אפשרויות נוספות זמינות בזרמים ספציפיים',
-		'keep_history_by_feed' => 'Minimum number of articles to keep by feed',	//TODO - Translation
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Minimum number of articles to keep by feed',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'מיטוב בסיס הנתונים',
 		'optimize' => 'מיטוב בסיס הנתונים',
 		'optimize_help' => 'ביצוע לעיתים קרובות על מנת למטב את בסיס הנתונים',
 		'optimize_help' => 'ביצוע לעיתים קרובות על מנת למטב את בסיס הנתונים',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'ניקוי עכשיו',
 		'purge_now' => 'ניקוי עכשיו',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'ארכוב',
 		'title' => 'ארכוב',
 		'ttl' => 'אין לרענן אוטומטית יותר מ',
 		'ttl' => 'אין לרענן אוטומטית יותר מ',
 	),
 	),
@@ -21,6 +29,7 @@ return array(
 			'publication_date' => 'תאריך הפרסום',
 			'publication_date' => 'תאריך הפרסום',
 			'related_tags' => 'תגיות קשורות',	//TODO - Translation
 			'related_tags' => 'תגיות קשורות',	//TODO - Translation
 			'sharing' => 'שיתוף',
 			'sharing' => 'שיתוף',
+			'display_authors' => 'Authors',  //TODO - Translation
 			'top_line' => 'שורה עליונה',
 			'top_line' => 'שורה עליונה',
 		),
 		),
 		'language' => 'שפה',
 		'language' => 'שפה',
@@ -45,6 +54,7 @@ return array(
 			'_' => 'Account deletion',	//TODO - Translation
 			'_' => 'Account deletion',	//TODO - Translation
 			'warn' => 'Your account and all related data will be deleted.',	//TODO - Translation
 			'warn' => 'Your account and all related data will be deleted.',	//TODO - Translation
 		),
 		),
+		'email' => 'Email address',	//TODO - Translation
 		'password_api' => 'סיסמת API<br /><small>(לדוגמה ליישומים סלולריים)</small>',
 		'password_api' => 'סיסמת API<br /><small>(לדוגמה ליישומים סלולריים)</small>',
 		'password_form' => 'סיסמה<br /><small>(לשימוש בטפוס ההרשמה)</small>',
 		'password_form' => 'סיסמה<br /><small>(לשימוש בטפוס ההרשמה)</small>',
 		'password_format' => 'At least 7 characters',	//TODO - Translation
 		'password_format' => 'At least 7 characters',	//TODO - Translation
@@ -133,7 +143,6 @@ return array(
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'דואר אלקטרוני',
 		'email' => 'דואר אלקטרוני',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'more_information' => 'מידע נוסף',
 		'more_information' => 'מידע נוסף',
 		'print' => 'הדפסה',
 		'print' => 'הדפסה',
 		'remove' => 'Remove sharing method',	//TODO - Translation
 		'remove' => 'Remove sharing method',	//TODO - Translation

+ 10 - 1
app/i18n/he/gen.php

@@ -3,6 +3,7 @@
 return array(
 return array(
 	'action' => array(
 	'action' => array(
 		'actualize' => 'מימוש',
 		'actualize' => 'מימוש',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← חזרה להזנות הRSS שלך',
 		'back_to_rss_feeds' => '← חזרה להזנות הRSS שלך',
 		'cancel' => 'ביטול',
 		'cancel' => 'ביטול',
 		'create' => 'יצירה',
 		'create' => 'יצירה',
@@ -22,6 +23,7 @@ return array(
 		'update' => 'Update',	//TODO - Translation
 		'update' => 'Update',	//TODO - Translation
 	),
 	),
 	'auth' => array(
 	'auth' => array(
+		'accept_tos' => 'I accept the <a href="%s">Terms of Service</a>.', // TODO - Translation
 		'email' => 'Email address',	//TODO - Translation
 		'email' => 'Email address',	//TODO - Translation
 		'keep_logged_in' => 'השאר מחובר <small>חודש</small>',
 		'keep_logged_in' => 'השאר מחובר <small>חודש</small>',
 		'login' => 'כניסה לחשבון',
 		'login' => 'כניסה לחשבון',
@@ -160,15 +162,22 @@ return array(
 		'nothing_to_load' => 'אין מאמרים נוספים',
 		'nothing_to_load' => 'אין מאמרים נוספים',
 		'previous' => 'הקודם',
 		'previous' => 'הקודם',
 	),
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'דואר אלקטרוני',
 		'email' => 'דואר אלקטרוני',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'gnusocial' => 'GNU social',
 		'gnusocial' => 'GNU social',
 		'jdh' => 'Journal du hacker',
 		'jdh' => 'Journal du hacker',
 		'Known' => 'Known based sites',
 		'Known' => 'Known based sites',
+		'lemmy' => 'Lemmy',
 		'linkedin' => 'LinkedIn',
 		'linkedin' => 'LinkedIn',
 		'mastodon' => 'Mastodon',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
 		'movim' => 'Movim',

+ 4 - 1
app/i18n/he/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'דיווח באגים',
 		'bugs_reports' => 'דיווח באגים',
 		'credits' => 'קרדיטים',
 		'credits' => 'קרדיטים',
 		'credits_content' => 'מאפייני עיצוב מסויימים הגיעו מ <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> אף על פי ש FreshRSS אינו משתמש בתשתית הזו. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">סמלילים</a> הגיעו מ <a href="https://www.gnome.org/"> פרוייקט GNOME </a>. <em>Open Sans</em> הגופן police נוצר על ידי <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Favicons נאספים בעזרת <a href="https://getfavicon.appspot.com/">getFavicon API</a>. FreshRSS מבוסס על <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, תשתית PHP.',
 		'credits_content' => 'מאפייני עיצוב מסויימים הגיעו מ <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> אף על פי ש FreshRSS אינו משתמש בתשתית הזו. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">סמלילים</a> הגיעו מ <a href="https://www.gnome.org/"> פרוייקט GNOME </a>. <em>Open Sans</em> הגופן police נוצר על ידי <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Favicons נאספים בעזרת <a href="https://getfavicon.appspot.com/">getFavicon API</a>. FreshRSS מבוסס על <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, תשתית PHP.',
-		'freshrss_description' => 'FreshRSS הוא קורא RSS לאחסון עצמי בדומה ל <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> או <a href="http://leed.idleman.fr/">Leed</a>. אינו צורך משאבים רבים, וקל לתפעול אך בו בזמן חזק וניתן להתאמה.',
+		'freshrss_description' => 'FreshRSS הוא קורא RSS לאחסון עצמי בדומה ל <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> או <a href="https://github.com/LeedRSS/Leed">Leed</a>. אינו צורך משאבים רבים, וקל לתפעול אך בו בזמן חזק וניתן להתאמה.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">בגיטהאב</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">בגיטהאב</a>',
 		'license' => 'רישיון',
 		'license' => 'רישיון',
 		'project_website' => 'אתר',
 		'project_website' => 'אתר',
@@ -15,6 +15,9 @@ return array(
 		'version' => 'גרסה',
 		'version' => 'גרסה',
 		'website' => 'אתר',
 		'website' => 'אתר',
 	),
 	),
+	'tos' => array(
+		'title' => 'Terms of Service', // TODO - Translation
+	),
 	'feed' => array(
 	'feed' => array(
 		'add' => 'ניתן להוסיף הזנות חדשות.',
 		'add' => 'ניתן להוסיף הזנות חדשות.',
 		'empty' => 'אין מאמר להצגה.',
 		'empty' => 'אין מאמר להצגה.',

+ 5 - 1
app/i18n/he/sub.php

@@ -13,9 +13,12 @@ return array(
 	'category' => array(
 	'category' => array(
 		'_' => 'קטגוריה',
 		'_' => 'קטגוריה',
 		'add' => 'הוספת קטגוריה',
 		'add' => 'הוספת קטגוריה',
+		'archiving' => 'ארכוב',
 		'empty' => 'Empty category',	//TODO - Translation
 		'empty' => 'Empty category',	//TODO - Translation
 		'information' => 'מידע',
 		'information' => 'מידע',
 		'new' => 'קטגוריה חדשה',
 		'new' => 'קטגוריה חדשה',
+		'position' => 'Display position',	//TODO - Translation
+		'position_help' => 'To control category sort order',	//TODO - Translation
 		'title' => 'כותרת',
 		'title' => 'כותרת',
 	),
 	),
 	'feed' => array(
 	'feed' => array(
@@ -40,7 +43,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		),
 		'information' => 'מידע',
 		'information' => 'מידע',
-		'keep_history' => 'מסםר מינימלי של מאמרים לשמור',
+		'keep_min' => 'מסםר מינימלי של מאמרים לשמור',
 		'moved_category_deleted' => 'כאשר הקטגוריה נמחקת ההזנות שבתוכה אוטומטית מקוטלגות תחת  <em>%s</em>.',
 		'moved_category_deleted' => 'כאשר הקטגוריה נמחקת ההזנות שבתוכה אוטומטית מקוטלגות תחת  <em>%s</em>.',
 		'mute' => 'mute',	//TODO - Translation
 		'mute' => 'mute',	//TODO - Translation
 		'no_selected' => 'אף הזנה לא נבחרה.',
 		'no_selected' => 'אף הזנה לא נבחרה.',
@@ -72,6 +75,7 @@ return array(
 	),
 	),
 	'firefox' => array(
 	'firefox' => array(
 		'documentation' => 'Follow the steps described <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">here</a> to add FreshRSS to Firefox feed reader list.',	//TODO - Translation
 		'documentation' => 'Follow the steps described <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">here</a> to add FreshRSS to Firefox feed reader list.',	//TODO - Translation
+		'obsolete_63' => 'From version 63 and onwards, Firefox has removed the ability to add your own subscription services that are not standalone programs.',	//TODO - Translation
 		'title' => 'Firefox feed reader',	//TODO - Translation
 		'title' => 'Firefox feed reader',	//TODO - Translation
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(

+ 37 - 0
app/i18n/he/user.php

@@ -0,0 +1,37 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'tos' => array(
+		'feedback' => array(
+			'invalid' => 'You must accept the Terms of Service to be able to register.', // TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/it/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 	'system' => array(
 		'_' => 'Configurazione di sistema',
 		'_' => 'Configurazione di sistema',
 		'auto-update-url' => 'Auto-update server URL',	//TODO - Translation
 		'auto-update-url' => 'Auto-update server URL',	//TODO - Translation
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Nome istanza',
 		'instance-name' => 'Nome istanza',
 		'max-categories' => 'Limite categorie per utente',
 		'max-categories' => 'Limite categorie per utente',
 		'max-feeds' => 'Limite feeds per utente',
 		'max-feeds' => 'Limite feeds per utente',

+ 12 - 3
app/i18n/it/conf.php

@@ -3,13 +3,21 @@
 return array(
 return array(
 	'archiving' => array(
 	'archiving' => array(
 		'_' => 'Archiviazione',
 		'_' => 'Archiviazione',
-		'advanced' => 'Avanzate',
 		'delete_after' => 'Rimuovi articoli dopo',
 		'delete_after' => 'Rimuovi articoli dopo',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Altre opzioni sono disponibili nelle impostazioni dei singoli feed',
 		'help' => 'Altre opzioni sono disponibili nelle impostazioni dei singoli feed',
-		'keep_history_by_feed' => 'Numero minimo di articoli da mantenere per feed',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Numero minimo di articoli da mantenere per feed',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Ottimizza database',
 		'optimize' => 'Ottimizza database',
 		'optimize_help' => 'Da fare occasionalmente per ridurre le dimensioni del database',
 		'optimize_help' => 'Da fare occasionalmente per ridurre le dimensioni del database',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Cancella ora',
 		'purge_now' => 'Cancella ora',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Archiviazione',
 		'title' => 'Archiviazione',
 		'ttl' => 'Non effettuare aggiornamenti per più di',
 		'ttl' => 'Non effettuare aggiornamenti per più di',
 	),
 	),
@@ -21,6 +29,7 @@ return array(
 			'publication_date' => 'Data di pubblicazione',
 			'publication_date' => 'Data di pubblicazione',
 			'related_tags' => 'Tags correlati',	//TODO - Translation
 			'related_tags' => 'Tags correlati',	//TODO - Translation
 			'sharing' => 'Condivisione',
 			'sharing' => 'Condivisione',
+			'display_authors' => 'Authors',  //TODO - Translation
 			'top_line' => 'Barra in alto',
 			'top_line' => 'Barra in alto',
 		),
 		),
 		'language' => 'Lingua',
 		'language' => 'Lingua',
@@ -45,6 +54,7 @@ return array(
 			'_' => 'Cancellazione account',
 			'_' => 'Cancellazione account',
 			'warn' => 'Il tuo account e tutti i dati associati saranno cancellati.',
 			'warn' => 'Il tuo account e tutti i dati associati saranno cancellati.',
 		),
 		),
+		'email' => 'Indirizzo email',
 		'password_api' => 'Password API<br /><small>(e.g., per applicazioni mobili)</small>',
 		'password_api' => 'Password API<br /><small>(e.g., per applicazioni mobili)</small>',
 		'password_form' => 'Password<br /><small>(per il login classico)</small>',
 		'password_form' => 'Password<br /><small>(per il login classico)</small>',
 		'password_format' => 'Almeno 7 caratteri',
 		'password_format' => 'Almeno 7 caratteri',
@@ -133,7 +143,6 @@ return array(
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'more_information' => 'Ulteriori informazioni',
 		'more_information' => 'Ulteriori informazioni',
 		'print' => 'Stampa',
 		'print' => 'Stampa',
 		'remove' => 'Remove sharing method',	//TODO - Translation
 		'remove' => 'Remove sharing method',	//TODO - Translation

+ 10 - 1
app/i18n/it/gen.php

@@ -3,6 +3,7 @@
 return array(
 return array(
 	'action' => array(
 	'action' => array(
 		'actualize' => 'Aggiorna',
 		'actualize' => 'Aggiorna',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← Indietro',
 		'back_to_rss_feeds' => '← Indietro',
 		'cancel' => 'Annulla',
 		'cancel' => 'Annulla',
 		'create' => 'Crea',
 		'create' => 'Crea',
@@ -22,6 +23,7 @@ return array(
 		'update' => 'Update', // TODO
 		'update' => 'Update', // TODO
 	),
 	),
 	'auth' => array(
 	'auth' => array(
+		'accept_tos' => 'I accept the <a href="%s">Terms of Service</a>.', // TODO - Translation
 		'email' => 'Indirizzo email',
 		'email' => 'Indirizzo email',
 		'keep_logged_in' => 'Ricorda i dati <small>(%s giorni)</small>',
 		'keep_logged_in' => 'Ricorda i dati <small>(%s giorni)</small>',
 		'login' => 'Accedi',
 		'login' => 'Accedi',
@@ -160,15 +162,22 @@ return array(
 		'nothing_to_load' => 'Non ci sono altri articoli',
 		'nothing_to_load' => 'Non ci sono altri articoli',
 		'previous' => 'Precedente',
 		'previous' => 'Precedente',
 	),
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
 		'facebook' => 'Facebook',
 		'facebook' => 'Facebook',
-		'g+' => 'Google+',
 		'gnusocial' => 'GNU social',
 		'gnusocial' => 'GNU social',
 		'jdh' => 'Journal du hacker',
 		'jdh' => 'Journal du hacker',
 		'Known' => 'Siti basati su Known',
 		'Known' => 'Siti basati su Known',
+		'lemmy' => 'Lemmy',
 		'linkedin' => 'LinkedIn',
 		'linkedin' => 'LinkedIn',
 		'mastodon' => 'Mastodon',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
 		'movim' => 'Movim',

+ 4 - 1
app/i18n/it/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Bugs',
 		'bugs_reports' => 'Bugs',
 		'credits' => 'Crediti',
 		'credits' => 'Crediti',
 		'credits_content' => 'Alcuni elementi di design provengono da <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> sebbene FreshRSS non usi questo framework. Le <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icone</a> provengono dal progetto <a href="https://www.gnome.org/">GNOME</a>. Il carattere <em>Open Sans</em> è stato creato da <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS è basato su <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
 		'credits_content' => 'Alcuni elementi di design provengono da <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> sebbene FreshRSS non usi questo framework. Le <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icone</a> provengono dal progetto <a href="https://www.gnome.org/">GNOME</a>. Il carattere <em>Open Sans</em> è stato creato da <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS è basato su <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
-		'freshrss_description' => 'FreshRSS è un aggregatore di feeds RSS da installare sul proprio host come <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> o <a href="http://leed.idleman.fr/">Leed</a>. Leggero e facile da mantenere pur essendo molto configurabile e potente.',
+		'freshrss_description' => 'FreshRSS è un aggregatore di feeds RSS da installare sul proprio host come <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> o <a href="https://github.com/LeedRSS/Leed">Leed</a>. Leggero e facile da mantenere pur essendo molto configurabile e potente.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">su Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">su Github</a>',
 		'license' => 'Licenza',
 		'license' => 'Licenza',
 		'project_website' => 'Sito del progetto',
 		'project_website' => 'Sito del progetto',
@@ -15,6 +15,9 @@ return array(
 		'version' => 'Versione',
 		'version' => 'Versione',
 		'website' => 'Sito',
 		'website' => 'Sito',
 	),
 	),
+	'tos' => array(
+		'title' => 'Terms of Service', // TODO - Translation
+	),
 	'feed' => array(
 	'feed' => array(
 		'add' => 'Aggiungi un Feed RSS',
 		'add' => 'Aggiungi un Feed RSS',
 		'empty' => 'Non ci sono articoli da mostrare.',
 		'empty' => 'Non ci sono articoli da mostrare.',

+ 5 - 1
app/i18n/it/sub.php

@@ -13,9 +13,12 @@ return array(
 	'category' => array(
 	'category' => array(
 		'_' => 'Categoria',
 		'_' => 'Categoria',
 		'add' => 'Aggiungi una categoria',
 		'add' => 'Aggiungi una categoria',
+		'archiving' => 'Archiviazione',
 		'empty' => 'Categoria vuota',
 		'empty' => 'Categoria vuota',
 		'information' => 'Informazioni',
 		'information' => 'Informazioni',
 		'new' => 'Nuova categoria',
 		'new' => 'Nuova categoria',
+		'position' => 'Display position',	//TODO - Translation
+		'position_help' => 'To control category sort order',	//TODO - Translation
 		'title' => 'Titolo',
 		'title' => 'Titolo',
 	),
 	),
 	'feed' => array(
 	'feed' => array(
@@ -40,7 +43,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		),
 		'information' => 'Informazioni',
 		'information' => 'Informazioni',
-		'keep_history' => 'Numero minimo di articoli da mantenere',
+		'keep_min' => 'Numero minimo di articoli da mantenere',
 		'moved_category_deleted' => 'Cancellando una categoria i feed al suo interno verranno classificati automaticamente come <em>%s</em>.',
 		'moved_category_deleted' => 'Cancellando una categoria i feed al suo interno verranno classificati automaticamente come <em>%s</em>.',
 		'mute' => 'mute',	//TODO - Translation
 		'mute' => 'mute',	//TODO - Translation
 		'no_selected' => 'Nessun feed selezionato.',
 		'no_selected' => 'Nessun feed selezionato.',
@@ -72,6 +75,7 @@ return array(
 	),
 	),
 	'firefox' => array(
 	'firefox' => array(
 		'documentation' => 'Follow the steps described <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">here</a> to add FreshRSS to Firefox feed reader list.',	//TODO - Translation
 		'documentation' => 'Follow the steps described <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">here</a> to add FreshRSS to Firefox feed reader list.',	//TODO - Translation
+		'obsolete_63' => 'From version 63 and onwards, Firefox has removed the ability to add your own subscription services that are not standalone programs.',	//TODO - Translation
 		'title' => 'Firefox feed reader',	//TODO - Translation
 		'title' => 'Firefox feed reader',	//TODO - Translation
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(

+ 37 - 0
app/i18n/it/user.php

@@ -0,0 +1,37 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'tos' => array(
+		'feedback' => array(
+			'invalid' => 'You must accept the Terms of Service to be able to register.', // TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است