Merge pull request #2599 from FreshRSS/dev

FreshRSS 1.15
pull/2628/head 1.15.0
Alexandre Alapetite 5 years ago committed by GitHub
commit 3aa66f317b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .github/FUNDING.yml
  2. 4
      .stylelintignore
  3. 74
      .stylelintrc
  4. 17
      .travis.yml
  5. 63
      CHANGELOG.md
  6. 2
      CONTRIBUTING.md
  7. 11
      CREDITS.md
  8. 7
      Docker/Dockerfile
  9. 5
      Docker/Dockerfile-Alpine
  10. 5
      Docker/Dockerfile-QEMU-ARM
  11. 83
      Docker/README.md
  12. 39
      Docker/docker-compose.yml
  13. 3
      Docker/entrypoint.sh
  14. 38
      Makefile
  15. 25
      README.fr.md
  16. 100
      README.md
  17. 14
      app/.htaccess
  18. 14
      app/Controllers/authController.php
  19. 59
      app/Controllers/configureController.php
  20. 22
      app/Controllers/entryController.php
  21. 4
      app/Controllers/extensionController.php
  22. 28
      app/Controllers/feedController.php
  23. 22
      app/Controllers/importExportController.php
  24. 21
      app/Controllers/indexController.php
  25. 2
      app/Controllers/javascriptController.php
  26. 67
      app/Controllers/subscriptionController.php
  27. 4
      app/Controllers/tagController.php
  28. 2
      app/Controllers/updateController.php
  29. 276
      app/Controllers/userController.php
  30. 22
      app/FreshRSS.php
  31. 31
      app/Mailers/UserMailer.php
  32. 7
      app/Models/Auth.php
  33. 31
      app/Models/Category.php
  34. 249
      app/Models/CategoryDAO.php
  35. 17
      app/Models/CategoryDAOSQLite.php
  36. 17
      app/Models/ConfigurationSetter.php
  37. 18
      app/Models/Context.php
  38. 244
      app/Models/DatabaseDAO.php
  39. 47
      app/Models/DatabaseDAOPGSQL.php
  40. 36
      app/Models/DatabaseDAOSQLite.php
  41. 12
      app/Models/Entry.php
  42. 447
      app/Models/EntryDAO.php
  43. 28
      app/Models/EntryDAOPGSQL.php
  44. 92
      app/Models/EntryDAOSQLite.php
  45. 10
      app/Models/Factory.php
  46. 55
      app/Models/Feed.php
  47. 208
      app/Models/FeedDAO.php
  48. 4
      app/Models/FeedDAOSQLite.php
  49. 47
      app/Models/StatsDAO.php
  50. 5
      app/Models/StatsDAOPGSQL.php
  51. 5
      app/Models/StatsDAOSQLite.php
  52. 2
      app/Models/Tag.php
  53. 139
      app/Models/TagDAO.php
  54. 2
      app/Models/TagDAOSQLite.php
  55. 83
      app/Models/UserDAO.php
  56. 104
      app/SQL/install.sql.mysql.php
  57. 101
      app/SQL/install.sql.pgsql.php
  58. 87
      app/SQL/install.sql.sqlite.php
  59. 1
      app/i18n/cz/admin.php
  60. 15
      app/i18n/cz/conf.php
  61. 11
      app/i18n/cz/gen.php
  62. 5
      app/i18n/cz/index.php
  63. 6
      app/i18n/cz/sub.php
  64. 37
      app/i18n/cz/user.php
  65. 1
      app/i18n/de/admin.php
  66. 15
      app/i18n/de/conf.php
  67. 11
      app/i18n/de/gen.php
  68. 5
      app/i18n/de/index.php
  69. 6
      app/i18n/de/sub.php
  70. 37
      app/i18n/de/user.php
  71. 1
      app/i18n/en/admin.php
  72. 15
      app/i18n/en/conf.php
  73. 12
      app/i18n/en/gen.php
  74. 5
      app/i18n/en/index.php
  75. 6
      app/i18n/en/sub.php
  76. 37
      app/i18n/en/user.php
  77. 1
      app/i18n/es/admin.php
  78. 15
      app/i18n/es/conf.php
  79. 11
      app/i18n/es/gen.php
  80. 5
      app/i18n/es/index.php
  81. 6
      app/i18n/es/sub.php
  82. 37
      app/i18n/es/user.php
  83. 1
      app/i18n/fr/admin.php
  84. 15
      app/i18n/fr/conf.php
  85. 11
      app/i18n/fr/gen.php
  86. 5
      app/i18n/fr/index.php
  87. 6
      app/i18n/fr/sub.php
  88. 37
      app/i18n/fr/user.php
  89. 1
      app/i18n/he/admin.php
  90. 15
      app/i18n/he/conf.php
  91. 11
      app/i18n/he/gen.php
  92. 5
      app/i18n/he/index.php
  93. 6
      app/i18n/he/sub.php
  94. 37
      app/i18n/he/user.php
  95. 1
      app/i18n/it/admin.php
  96. 15
      app/i18n/it/conf.php
  97. 11
      app/i18n/it/gen.php
  98. 5
      app/i18n/it/index.php
  99. 6
      app/i18n/it/sub.php
  100. 37
      app/i18n/it/user.php
  101. Some files were not shown because too many files have changed in this diff Show More

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

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

@ -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"
}
}

@ -1,11 +1,6 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- 7.3
install:
@ -14,7 +9,7 @@ install:
script:
- 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 [[ $VALIDATE_STANDARD == yes ]]; then
@ -32,9 +27,6 @@ env:
matrix:
fast_finish: true
include:
# PHP 5.3 only runs on Ubuntu 12.04 (precise), not 14.04 (trusty)
- php: "5.3"
dist: precise
- php: "7.2"
env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
- language: node_js
@ -45,12 +37,15 @@ matrix:
env:
- HADOLINT="$HOME/hadolint"
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}
script:
- node_modules/jshint/bin/jshint .
# check SCSS separately
- stylelint --syntax scss "**/*.scss"
- stylelint "**/*.css"
- bash tests/shellchecks.sh
- git ls-files --exclude='*Dockerfile*' --ignored | xargs --max-lines=1 "$HADOLINT"
allow_failures:
- env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
- dist: precise

@ -1,5 +1,64 @@
# 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
* UI
@ -280,8 +339,8 @@
* API
* 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
* 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)

@ -21,7 +21,7 @@ If you have to create a new ticket, try to apply the following advices:
- We also need some information:
+ Your FreshRSS version (on about page or `constants.php` file)
+ 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`)
## Fix a bug

@ -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)
* [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/)
* [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)
* [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)
@ -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)
* [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/)
* [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)
* [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/)
@ -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/)
* [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)
* [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/)
* [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)
@ -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 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/)
* [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)
* [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)
* [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)
* [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/)
* [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)
* [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)
* [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/)
@ -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 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)
* [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)
* [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)
* [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/)

@ -1,4 +1,4 @@
FROM ubuntu:19.04
FROM ubuntu:19.10
ENV TZ UTC
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 && \
sed -r -i "/^\s*Listen /s/^/#/" /etc/apache2/ports.conf && \
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' \
2>> /proc/1/fd/2 > /tmp/FreshRSS.log" | crontab -
ENV COPY_LOG_TO_SYSLOG On
ENV COPY_SYSLOG_TO_STDERR On
ENV CRON_MIN ''
ENV FRESHRSS_ENV ''
ENTRYPOINT ["./Docker/entrypoint.sh"]
EXPOSE 80

@ -5,7 +5,7 @@ SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN apk add --no-cache \
apache2 php7-apache2 \
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
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' \
2>> /proc/1/fd/2 > /tmp/FreshRSS.log" | crontab -
ENV COPY_LOG_TO_SYSLOG On
ENV COPY_SYSLOG_TO_STDERR On
ENV CRON_MIN ''
ENV FRESHRSS_ENV ''
ENTRYPOINT ["./Docker/entrypoint.sh"]
EXPOSE 80

@ -1,7 +1,7 @@
# Only relevant for Docker Hub or QEMU multi-architecture builds.
# 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/*
COPY ./Docker/qemu-arm-* /usr/bin/
@ -59,8 +59,11 @@ RUN update-ca-certificates -f
# Useful with the `--squash` build option
RUN rm /usr/bin/qemu-* /var/www/FreshRSS/Docker/qemu-*
ENV COPY_LOG_TO_SYSLOG On
ENV COPY_SYSLOG_TO_STDERR On
ENV CRON_MIN ''
ENV FRESHRSS_ENV ''
ENTRYPOINT ["./Docker/entrypoint.sh"]
EXPOSE 80

@ -17,7 +17,7 @@ sh get-docker.sh
## Create an isolated network
```sh
sudo docker network create freshrss-network
docker network create freshrss-network
```
## 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.
```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:
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-tmp:/tmp \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
--net freshrss-network \
-p 80:80 \
-p 443:443 \
--name traefik traefik --docker \
--name traefik traefik:1.7 --docker \
--loglevel=info \
--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' \
@ -55,10 +55,10 @@ 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.
```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:
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 \
-e 'CRON_MIN=4,34' \
-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:
### [MySQL](https://hub.docker.com/_/mysql/)
### [MySQL](https://hub.docker.com/_/mysql/) or [MariaDB](https://hub.docker.com/_/mariadb)
```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:
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 \
-e MYSQL_ROOT_PASSWORD=rootpass
-e MYSQL_ROOT_PASSWORD=rootpass \
-e MYSQL_DATABASE=freshrss \
-e MYSQL_USER=freshrss \
-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/)
```sh
# 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:
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 \
-e POSTGRES_DB=freshrss \
-e POSTGRES_USER=freshrss \
@ -121,14 +121,14 @@ or use the command line described below.
```sh
# Rebuild an image (see build section above) or get a new online version:
sudo docker pull freshrss/freshrss
docker pull freshrss/freshrss
# 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
sudo docker run ... --name freshrss freshrss/freshrss
docker run ... --name freshrss freshrss/freshrss
# 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
git clone https://github.com/FreshRSS/FreshRSS.git
cd ./FreshRSS/
cd FreshRSS/
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
```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.
@ -173,14 +172,14 @@ See the [CLI documentation](../cli/) for all the other commands.
```sh
# 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/
# See Web server logs
sudo docker logs -f freshrss
docker logs -f freshrss
# Enter inside FreshRSS docker container
sudo docker exec -it freshrss sh
docker exec -it freshrss sh
## See FreshRSS root inside the container
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.
```sh
sudo docker run ... \
docker run ... \
-e 'CRON_MIN=13,43' \
--name freshrss freshrss/freshrss
```
@ -221,7 +220,7 @@ See cron option 1 for customising the cron schedule.
#### For the Ubuntu image (default)
```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 \
-e 'CRON_MIN=17,47' \
--net freshrss-network \
@ -231,7 +230,7 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
#### For the Alpine image
```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 \
-e 'CRON_MIN=27,57' \
--net freshrss-network \
@ -239,6 +238,22 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
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
@ -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`:
```
sudo docker run ...
docker run ...
-v /your/.htaccess:/var/www/FreshRSS/p/i/.htaccess \
-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:
```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/)
@ -313,7 +328,7 @@ server {
}
location /freshrss/ {
proxy_pass http://freshrss/;
proxy_pass http://freshrss;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
proxy_redirect off;

@ -1,38 +1,31 @@
version: '2.3'
version: "3"
services:
postgresql:
image: postgres:latest
freshrss_postgresql:
image: postgres
restart: unless-stopped
volumes:
- '/path/to/pgsql-data:/var/lib/postgresql/data'
- pgsql_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=freshrss
- POSTGRES_PASSWORD=password
- POSTGRES_PASSWORD=freshrss
- POSTGRES_DB=freshrss
freshrss:
image: freshrss/freshrss:latest
image: freshrss/freshrss
restart: unless-stopped
ports:
- "8080:80"
depends_on:
- postgresql
networks:
- web
- default
- freshrss_postgresql
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:
- CRON_MIN=*/20
- TZ=Europe/Copenhagen
labels:
- "traefik.port=80"
networks:
web:
external: true
volumes:
pgsql_data:
freshrss_data:

@ -6,10 +6,13 @@ chown -R :www-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 "\\#^;?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
(
echo "export TZ=$TZ"
echo "export COPY_LOG_TO_SYSLOG=$COPY_LOG_TO_SYSLOG"
echo "export COPY_SYSLOG_TO_STDERR=$COPY_SYSLOG_TO_STDERR"
) >/var/www/FreshRSS/Docker/env.txt
crontab -l | sed -r "\\#FreshRSS#s#^[^ ]+ #$CRON_MIN #" | crontab -

@ -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}'

@ -5,7 +5,7 @@
* [English version](README.md)
# 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.
@ -43,10 +43,10 @@ FreshRSS n’est fourni avec aucune garantie.
* 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)
* 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 dencodages), [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 dencodages), [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
@ -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
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
* 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.
@ -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/))
* GNU/Linux
* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre)
* iOS
* [Reeder](https://www.reederapp.com/) (Commercial)
* MacOS
* [Vienna RSS](http://www.vienna-rss.com/) (Libre)
* [Reeder](https://www.reederapp.com/) (Commercial)
## Via l’API compatible Fever
@ -199,11 +202,10 @@ Tout client supportant une API de type Fever ; Sélection :
* Android
* [Readably](https://play.google.com/store/apps/details?id=com.isaiasmatewos.readably) (Propriétaire)
* 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
* [Readkit](https://itunes.apple.com/app/readkit/id588726889) (Propriétaire)
* [Readkit](https://apps.apple.com/app/readkit/id588726889) (Commercial)
# Bibliothèques incluses
@ -213,12 +215,11 @@ Tout client supportant une API de type Fever ; Sélection :
* [jQuery](https://jquery.com/)
* [lib_opml](https://github.com/marienfressinaud/lib_opml)
* [flotr2](http://www.humblesoftware.com/flotr2)
* [PHPMailer](https://github.com/PHPMailer/PHPMailer)
## Uniquement pour certaines options ou configurations
* [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
* [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)

@ -5,7 +5,7 @@
* [Version française](README.fr.md)
# 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.
@ -43,10 +43,10 @@ FreshRSS comes with absolutely no warranty.
* 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)
* 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
@ -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).
### 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
* 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.
@ -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.
# 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
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).
@ -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/))
* GNU/Linux
* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source)
* iOS
* [Reeder](https://www.reederapp.com/) (Commercial)
* MacOS
* [Vienna RSS](http://www.vienna-rss.com/) (Open source)
* [Reeder](https://www.reederapp.com/) (Commercial)
## Fever API
@ -199,11 +125,10 @@ Supported clients are:
* Android
* [Readably](https://play.google.com/store/apps/details?id=com.isaiasmatewos.readably) (Closed source)
* 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
* [Readkit](https://itunes.apple.com/app/readkit/id588726889) (Closed source)
* [Readkit](https://apps.apple.com/app/readkit/id588726889) (Commercial)
# Included libraries
@ -213,12 +138,11 @@ Supported clients are:
* [jQuery](https://jquery.com/)
* [lib_opml](https://github.com/marienfressinaud/lib_opml)
* [flotr2](http://www.humblesoftware.com/flotr2)
* [PHPMailer](https://github.com/PHPMailer/PHPMailer)
## Only for some options or configurations
* [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
* [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-link]:https://travis-ci.org/FreshRSS/FreshRSS

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

@ -169,10 +169,6 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
return;
}
if (!function_exists('password_verify')) {
include_once(LIB_PATH . '/password_compat.php');
}
$s = $conf->passwordHash;
$ok = password_verify($password, $s);
unset($password);
@ -203,12 +199,22 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
/**
* 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() {
if (FreshRSS_Auth::hasAccess()) {
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
}
if (max_registrations_reached()) {
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') . ' · ');
}
}

@ -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_date = Minz_Request::param('topline_date', 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_favorite = Minz_Request::param('bottomline_favorite', 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.
*/
public function shortcutAction() {
global $SHORTCUT_KEYS;
$this->view->list_keys = $SHORTCUT_KEYS;
$this->view->list_keys = SHORTCUT_KEYS;
if (Minz_Request::isPost()) {
FreshRSS_Context::$user_conf->shortcuts = validateShortcutList(Minz_Request::param('shortcuts'));
@ -196,9 +196,31 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
*/
public function archivingAction() {
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->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();
invalidateHttpCache();
@ -206,7 +228,20 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
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();
$this->view->nb_total = $entryDAO->count();
@ -217,6 +252,8 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
if (FreshRSS_Auth::hasAccess('admin')) {
$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.
*
* 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 category limit (default: 16384)
* - user feed limit (default: 16384)
* - user login duration for form auth (default: 2592000)
*
* The `force-email-validation` is ignored with PHP < 5.5
*/
public function systemAction() {
if (!FreshRSS_Auth::hasAccess('admin')) {
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()) {
$limits = FreshRSS_Context::$system_conf->limits;
$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->title = Minz_Request::param('instance-name', 'FreshRSS');
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();
invalidateHttpCache();

@ -17,7 +17,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
// If ajax request, we do not print layout
$this->ajax = Minz_Request::param('ajax');
if ($this->ajax) {
$this->view->_useLayout(false);
$this->view->_layout(false);
Minz_Request::_param('ajax');
}
}
@ -181,32 +181,20 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
public function purgeAction() {
@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();
$feeds = $feedDAO->listFeeds();
$nb_total = 0;
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->commit();
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
$databaseDAO->minorDbMaintenance();

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

@ -267,10 +267,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
$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
$pubsubhubbubEnabledGeneral = FreshRSS_Context::$system_conf->pubsubhubbub_enabled;
$pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration.
@ -323,12 +319,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
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;
// We want chronological order and SimplePie uses reverse order.
@ -376,15 +366,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
}
$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 {
$id = uTimeString();
$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();
@ -413,23 +397,19 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
$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()) {
$entryDAO->beginTransaction();
}
$nb = $entryDAO->cleanOldEntries($feed->id(), $date_min, max($feed_history, count($entries) + 10));
$nb = $feed->cleanOldEntries();
if ($nb > 0) {
$needFeedCacheRefresh = true;
Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url(false) . ']');
}
}
$feedDAO->updateLastUpdate($feed->id(), false, $mtime);
if ($needFeedCacheRefresh) {
$feedDAO->updateCachedValue($feed->id());
$feedDAO->updateCachedValues($feed->id());
}
if ($entryDAO->inTransaction()) {
$entryDAO->commit();
@ -530,7 +510,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
);
Minz_Session::_param('notification', $notif);
// No layout in ajax request.
$this->view->_useLayout(false);
$this->view->_layout(false);
} else {
// Redirect to the main page with correct notification.
if ($updated_feeds === 1) {

@ -29,7 +29,25 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
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) {
self::minimumMemory(256);
require_once(LIB_PATH . '/lib_opml.php');
$this->catDAO = new FreshRSS_CategoryDAO($username);
@ -709,8 +727,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
$this->entryDAO->disableBuffering();
if ($export_feeds === true) {
//All feeds
$export_feeds = $this->feedDAO->listFeedsIds();
@ -773,7 +789,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
if (!Minz_Request::isPost()) {
Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
}
$this->view->_useLayout(false);
$this->view->_layout(false);
$nb_files = 0;
try {

@ -155,7 +155,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
// No layout for RSS output.
$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->_useLayout(false);
$this->view->_layout(false);
header('Content-Type: application/rss+xml; charset=utf-8');
}
@ -173,7 +173,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
private function updateContext() {
if (empty(FreshRSS_Context::$categories)) {
$catDAO = FreshRSS_Factory::createCategoryDao();
FreshRSS_Context::$categories = $catDAO->listCategories();
FreshRSS_Context::$categories = $catDAO->listSortedCategories();
}
// Update number of read / unread variables.
@ -259,6 +259,23 @@ class FreshRSS_index_Controller extends Minz_ActionController {
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.
*/

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

@ -19,7 +19,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
$catDAO->checkDefault();
$feedDAO->updateTTL();
$this->view->categories = $catDAO->listCategories(false);
$this->view->categories = $catDAO->listSortedCategories(false);
$this->view->default_category = $catDAO->getDefault();
}
@ -74,7 +74,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
*/
public function feedAction() {
if (Minz_Request::param('ajax')) {
$this->view->_useLayout(false);
$this->view->_layout(false);
}
$feedDAO = FreshRSS_Factory::createFeedDao();
@ -121,6 +121,32 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
$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', '')));
$values = array(
@ -132,7 +158,6 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
'pathEntries' => Minz_Request::param('path_entries', ''),
'priority' => intval(Minz_Request::param('priority', FreshRSS_Feed::PRIORITY_MAIN_STREAM)),
'httpAuth' => $httpAuth,
'keep_history' => intval(Minz_Request::param('keep_history', FreshRSS_Feed::KEEP_HISTORY_DEFAULT)),
'ttl' => $ttl * ($mute ? -1 : 1),
'attributes' => $feed->attributes(),
);
@ -152,7 +177,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
}
public function categoryAction() {
$this->view->_useLayout(false);
$this->view->_layout(false);
$categoryDAO = FreshRSS_Factory::createCategoryDao();
@ -165,9 +190,39 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
$this->view->category = $category;
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', ''),
);
'attributes' => $category->attributes(),
];
invalidateHttpCache();

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

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

@ -8,26 +8,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
// so do not use a too high cost
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) {
if (!function_exists('password_hash')) {
include_once(LIB_PATH . '/password_compat.php');
}
$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
$passwordPlain = '';
$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
@ -52,12 +33,23 @@ class FreshRSS_user_Controller extends Minz_ActionController {
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);
if ($userConfig === null) {
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 != '') {
$passwordHash = self::hashPassword($passwordPlain);
$userConfig->passwordHash = $passwordHash;
@ -103,7 +95,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
$apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true);
$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),
));
@ -126,25 +118,63 @@ class FreshRSS_user_Controller extends Minz_ActionController {
* This action displays the user profile page.
*/
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::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
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);
Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP
$_POST['newPasswordPlain'] = '';
$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),
));
)
);
Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash);
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'));
} else {
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') . ' · ');
$this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation;
$this->view->current_user = Minz_Request::param('u');
$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);
@ -206,9 +247,9 @@ class FreshRSS_user_Controller extends Minz_ActionController {
$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
}
if ($ok) {
$userDAO = new FreshRSS_UserDAO();
$ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds);
$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;
}
@ -219,6 +260,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
* Request parameters are:
* - new_user_language
* - new_user_name
* - new_user_email
* - new_user_passwordPlain
* - 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?
*/
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');
$email = Minz_Request::param('new_user_email', '');
$passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
$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
$_POST['new_user_passwordPlain'] = '';
invalidateHttpCache();
@ -266,9 +336,6 @@ class FreshRSS_user_Controller extends Minz_ActionController {
}
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);
if ($ok) {
$default_user = FreshRSS_Context::$system_conf->default_user;
@ -278,14 +345,130 @@ class FreshRSS_user_Controller extends Minz_ActionController {
$ok &= is_dir($user_data);
if ($ok) {
self::deleteFeverKey($username);
$userDAO = new FreshRSS_UserDAO();
$ok &= $userDAO->deleteUser($username);
$oldUserDAO = FreshRSS_Factory::createUserDao($username);
$ok &= $oldUserDAO->deleteUser();
$ok &= recursive_unlink($user_data);
array_map('unlink', glob(PSHB_PATH . '/feeds/*/' . $username . '.txt'));
}
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.
*
@ -296,17 +479,18 @@ class FreshRSS_user_Controller extends Minz_ActionController {
*/
public function deleteAction() {
$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));
if (!$redirect_url) {
$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;
if ($ok && $self_deletion) {
// We check the password if it's a self-destruction

@ -53,6 +53,10 @@ class FreshRSS extends Minz_FrontController {
$ext_list = FreshRSS_Context::$user_conf->extensions_enabled;
Minz_ExtensionManager::enableByList($ext_list);
}
self::checkEmailValidated();
Minz_ExtensionManager::callHook('freshrss_init');
}
private static function initAuth() {
@ -142,4 +146,22 @@ class FreshRSS extends Minz_FrontController {
FreshRSS_Share::load(join_path(APP_PATH, 'shares.php'));
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);
}
}
}

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

@ -219,10 +219,6 @@ class FreshRSS_FormAuth {
return false;
}
if (!function_exists('password_verify')) {
include_once(LIB_PATH . '/password_compat.php');
}
return password_verify($nonce . $hash, $challenge);
}
@ -283,8 +279,7 @@ class FreshRSS_FormAuth {
$cookie_duration = empty($limits['cookie_duration']) ? 2592000 : $limits['cookie_duration'];
$oldest = time() - $cookie_duration;
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) {
@unlink($file_info->getPathname());
}

@ -8,6 +8,7 @@ class FreshRSS_Category extends Minz_Model {
private $feeds = null;
private $hasFeedsWithError = false;
private $isDefault = false;
private $attributes = [];
public function __construct($name = '', $feeds = null) {
$this->_name($name);
@ -68,8 +69,19 @@ class FreshRSS_Category extends Minz_Model {
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) {
$this->name = trim($value);
@ -84,4 +96,19 @@ class FreshRSS_Category extends Minz_Model {
$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;
}
}
}

@ -4,23 +4,92 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
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) {
$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');
if (!isset($valuesTmp['attributes'])) {
$valuesTmp['attributes'] = [];
}
$values = array(
$valuesTmp['name'],
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
$valuesTmp['name'],
);
if ($stm && $stm->execute($values)) {
return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"');
return $this->pdo->lastInsertId('`_category_id_seq`');
} 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;
}
}
@ -39,13 +108,17 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
}
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');
if (!isset($valuesTmp['attributes'])) {
$valuesTmp['attributes'] = [];
}
$values = array(
$valuesTmp['name'],
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
$id,
$valuesTmp['name'],
);
@ -53,8 +126,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} 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;
}
}
@ -63,27 +139,42 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
if ($id <= self::DEFAULTCATEGORYID) {
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();
} 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;
}
}
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);
$cat = self::daoToCategory($res);
@ -94,15 +185,15 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
}
}
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);
$cat = self::daoToCategory($res);
if (isset($cat[0])) {
return $cat[0];
} 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) {
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 ')
. '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 '
. 'GROUP BY f.id, c_id '
. 'ORDER BY c.name, f.name';
$stm = $this->bd->prepare($sql);
$stm->execute(array(':priority_normal' => FreshRSS_Feed::PRIORITY_NORMAL));
$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 {
$sql = 'SELECT * FROM `' . $this->prefix . 'category` ORDER BY name';
$stm = $this->bd->prepare($sql);
$stm->execute();
$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 {
$sql = 'SELECT * FROM `_category` ORDER BY name';
$stm = $this->pdo->query($sql);
return self::daoToCategory($stm->fetchAll(PDO::FETCH_ASSOC));
}
}
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();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$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->_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()
$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(
$cat->id(),
@ -168,9 +290,9 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
);
if ($stm && $stm->execute($values)) {
return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"');
return $this->pdo->lastInsertId('`_category_id_seq`');
} 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));
return false;
}
@ -179,31 +301,27 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
}
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);
return $res[0]['count'];
}
public function countFeed($id) {
$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'feed` WHERE category=?';
$stm = $this->bd->prepare($sql);
$values = array($id);
$stm->execute($values);
$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);
return $res[0]['count'];
}
public function countNotRead($id) {
$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE category=? AND e.is_read=0';
$stm = $this->bd->prepare($sql);
$values = array($id);
$stm->execute($values);
$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);
return $res[0]['count'];
}
@ -248,6 +366,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
);
$cat->_id($previousLine['c_id']);
$cat->_attributes('', $previousLine['c_attributes']);
$list[$previousLine['c_id']] = $cat;
$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'])
);
$cat->_id($previousLine['c_id']);
$cat->_attributes('', $previousLine['c_attributes']);
$list[$previousLine['c_id']] = $cat;
}
@ -282,6 +402,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
$dao['name']
);
$cat->_id($dao['id']);
$cat->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : '');
$cat->_isDefault(static::DEFAULTCATEGORYID === intval($dao['id']));
$list[$key] = $cat;
}

@ -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;
}
}

@ -79,11 +79,6 @@ class FreshRSS_ConfigurationSetter {
$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!
private function _language(&$data, $value) {
$value = strtolower($value);
@ -94,11 +89,6 @@ class FreshRSS_ConfigurationSetter {
$data['language'] = $value;
}
private function _old_entries(&$data, $value) {
$value = intval($value);
$data['old_entries'] = $value > 0 ? $value : 3;
}
private function _passwordHash(&$data, $value) {
$data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
}
@ -257,6 +247,9 @@ class FreshRSS_ConfigurationSetter {
private function _topline_read(&$data, $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.
@ -386,4 +379,8 @@ class FreshRSS_ConfigurationSetter {
$data['auto_update_url'] = $value;
}
private function _force_email_validation(&$data, $value) {
$data['force_email_validation'] = $this->handleBool($value);
}
}

@ -51,6 +51,24 @@ class FreshRSS_Context {
// Init configuration.
self::$system_conf = Minz_Configuration::get('system');
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;
}
}
/**

@ -8,25 +8,50 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
//MySQL error codes
const ER_BAD_FIELD_ERROR = '42S22';
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
//https://dev.mysql.com/doc/refman/8.0/en/innodb-restrictions.html
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() {
$sql = 'SHOW TABLES';
$stm = $this->bd->prepare($sql);
$stm->execute();
$stm = $this->pdo->query('SHOW TABLES');
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$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) {
$tables[array_pop($value)] = true;
@ -36,10 +61,8 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
}
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));
}
@ -63,7 +86,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
public function feedIsCorrect() {
return $this->checkTable('feed', array(
'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',
));
}
@ -119,9 +142,9 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
$values = array($db['base']);
if (!$all) {
$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);
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
return $res[0];
@ -132,30 +155,23 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
$tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag');
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;
}
public function ensureCaseInsensitiveGuids() {
$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
if ($this->pdo->dbType() === 'mysql') {
include(APP_PATH . '/SQL/install.sql.mysql.php');
$ok = false;
try {
$sql = sprintf(SQL_UPDATE_GUID_LATIN1_BIN, $this->prefix);
$stm = $this->bd->prepare($sql);
$ok = $stm->execute();
$ok = $this->pdo->exec($SQL_UPDATE_GUID_LATIN1_BIN) !== false; //FreshRSS 1.12
} catch (Exception $e) {
$ok = false;
Minz_Log::error('FreshRSS_DatabaseDAO::ensureCaseInsensitiveGuids error: ' . $e->getMessage());
}
Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage());
}
}
return $ok;
@ -164,4 +180,168 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
public function minorDbMaintenance() {
$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;
}
}

@ -13,18 +13,18 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
$db = FreshRSS_Context::$system_conf->db;
$dbowner = $db['user'];
$sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=?';
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
$values = array($dbowner);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$tables = array(
$this->prefix . 'category' => false,
$this->prefix . 'feed' => false,
$this->prefix . 'entry' => false,
$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) {
$tables[array_pop($value)] = true;
@ -35,8 +35,8 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
public function getSchema($table) {
$sql = 'select column_name as field, data_type as type, column_default as default, is_nullable as null from INFORMATION_SCHEMA.COLUMNS where table_name = ?';
$stm = $this->bd->prepare($sql);
$stm->execute(array($this->prefix . $table));
$stm = $this->pdo->prepare($sql);
$stm->execute(array($this->pdo->prefix() . $table));
return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
}
@ -49,12 +49,23 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
);
}
public function size($all = true) {
public function size($all = false) {
if ($all) {
$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);
$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);
return $res[0];
}
@ -64,12 +75,8 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
$tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag');
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;
}

@ -6,17 +6,16 @@
class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
public function tablesAreCorrect() {
$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);
$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) {
$tables[$value['name']] = true;
@ -27,9 +26,7 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
public function getSchema($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));
}
@ -57,15 +54,18 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
}
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() {
$sql = 'VACUUM';
$stm = $this->bd->prepare($sql);
if ($stm) {
return $stm->execute();
}
return false;
return $this->pdo->exec('VACUUM') !== false;
}
}

@ -327,7 +327,7 @@ class FreshRSS_Entry extends Minz_Model {
}
$ch = curl_init();
curl_setopt_array($ch, array(
curl_setopt_array($ch, [
CURLOPT_URL => $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'),
@ -337,13 +337,9 @@ class FreshRSS_Entry extends Minz_Model {
//CURLOPT_FAILONERROR => true;
CURLOPT_MAXREDIRS => 4,
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);
if (isset($attributes['ssl_verify'])) {
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $attributes['ssl_verify'] ? 2 : 0);

@ -3,11 +3,11 @@
class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function isCompressed() {
return parent::$sharedDbType === 'mysql';
return true;
}
public function hasNativeHex() {
return parent::$sharedDbType !== 'sqlite';
return true;
}
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
protected function addColumn($name) {
Minz_Log::warning('FreshRSS_EntryDAO::addColumn: ' . $name);
$hasTransaction = false;
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();
}
}
return false;
}
private $triedUpdateToUtf8mb4 = false;
//TODO: Move the database auto-updates to DatabaseDAO
protected function updateToUtf8mb4() {
if ($this->triedUpdateToUtf8mb4) {
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();
protected function createEntryTempTable() {
$ok = false;
$hadTransaction = $this->pdo->inTransaction();
if ($hadTransaction) {
$this->bd->commit();
$this->pdo->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());
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());
}
if ($hadTransaction) {
$this->bd->beginTransaction();
//NB: Transaction not starting. Why? (tested on PHP 7.0.8-0ubuntu and MySQL 5.7.13-0ubuntu)
$this->pdo->beginTransaction();
}
return $ok;
}
}
private function updateToMediumBlob() {
if ($this->pdo->dbType() !== 'mysql') {
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 {
$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) {
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;
}
@ -126,14 +60,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
//TODO: Move the database auto-updates to DatabaseDAO
protected function autoUpdateDb($errorInfo) {
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) {
$tagDAO = FreshRSS_Factory::createTagDao();
return $tagDAO->createTagTable(); //v1.12.0
@ -143,8 +70,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
}
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;
@ -152,9 +81,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
private $addEntryPrepared = null;
public function addEntry($valuesTmp) {
public function addEntry($valuesTmp, $useTmpTable = true) {
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')
. ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) '
. 'VALUES(:id, :guid, :title, :author, '
@ -162,7 +91,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
. ', :link, :date, :last_seen, '
. $this->sqlHexDecode(':hash')
. ', :is_read, :is_favorite, :id_feed, :tags)';
$this->addEntryPrepared = $this->bd->prepare($sql);
$this->addEntryPrepared = $this->pdo->prepare($sql);
}
if ($this->addEntryPrepared) {
$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']);
$this->addEntryPrepared->bindParam(':link', $valuesTmp['link']);
$this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
if (empty($valuesTmp['lastSeen'])) {
$valuesTmp['lastSeen'] = time();
}
$this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
$valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0;
$this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT);
@ -191,14 +122,14 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($this->hasNativeHex()) {
$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
} else {
$valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']); //hex2bin() is PHP5.4+
$valuesTmp['hashBin'] = hex2bin($valuesTmp['hash']);
$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
}
}
if ($this->addEntryPrepared && $this->addEntryPrepared->execute()) {
return true;
} 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)) {
$this->addEntryPrepared = null;
return $this->addEntry($valuesTmp);
@ -211,22 +142,26 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
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) {
$this->bd->beginTransaction();
$this->pdo->beginTransaction();
}
$result = $this->bd->exec($sql) !== false;
$result = $this->pdo->exec($sql) !== false;
if (!$hadTransaction) {
$this->bd->commit();
$this->pdo->commit();
}
return $result;
}
@ -239,7 +174,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
if ($this->updateEntryPrepared === null) {
$sql = 'UPDATE `' . $this->prefix . 'entry` '
$sql = 'UPDATE `_entry` '
. 'SET title=:title, author=:author, '
. ($this->isCompressed() ? 'content_bin=COMPRESS(:content)' : 'content=:content')
. ', 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, ')
. 'tags=:tags '
. '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);
@ -273,14 +208,14 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($this->hasNativeHex()) {
$this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
} else {
$valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']); //hex2bin() is PHP5.4+
$valuesTmp['hashBin'] = hex2bin($valuesTmp['hash']);
$this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
}
if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute()) {
return true;
} 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)) {
return $this->updateEntry($valuesTmp);
}
@ -308,16 +243,16 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return 0;
}
FreshRSS_UserDAO::touch();
$sql = 'UPDATE `' . $this->prefix . 'entry` '
$sql = 'UPDATE `_entry` '
. 'SET is_favorite=? '
. 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
$values = array($is_favorite ? 1 : 0);
$values = array_merge($values, $ids);
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} 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]);
return false;
}
@ -335,11 +270,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
* @return boolean
*/
protected function updateCacheUnreads($catId = false, $feedId = false) {
$sql = 'UPDATE `' . $this->prefix . 'feed` f '
$sql = 'UPDATE `_feed` f '
. 'LEFT OUTER JOIN ('
. 'SELECT e.id_feed, '
. 'COUNT(*) AS nbUnreads '
. 'FROM `' . $this->prefix . 'entry` e '
. 'FROM `_entry` e '
. 'WHERE e.is_read=0 '
. 'GROUP BY e.id_feed'
. ') x ON x.id_feed=f.id '
@ -358,11 +293,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$sql .= ' f.category=?';
$values[] = $catId;
}
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
if ($stm && $stm->execute($values)) {
return true;
} 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]);
return false;
}
@ -392,14 +327,14 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return $affected;
}
$sql = 'UPDATE `' . $this->prefix . 'entry` '
$sql = 'UPDATE `_entry` '
. 'SET is_read=? '
. 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
$values = array($is_read ? 1 : 0);
$values = array_merge($values, $ids);
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
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]);
return false;
}
@ -409,16 +344,16 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
return $affected;
} 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=?,'
. 'f.`cache_nbUnreads`=f.`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
. 'WHERE e.id=? AND e.is_read=?';
$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} 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]);
return false;
}
@ -453,7 +388,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
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=? '
. 'WHERE e.is_read <> ? AND e.id <= ?';
if ($onlyFavorites) {
@ -465,9 +400,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
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)))) {
$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]);
return false;
}
@ -496,16 +431,16 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
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=? '
. 'WHERE f.category=? AND e.is_read <> ? AND e.id <= ?';
$values = array($is_read ? 1 : 0, $id, $is_read ? 1 : 0, $idMax);
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)))) {
$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]);
return false;
}
@ -533,39 +468,39 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$idMax = time() . '000000';
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=? '
. 'WHERE id_feed=? AND is_read <> ? AND id <= ?';
$values = array($is_read ? 1 : 0, $id_feed, $is_read ? 1 : 0, $idMax);
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)))) {
$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);
$this->bd->rollBack();
$this->pdo->rollBack();
return false;
}
$affected = $stm->rowCount();
if ($affected > 0) {
$sql = 'UPDATE `' . $this->prefix . 'feed` '
$sql = 'UPDATE `_feed` '
. '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]);
$this->bd->rollBack();
$this->pdo->rollBack();
return false;
}
}
$this->bd->commit();
$this->pdo->commit();
return $affected;
}
@ -582,7 +517,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
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 = ? '
. 'WHERE '
. ($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);
$stm = $this->bd->prepare($sql . $search);
$stm = $this->pdo->prepare($sql . $search);
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]);
return false;
}
@ -609,48 +544,86 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
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;
if ($stm && $stm->execute()) {
//==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($params)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
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;
}
}
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) {
// un guid est unique pour un flux donné
$sql = 'SELECT id, guid, title, author, '
. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
. ', link, date, is_read, is_favorite, id_feed, tags '
. 'FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid=?';
$stm = $this->bd->prepare($sql);
$values = array(
$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);
$entries = self::daoToEntries($res);
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, '
. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
. ', link, date, is_read, is_favorite, id_feed, tags '
. 'FROM `' . $this->prefix . 'entry` WHERE id=?';
$stm = $this->bd->prepare($sql);
$values = array($id);
$stm->execute($values);
. '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);
$entries = self::daoToEntries($res);
return isset($entries[0]) ? $entries[0] : null;
}
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);
return isset($res[0]) ? $res[0] : null;
}
@ -859,7 +831,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$where .= '1=1 ';
break;
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;
default:
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),
'SELECT '
. ($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
. $search
. '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, '
. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
. ', 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 ('
. $sql
. ') e2 ON e2.id=e0.id '
. 'ORDER BY e0.id ' . $order;
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
if ($stm && $stm->execute($values)) {
return $stm;
} 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]);
return false;
}
@ -918,11 +890,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$sql = 'SELECT id, guid, title, author, '
. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
. ', link, date, is_read, is_favorite, id_feed, tags '
. 'FROM `' . $this->prefix . 'entry` '
. 'FROM `_entry` '
. 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?) '
. 'ORDER BY id ' . $order;
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
$stm->execute($ids);
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
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);
return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
@ -941,8 +913,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return array();
}
$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_merge($values, $guids);
if ($stm && $stm->execute($values)) {
@ -953,7 +925,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
return $result;
} else {
$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->listHashForFeedGuids($id_feed, $guids);
}
@ -967,8 +939,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if (count($guids) < 1) {
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) {
$mtime = time();
}
@ -977,7 +949,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->updateLastSeen($id_feed, $guids);
}
@ -988,65 +960,70 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
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);
rsort($res);
$all = empty($res[0]) ? 0 : $res[0];
$unread = empty($res[1]) ? 0 : $res[1];
return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
}
public function count($minPriority = null) {
$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e';
$sql = 'SELECT COUNT(e.id) AS count FROM `_entry` e';
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);
}
$stm = $this->bd->prepare($sql);
$stm->execute();
$stm = $this->pdo->query($sql);
if ($stm == false) {
return false;
}
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
return isset($res[0]) ? $res[0] : 0;
}
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) {
$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';
if ($minPriority !== null) {
$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);
return $res[0];
}
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
$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_normal
AND f1.priority >= :priority_normal1
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
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_normal
AND f2.priority >= :priority_normal2
) u
ORDER BY o
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);
rsort($res);
$all = empty($res[0]) ? 0 : $res[0];

@ -2,6 +2,10 @@
class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
public function hasNativeHex() {
return true;
}
public function sqlHexDecode($x) {
return 'decode(' . $x . ", 'hex')";
}
@ -31,25 +35,27 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
public function commitNewEntries() {
$sql = 'DO $$
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
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 (
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))
ORDER BY date);
DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= maxrank;
DELETE FROM `_entrytmp` WHERE id <= maxrank;
END $$;';
$hadTransaction = $this->bd->inTransaction();
$hadTransaction = $this->pdo->inTransaction();
if (!$hadTransaction) {
$this->bd->beginTransaction();
$this->pdo->beginTransaction();
}
$result = $this->bd->exec($sql) !== false;
$result = $this->pdo->exec($sql) !== false;
if (!$hadTransaction) {
$this->bd->commit();
$this->pdo->commit();
}
return $result;
}

@ -2,32 +2,32 @@
class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
public function isCompressed() {
return false;
}
public function hasNativeHex() {
return false;
}
public function sqlHexDecode($x) {
return $x;
}
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();
if (stripos($showCreate, 'tag') === false) {
$tagDAO = FreshRSS_Factory::createTagDao();
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();
if (stripos($showCreate, 'entrytmp') === false) {
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;
}
@ -36,27 +36,27 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
DROP TABLE IF EXISTS `tmp`;
CREATE TEMP TABLE `tmp` AS
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;
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)
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
FROM `tmp`
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`;
';
$hadTransaction = $this->bd->inTransaction();
$hadTransaction = $this->pdo->inTransaction();
if (!$hadTransaction) {
$this->bd->beginTransaction();
$this->pdo->beginTransaction();
}
$result = $this->bd->exec($sql) !== false;
$result = $this->pdo->exec($sql) !== false;
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) {
$this->bd->commit();
$this->pdo->commit();
}
return $result;
}
@ -66,10 +66,10 @@ DROP TABLE IF EXISTS `tmp`;
}
protected function updateCacheUnreads($catId = false, $feedId = false) {
$sql = 'UPDATE `' . $this->prefix . 'feed` '
$sql = 'UPDATE `_feed` '
. 'SET `cache_nbUnreads`=('
. 'SELECT COUNT(*) AS nbUnreads FROM `' . $this->prefix . 'entry` e '
. 'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0)';
. 'SELECT COUNT(*) AS nbUnreads FROM `_entry` e '
. 'WHERE e.id_feed=`_feed`.id AND e.is_read=0)';
$hasWhere = false;
$values = array();
if ($feedId !== false) {
@ -84,11 +84,11 @@ DROP TABLE IF EXISTS `tmp`;
$sql .= ' category=?';
$values[] = $catId;
}
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
if ($stm && $stm->execute($values)) {
return true;
} 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]);
return false;
}
@ -118,30 +118,30 @@ DROP TABLE IF EXISTS `tmp`;
return $affected;
}
} 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);
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
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]);
$this->bd->rollBack();
$this->pdo->rollBack();
return false;
}
$affected = $stm->rowCount();
if ($affected > 0) {
$sql = 'UPDATE `' . $this->prefix . 'feed` SET `cache_nbUnreads`=`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
. 'WHERE id=(SELECT e.id_feed FROM `' . $this->prefix . 'entry` e WHERE e.id=?)';
$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);
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
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]);
$this->bd->rollBack();
$this->pdo->rollBack();
return false;
}
}
$this->bd->commit();
$this->pdo->commit();
return $affected;
}
}
@ -174,19 +174,19 @@ DROP TABLE IF EXISTS `tmp`;
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) {
$sql .= ' AND is_favorite=1';
} elseif ($priorityMin >= 0) {
$sql .= ' AND id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.priority > ' . intval($priorityMin) . ')';
$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);
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)))) {
$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]);
return false;
}
@ -215,17 +215,17 @@ DROP TABLE IF EXISTS `tmp`;
Minz_Log::debug('Calling markReadCat(0) is deprecated!');
}
$sql = 'UPDATE `' . $this->prefix . 'entry` '
$sql = 'UPDATE `_entry` '
. 'SET is_read = ? '
. '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);
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)))) {
$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]);
return false;
}
@ -249,10 +249,10 @@ DROP TABLE IF EXISTS `tmp`;
Minz_Log::debug('Calling markReadTag(0) is deprecated!');
}
$sql = 'UPDATE `' . $this->prefix . 'entry` e '
$sql = 'UPDATE `_entry` e '
. 'SET e.is_read = ? '
. '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 = ?')
. ')';
$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);
$stm = $this->bd->prepare($sql . $search);
$stm = $this->pdo->prepare($sql . $search);
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]);
return false;
}

@ -2,9 +2,19 @@
class FreshRSS_Factory {
public static function createUserDao($username = null) {
return new FreshRSS_UserDAO($username);
}
public static function createCategoryDao($username = null) {
$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) {
$conf = Minz_Configuration::get('system');

@ -7,8 +7,8 @@ class FreshRSS_Feed extends Minz_Model {
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 $url;
@ -24,9 +24,8 @@ class FreshRSS_Feed extends Minz_Model {
private $pathEntries = '';
private $httpAuth = '';
private $error = false;
private $keep_history = self::KEEP_HISTORY_DEFAULT;
private $ttl = self::TTL_DEFAULT;
private $attributes = array();
private $attributes = [];
private $mute = false;
private $hash = null;
private $lockPath = '';
@ -110,9 +109,6 @@ class FreshRSS_Feed extends Minz_Model {
public function inError() {
return $this->error;
}
public function keepHistory() {
return $this->keep_history;
}
public function ttl() {
return $this->ttl;
}
@ -153,18 +149,17 @@ class FreshRSS_Feed extends Minz_Model {
return $this->nbNotRead;
}
public function faviconPrepare() {
global $favicons_dir;
require_once(LIB_PATH . '/favicons.php');
$url = $this->website;
if ($url == '') {
$url = $this->url;
}
$txt = $favicons_dir . $this->hash() . '.txt';
$txt = FAVICONS_DIR . $this->hash() . '.txt';
if (!file_exists($txt)) {
file_put_contents($txt, $url);
}
if (FreshRSS_Context::$isCli) {
$ico = $favicons_dir . $this->hash() . '.ico';
$ico = FAVICONS_DIR . $this->hash() . '.ico';
$ico_mtime = @filemtime($ico);
$txt_mtime = @filemtime($txt);
if ($txt_mtime != false &&
@ -231,12 +226,6 @@ class FreshRSS_Feed extends Minz_Model {
public function _error($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) {
$value = intval($value);
$value = min($value, 100000000);
@ -470,6 +459,28 @@ class FreshRSS_Feed extends Minz_Model {
$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() {
return CACHE_PATH . '/' . md5($this->url) . '.spc';
}
@ -701,7 +712,7 @@ class FreshRSS_Feed extends Minz_Model {
file_put_contents($hubFilename, json_encode($hubJson));
}
$ch = curl_init();
curl_setopt_array($ch, array(
curl_setopt_array($ch, [
CURLOPT_URL => $hubJson['hub'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query(array(
@ -712,13 +723,9 @@ class FreshRSS_Feed extends Minz_Model {
)),
CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
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);
$info = curl_getinfo($ch);

@ -3,14 +3,13 @@
class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
protected function addColumn($name) {
Minz_Log::warning('FreshRSS_FeedDAO::addColumn: ' . $name);
Minz_Log::warning(__method__ . ': ' . $name);
try {
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) {
Minz_Log::error('FreshRSS_FeedDAO::addColumn error: ' . $e->getMessage());
Minz_Log::error(__method__ . ' error: ' . $e->getMessage());
}
return false;
}
@ -18,7 +17,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
protected function autoUpdateDb($errorInfo) {
if (isset($errorInfo[0])) {
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) {
return $this->addColumn($column);
}
@ -30,7 +29,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function addFeed($valuesTmp) {
$sql = '
INSERT INTO `' . $this->prefix . 'feed`
INSERT INTO `_feed`
(
url,
category,
@ -39,18 +38,24 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
description,
`lastUpdate`,
priority,
`pathEntries`,
`httpAuth`,
error,
keep_history,
ttl,
attributes
)
VALUES
(?, ?, ?, ?, ?, ?, 10, ?, 0, ?, ?, ?)';
$stm = $this->bd->prepare($sql);
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stm = $this->pdo->prepare($sql);
$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
$valuesTmp['website'] = safe_ascii($valuesTmp['website']);
if (!isset($valuesTmp['pathEntries'])) {
$valuesTmp['pathEntries'] = '';
}
if (!isset($valuesTmp['attributes'])) {
$valuesTmp['attributes'] = [];
}
$values = array(
substr($valuesTmp['url'], 0, 511),
@ -59,16 +64,18 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
substr($valuesTmp['website'], 0, 255),
mb_strcut($valuesTmp['description'], 0, 1023, 'UTF-8'),
$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']),
FreshRSS_Feed::KEEP_HISTORY_DEFAULT,
isset($valuesTmp['error']) ? intval($valuesTmp['error']) : 0,
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)) {
return $this->bd->lastInsertId('"' . $this->prefix . 'feed_id_seq"');
return $this->pdo->lastInsertId('`_feed_id_seq`');
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->addFeed($valuesTmp);
}
@ -129,13 +136,13 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($key === 'httpAuth') {
$valuesTmp[$key] = base64_encode($v);
} 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);
$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) {
$values[] = $v;
@ -145,7 +152,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
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()
$sql = 'UPDATE `' . $this->prefix . 'feed` '
$sql = 'UPDATE `_feed` '
. 'SET `lastUpdate`=?, error=? '
. 'WHERE id=?';
$values = array(
@ -174,12 +181,12 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$inError ? 1 : 0,
$id,
);
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} 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]);
return false;
}
@ -192,8 +199,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$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(
$newCat->id(),
@ -203,44 +210,54 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} 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]);
return false;
}
}
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);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} 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]);
return false;
}
}
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);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} 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]);
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) {
$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);
@ -255,8 +272,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
}
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);
@ -272,25 +289,21 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
public function listFeedsIds() {
$sql = 'SELECT id FROM `' . $this->prefix . 'feed`';
$stm = $this->bd->prepare($sql);
$stm->execute();
$sql = 'SELECT id FROM `_feed`';
$stm = $this->pdo->query($sql);
return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
}
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));
}
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);
$feedCategoryNames = array();
foreach ($res as $line) {
@ -307,17 +320,18 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
*/
public function listFeedsOrderUpdate($defaultCacheDuration = 3600, $limit = 0) {
$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
. ' 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` '
. ($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));
} else {
$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->listFeedsOrderUpdate($defaultCacheDuration);
}
@ -327,8 +341,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
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);
@ -338,8 +352,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
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);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
@ -348,8 +362,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
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);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
@ -357,62 +371,51 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
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()) {
return $stm->rowCount();
} 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;
}
}
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]);
$this->bd->rollBack();
$this->pdo->rollBack();
return false;
}
$affected = $stm->rowCount();
$sql = 'UPDATE `' . $this->prefix . 'feed` '
. 'SET `cache_nbEntries`=0, `cache_nbUnreads`=0 WHERE id=?';
$values = array($id);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
$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]);
$this->bd->rollBack();
$this->pdo->rollBack();
return false;
}
$this->bd->commit();
$this->pdo->commit();
return $affected;
}
@ -446,7 +449,6 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$myFeed->_pathEntries(isset($dao['pathEntries']) ? $dao['pathEntries'] : '');
$myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode($dao['httpAuth']) : '');
$myFeed->_error(isset($dao['error']) ? $dao['error'] : 0);
$myFeed->_keepHistory(isset($dao['keep_history']) ? $dao['keep_history'] : FreshRSS_Feed::KEEP_HISTORY_DEFAULT);
$myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : FreshRSS_Feed::TTL_DEFAULT);
$myFeed->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : '');
$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() {
$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)))) {
$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);
$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);
}
} else {

@ -3,9 +3,9 @@
class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
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);
foreach (array('attributes') as $column) {
foreach (['attributes'] as $column) {
if (!in_array($column, $columns)) {
return $this->addColumn($column);
}

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

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

@ -15,14 +15,13 @@ class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
$sql = <<<SQL
SELECT strftime('{$period}', e.date, 'unixepoch') AS period
, COUNT(1) AS count
FROM `{$this->prefix}entry` AS e
FROM `_entry` AS e
{$restrict}
GROUP BY period
ORDER BY period ASC
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$stm = $this->pdo->query($sql);
$res = $stm->fetchAll(PDO::FETCH_NAMED);
$repartition = array();

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

@ -8,37 +8,24 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function createTagTable() {
$ok = false;
$hadTransaction = $this->bd->inTransaction();
$hadTransaction = $this->pdo->inTransaction();
if ($hadTransaction) {
$this->bd->commit();
$this->pdo->commit();
}
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...');
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
$databaseDAO->ensureCaseInsensitiveGuids();
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) {
Minz_Log::error('FreshRSS_EntryDAO::createTagTable error: ' . $e->getMessage());
}
if ($hadTransaction) {
$this->bd->beginTransaction();
$this->pdo->beginTransaction();
}
return $ok;
}
@ -55,22 +42,25 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
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');
if (!isset($valuesTmp['attributes'])) {
$valuesTmp['attributes'] = [];
}
$values = array(
$valuesTmp['name'],
isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
$valuesTmp['name'],
);
if ($stm && $stm->execute($values)) {
return $this->bd->lastInsertId('"' . $this->prefix . 'tag_id_seq"');
return $this->pdo->lastInsertId('`_tag_id_seq`');
} 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]);
return false;
}
@ -89,14 +79,17 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
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');
if (!isset($valuesTmp['attributes'])) {
$valuesTmp['attributes'] = [];
}
$values = array(
$valuesTmp['name'],
isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
$id,
$valuesTmp['name'],
);
@ -104,7 +97,7 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} 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]);
return false;
}
@ -125,23 +118,39 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($id <= 0) {
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);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} 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]);
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) {
$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);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
@ -150,8 +159,8 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
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);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
@ -162,20 +171,20 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function listTags($precounts = false) {
if ($precounts) {
$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 '
. 'ORDER BY t.name';
} 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));
} else {
$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->listTags($precounts);
}
@ -185,13 +194,13 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
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);
return $res[0]['count'];
} else {
$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->count();
}
@ -201,8 +210,8 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
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);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
@ -210,10 +219,10 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
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';
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
$values = array($id);
$stm->execute($values);
$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) {
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 {
$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);
if ($stm && $stm->execute($values)) {
return true;
} 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]);
return false;
}
@ -240,11 +249,11 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function getTagsForEntry($id_entry) {
$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';
$stm = $this->bd->prepare($sql);
$stm = $this->pdo->prepare($sql);
$values = array($id_entry);
if ($stm && $stm->execute($values)) {
@ -255,7 +264,7 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
return $lines;
} else {
$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->getTagsForEntry($id_entry);
}
@ -266,8 +275,8 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function getTagsForEntries($entries) {
$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();
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)) {
return $stm->fetchAll(PDO::FETCH_ASSOC);
} else {
$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->getTagsForEntries($entries);
}

@ -7,7 +7,7 @@ class FreshRSS_TagDAOSQLite extends FreshRSS_TagDAO {
}
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();
if (stripos($showCreate, 'tag') === false) {
return $this->createTagTable(); //v1.12.0

@ -1,85 +1,54 @@
<?php
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 {
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 (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) {
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) {
return true;
} 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;
}
}
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");
}
if ($db['type'] === 'sqlite') {
return unlink(USERS_PATH . '/' . $username . '/db.sqlite');
} else {
$userPDO = new Minz_ModelPdo($username);
require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
$ok = $this->pdo->exec($SQL_DROP_TABLES) !== false;
$sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_');
$stm = $userPDO->bd->prepare($sql);
if ($stm && $stm->execute()) {
if ($ok) {
return true;
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error : ' . $info[2]);
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
Minz_Log::error(__METHOD__ . ' error: ' . $info[2]);
return false;
}
}
}
public static function exists($username) {
return is_dir(USERS_PATH . '/' . $username);

@ -1,20 +1,23 @@
<?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
`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`),
UNIQUE KEY (`name`) -- v0.7
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `%1$sfeed` (
CREATE TABLE IF NOT EXISTS `_feed` (
`id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7
`url` VARCHAR(511) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`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,
`description` TEXT,
`lastUpdate` INT(11) DEFAULT 0, -- Until year 2038
@ -22,26 +25,24 @@ CREATE TABLE IF NOT EXISTS `%1$sfeed` (
`pathEntries` VARCHAR(511) DEFAULT NULL,
`httpAuth` VARCHAR(511) DEFAULT NULL,
`error` BOOLEAN DEFAULT 0,
`keep_history` MEDIUMINT NOT NULL DEFAULT -2, -- v0.7
`ttl` INT NOT NULL DEFAULT 0, -- v0.7.3
`attributes` TEXT, -- v1.11.0
`cache_nbEntries` INT DEFAULT 0, -- v0.7
`cache_nbUnreads` INT DEFAULT 0, -- v0.7
PRIMARY KEY (`id`),
FOREIGN KEY (`category`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
FOREIGN KEY (`category`) REFERENCES `_category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
UNIQUE KEY (`url`), -- v0.7
INDEX (`name`), -- v0.7
INDEX (`priority`), -- v0.7
INDEX (`keep_history`) -- v0.7
INDEX (`priority`) -- v0.7
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `%1$sentry` (
CREATE TABLE IF NOT EXISTS `_entry` (
`id` BIGINT NOT NULL, -- v0.7
`guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, -- Maximum for UNIQUE is 767B
`title` VARCHAR(255) NOT NULL,
`author` VARCHAR(255),
`content_bin` BLOB, -- v0.7
`content_bin` MEDIUMBLOB, -- v0.7
`link` VARCHAR(1023) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`date` INT(11), -- 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
`tags` VARCHAR(1023),
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
INDEX (`is_favorite`), -- 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
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,
`guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`title` VARCHAR(255) NOT NULL,
`author` VARCHAR(255),
`content_bin` BLOB,
`content_bin` MEDIUMBLOB,
`link` VARCHAR(1023) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`date` INT(11),
`lastSeen` INT(11) DEFAULT 0,
@ -79,17 +84,15 @@ CREATE TABLE IF NOT EXISTS `%1$sentrytmp` ( -- v1.7
`id_feed` SMALLINT,
`tags` VARCHAR(1023),
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`),
INDEX (`date`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
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,
`name` VARCHAR(63) NOT NULL,
`attributes` TEXT,
@ -98,46 +101,27 @@ CREATE TABLE IF NOT EXISTS `%1$stag` ( -- v1.12
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `%1$sentrytag` ( -- v1.12
CREATE TABLE IF NOT EXISTS `_entrytag` ( -- v1.12
`id_tag` SMALLINT,
`id_entry` BIGINT,
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`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
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;

@ -1,14 +1,16 @@
<?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,
"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,
"url" VARCHAR(511) UNIQUE NOT NULL,
"category" SMALLINT DEFAULT 0,
@ -20,18 +22,16 @@ $SQL_CREATE_TABLES = array(
"pathEntries" VARCHAR(511) DEFAULT NULL,
"httpAuth" VARCHAR(511) DEFAULT NULL,
"error" SMALLINT DEFAULT 0,
"keep_history" INT NOT NULL DEFAULT -2,
"ttl" INT NOT NULL DEFAULT 0,
"attributes" TEXT, -- v1.11.0
"cache_nbEntries" INT DEFAULT 0,
"cache_nbUnreads" INT DEFAULT 0,
FOREIGN KEY ("category") REFERENCES "%1$scategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);',
'CREATE INDEX "%1$sname_index" ON "%1$sfeed" ("name");',
'CREATE INDEX "%1$spriority_index" ON "%1$sfeed" ("priority");',
'CREATE INDEX "%1$skeep_history_index" ON "%1$sfeed" ("keep_history");',
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,
"guid" VARCHAR(760) NOT NULL,
"title" VARCHAR(255) NOT NULL,
@ -45,22 +45,26 @@ $SQL_CREATE_TABLES = array(
"is_favorite" SMALLINT NOT NULL DEFAULT 0,
"id_feed" SMALLINT,
"tags" VARCHAR(1023),
FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("id_feed") REFERENCES `_feed` ("id") ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE ("id_feed","guid")
);',
'CREATE INDEX "%1$sis_favorite_index" ON "%1$sentry" ("is_favorite");',
'CREATE INDEX "%1$sis_read_index" ON "%1$sentry" ("is_read");',
'CREATE INDEX "%1$sentry_lastSeen_index" ON "%1$sentry" ("lastSeen");',
'INSERT INTO "%1$scategory" (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,
"guid" VARCHAR(760) NOT NULL,
"title" VARCHAR(255) NOT NULL,
@ -74,39 +78,34 @@ $SQL_CREATE_TABLE_ENTRYTMP = array(
"is_favorite" SMALLINT NOT NULL DEFAULT 0,
"id_feed" SMALLINT,
"tags" VARCHAR(1023),
FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("id_feed") REFERENCES `_feed` ("id") ON DELETE CASCADE ON UPDATE CASCADE,
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,
"name" VARCHAR(63) UNIQUE NOT NULL,
"attributes" TEXT
);',
'CREATE TABLE IF NOT EXISTS "%1$sentrytag" (
);
CREATE TABLE IF NOT EXISTS `_entrytag` (
"id_tag" SMALLINT,
"id_entry" BIGINT,
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;

@ -1,13 +1,17 @@
<?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,
`name` VARCHAR(255) NOT NULL,
`attributes` TEXT, -- v1.15.0
UNIQUE (`name`)
);',
);
'CREATE TABLE IF NOT EXISTS `feed` (
CREATE TABLE IF NOT EXISTS `feed` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`url` VARCHAR(511) NOT NULL,
`category` SMALLINT DEFAULT 0,
@ -19,19 +23,17 @@ $SQL_CREATE_TABLES = array(
`pathEntries` VARCHAR(511) DEFAULT NULL,
`httpAuth` VARCHAR(511) DEFAULT NULL,
`error` BOOLEAN DEFAULT 0,
`keep_history` MEDIUMINT NOT NULL DEFAULT -2,
`ttl` INT NOT NULL DEFAULT 0,
`attributes` TEXT, -- v1.11.0
`cache_nbEntries` INT DEFAULT 0,
`cache_nbUnreads` INT DEFAULT 0,
FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
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,
`guid` VARCHAR(760) NOT NULL,
`title` VARCHAR(255) NOT NULL,
@ -48,17 +50,21 @@ $SQL_CREATE_TABLES = array(
PRIMARY KEY (`id`),
FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
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,
`guid` VARCHAR(760) NOT NULL,
`title` VARCHAR(255) NOT NULL,
@ -75,36 +81,37 @@ $SQL_CREATE_TABLE_ENTRYTMP = array(
PRIMARY KEY (`id`),
FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
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,
`name` VARCHAR(63) NOT NULL,
`attributes` TEXT,
UNIQUE (`name`)
);',
'CREATE TABLE IF NOT EXISTS `entrytag` (
);
CREATE TABLE IF NOT EXISTS `entrytag` (
`id_tag` SMALLINT,
`id_entry` SMALLINT,
`id_entry` BIGINT,
PRIMARY KEY (`id_tag`,`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 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;

@ -163,6 +163,7 @@ return array(
'help' => 'in seconds', //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
'max-categories' => 'Categories per user limit', //TODO - Translation
'max-feeds' => 'Feeds per user limit', //TODO - Translation

@ -3,13 +3,21 @@
return array(
'archiving' => array(
'_' => 'Archivace',
'advanced' => 'Pokročilé',
'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ů',
'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_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í',
'keep_max' => 'Maximum number of articles to keep', //TODO - Translation
'keep_period' => 'Maximum age of articles to keep', //TODO - Translation
'title' => 'Archivace',
'ttl' => 'Neaktualizovat častěji než',
),
@ -21,6 +29,7 @@ return array(
'publication_date' => 'Datum vydání',
'related_tags' => 'Související tagy', //TODO - Translation
'sharing' => 'Sdílení',
'display_authors' => 'Authors', //TODO - Translation
'top_line' => 'Horní řádek',
),
'language' => 'Jazyk',
@ -45,6 +54,7 @@ return array(
'_' => 'Smazání účtu',
'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_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
'password_format' => 'Alespoň 7 znaků',
@ -133,7 +143,6 @@ return array(
'diaspora' => 'Diaspora*',
'email' => 'Email',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'Více informací',
'print' => 'Tisk',
'remove' => 'Remove sharing method', //TODO - Translation

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

@ -7,7 +7,7 @@ return array(
'bugs_reports' => 'Hlášení chyb',
'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>.',
'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>',
'license' => 'Licence',
'project_website' => 'Stránka projektu',
@ -15,6 +15,9 @@ return array(
'version' => 'Verze',
'website' => 'Webové stránka',
),
'tos' => array(
'title' => 'Terms of Service', // TODO - Translation
),
'feed' => array(
'add' => 'Můžete přidat kanály.',
'empty' => 'Žádné články k zobrazení.',

@ -13,9 +13,12 @@ return array(
'category' => array(
'_' => 'Kategorie',
'add' => 'Přidat kategorii',
'archiving' => 'Archivace',
'empty' => 'Vyprázdit kategorii',
'information' => 'Informace',
'new' => 'Nová kategorie',
'position' => 'Display position', //TODO - Translation
'position_help' => 'To control category sort order', //TODO - Translation
'title' => 'Název',
),
'feed' => array(
@ -40,7 +43,7 @@ return array(
'help' => 'Write one search filter per line.', //TODO - Translation
),
'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>.',
'mute' => 'mute', //TODO - Translation
'no_selected' => 'Nejsou označeny žádné kanály.',
@ -72,6 +75,7 @@ return 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
'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
),
'import_export' => array(

@ -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
),
),
);

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

@ -3,13 +3,21 @@
return array(
'archiving' => array(
'_' => 'Archivierung',
'advanced' => 'Erweitert',
'delete_after' => 'Entferne Artikel nach',
'exception' => 'Purge exception', //TODO - Translation
'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_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',
'keep_max' => 'Maximum number of articles to keep', //TODO - Translation
'keep_period' => 'Maximum age of articles to keep', //TODO - Translation
'title' => 'Archivierung',
'ttl' => 'Aktualisiere automatisch nicht öfter als',
),
@ -21,6 +29,7 @@ return array(
'publication_date' => 'Datum der Veröffentlichung',
'related_tags' => 'Verwandte Tags',
'sharing' => 'Teilen',
'display_authors' => 'Authors', //TODO - Translation
'top_line' => 'Kopfzeile',
),
'language' => 'Sprache',
@ -45,6 +54,7 @@ return array(
'_' => 'Accountlöschung',
'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_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',
'password_format' => 'mindestens 7 Zeichen',
@ -133,7 +143,6 @@ return array(
'diaspora' => 'Diaspora*',
'email' => 'E-Mail',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'Weitere Informationen',
'print' => 'Drucken',
'remove' => 'Entferne Teilen-Dienst',

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

@ -7,7 +7,7 @@ return array(
'bugs_reports' => 'Fehlerberichte',
'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.',
'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>',
'license' => 'Lizenz',
'project_website' => 'Projekt-Webseite',
@ -15,6 +15,9 @@ return array(
'version' => 'Version',
'website' => 'Webseite',
),
'tos' => array(
'title' => 'Terms of Service', // TODO - Translation
),
'feed' => array(
'add' => 'Sie können Feeds hinzufügen.',
'empty' => 'Es gibt keinen Artikel zum Anzeigen.',

@ -13,9 +13,12 @@ return array(
'category' => array(
'_' => 'Kategorie',
'add' => 'Eine Kategorie hinzufügen',
'archiving' => 'Archivierung',
'empty' => 'Leere Kategorie',
'information' => 'Information',
'new' => 'Neue Kategorie',
'position' => 'Display position', //TODO - Translation
'position_help' => 'To control category sort order', //TODO - Translation
'title' => 'Titel',
),
'feed' => array(
@ -40,7 +43,7 @@ return array(
'help' => 'Write one search filter per line.', //TODO - Translation
),
'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.',
'mute' => 'Stumm schalten',
'no_selected' => 'Kein Feed ausgewählt.',
@ -72,6 +75,7 @@ return 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.',
'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',
),
'import_export' => array(

@ -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
),
),
);

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

@ -3,13 +3,21 @@
return array(
'archiving' => array(
'_' => 'Archiving',
'advanced' => 'Advanced',
'delete_after' => 'Remove articles after',
'exception' => 'Purge exception',
'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_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',
'keep_max' => 'Maximum number of articles to keep',
'keep_period' => 'Maximum age of articles to keep',
'title' => 'Archiving',
'ttl' => 'Do not automatically refresh more often than',
),
@ -21,6 +29,7 @@ return array(
'publication_date' => 'Date of publication',
'related_tags' => 'Article tags',
'sharing' => 'Sharing',
'display_authors' => 'Authors',
'top_line' => 'Top line',
),
'language' => 'Language',
@ -45,6 +54,7 @@ return array(
'_' => 'Account deletion',
'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_form' => 'Password<br /><small>(for the Web-form login method)</small>',
'password_format' => 'At least 7 characters',
@ -133,7 +143,6 @@ return array(
'diaspora' => 'Diaspora*',
'email' => 'Email',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'More information',
'print' => 'Print',
'remove' => 'Remove sharing method',

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

@ -7,7 +7,7 @@ return array(
'bugs_reports' => 'Bugs reports',
'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.',
'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>',
'license' => 'License',
'project_website' => 'Project website',
@ -15,6 +15,9 @@ return array(
'version' => 'Version',
'website' => 'Website',
),
'tos' => array(
'title' => 'Terms of Service',
),
'feed' => array(
'add' => 'You may add some feeds.',
'empty' => 'There is no article to show.',

@ -13,9 +13,12 @@ return array(
'category' => array(
'_' => 'Category',
'add' => 'Add a category',
'archiving' => 'Archiving',
'empty' => 'Empty category',
'information' => 'Information',
'new' => 'New category',
'position' => 'Display position',
'position_help' => 'To control category sort order',
'title' => 'Title',
),
'feed' => array(
@ -40,7 +43,7 @@ return array(
'help' => 'Write one search filter per line.',
),
'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>.',
'mute' => 'mute',
'no_selected' => 'No feed selected.',
@ -72,6 +75,7 @@ return 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.',
'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',
),
'import_export' => array(

@ -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:',
),
),
);

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

@ -3,13 +3,21 @@
return array(
'archiving' => array(
'_' => 'Archivo',
'advanced' => 'Avanzado',
'delete_after' => 'Eliminar artículos tras',
'exception' => 'Purge exception', //TODO - Translation
'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_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',
'keep_max' => 'Maximum number of articles to keep', //TODO - Translation
'keep_period' => 'Maximum age of articles to keep', //TODO - Translation
'title' => 'Archivo',
'ttl' => 'No actualizar automáticamente más de',
),
@ -21,6 +29,7 @@ return array(
'publication_date' => 'Fecha de publicación',
'related_tags' => 'Etiquetas relacionadas',
'sharing' => 'Compartir',
'display_authors' => 'Authors', //TODO - Translation
'top_line' => 'Línea superior',
),
'language' => 'Idioma',
@ -45,6 +54,7 @@ return array(
'_' => 'Borrar cuenta',
'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_form' => 'Contraseña<br /><small>(para el método de identificación por formulario web)</small>',
'password_format' => 'Mínimo de 7 caracteres',
@ -133,7 +143,6 @@ return array(
'diaspora' => 'Diaspora*',
'email' => 'Email',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'Más información',
'print' => 'Print',
'remove' => 'Remove sharing method', //TODO - Translation

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

@ -7,7 +7,7 @@ return array(
'bugs_reports' => 'Informe de fallos',
'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>.',
'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>',
'license' => 'Licencia',
'project_website' => 'Web del proyecto',
@ -15,6 +15,9 @@ return array(
'version' => 'Versión',
'website' => 'Web',
),
'tos' => array(
'title' => 'Terms of Service', // TODO - Translation
),
'feed' => array(
'add' => 'Puedes añadir fuentes.',
'empty' => 'No hay artículos a mostrar.',

@ -13,9 +13,12 @@ return array(
'category' => array(
'_' => 'Categoría',
'add' => 'Añadir a la categoría',
'archiving' => 'Archivo',
'empty' => 'Vaciar categoría',
'information' => 'Información',
'new' => 'Nueva categoría',
'position' => 'Display position', //TODO - Translation
'position_help' => 'To control category sort order', //TODO - Translation
'title' => 'Título',
),
'feed' => array(
@ -40,7 +43,7 @@ return array(
'help' => 'Write one search filter per line.', //TODO - Translation
),
'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>.',
'mute' => 'mute', //TODO - Translation
'no_selected' => 'No hay funentes seleccionadas.',
@ -72,6 +75,7 @@ return 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
'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
),
'import_export' => array(

@ -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
),
),
);

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

@ -3,13 +3,21 @@
return array(
'archiving' => array(
'_' => 'Archivage',
'advanced' => 'Avancé',
'delete_after' => 'Supprimer les articles après',
'exception' => 'Exception de nettoyage',
'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_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',
'keep_max' => 'Nombre maximum d’articles à conserver',
'keep_period' => 'Âge maximum des articles à conserver',
'title' => 'Archivage',
'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',
),
@ -21,6 +29,7 @@ return array(
'publication_date' => 'Date de publication',
'related_tags' => 'Tags de l’article',
'sharing' => 'Partage',
'display_authors' => 'Authors', //TODO - Translation
'top_line' => 'Ligne du haut',
),
'language' => 'Langue',
@ -45,6 +54,7 @@ return array(
'_' => 'Suppression du compte',
'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_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
'password_format' => '7 caractères minimum',
@ -133,7 +143,6 @@ return array(
'diaspora' => 'Diaspora*',
'email' => 'Courriel',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'Plus d’informations',
'print' => 'Print',
'remove' => 'Supprimer la méthode de partage',

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

@ -7,7 +7,7 @@ return array(
'bugs_reports' => 'Rapports de bugs',
'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.',
'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>',
'license' => 'Licence',
'project_website' => 'Site du projet',
@ -15,6 +15,9 @@ return array(
'version' => 'Version',
'website' => 'Site Internet',
),
'tos' => array(
'title' => 'Conditions Générales d’Utilisation',
),
'feed' => array(
'add' => 'Vous pouvez ajouter des flux.',
'empty' => 'Il n’y a aucun article à afficher.',

@ -13,9 +13,12 @@ return array(
'category' => array(
'_' => 'Catégorie',
'add' => 'Ajouter une catégorie',
'archiving' => 'Archivage',
'empty' => 'Catégorie vide',
'information' => 'Informations',
'new' => 'Nouvelle catégorie',
'position' => 'Position d’affichage',
'position_help' => 'Pour contrôler l’ordre de tri des catégories',
'title' => 'Titre',
),
'feed' => array(
@ -40,7 +43,7 @@ return array(
'help' => 'Écrivez une recherche par ligne.',
),
'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>.',
'mute' => 'muet',
'no_selected' => 'Aucun flux sélectionné.',
@ -72,6 +75,7 @@ return 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.',
'obsolete_63' => 'À partir de la version 63, Firefox ne supporte plus l’ajout de services d’abonnements.',
'title' => 'Lecteur de flux dans Firefox',
),
'import_export' => array(

@ -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 :',
),
),
);

@ -163,6 +163,7 @@ return array(
'help' => 'in seconds', //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
'max-categories' => 'Categories per user limit', //TODO - Translation
'max-feeds' => 'Feeds per user limit', //TODO - Translation

@ -3,13 +3,21 @@
return array(
'archiving' => array(
'_' => 'ארכוב',
'advanced' => 'מתקדם',
'delete_after' => 'מחיקת מאמרים לאחר',
'exception' => 'Purge exception', //TODO - Translation
'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_help' => 'ביצוע לעיתים קרובות על מנת למטב את בסיס הנתונים',
'policy' => 'Purge policy', //TODO - Translation
'policy_warning' => 'If no purge policy is selected, every article will be kept.', //TODO - Translation
'purge_now' => 'ניקוי עכשיו',
'keep_max' => 'Maximum number of articles to keep', //TODO - Translation
'keep_period' => 'Maximum age of articles to keep', //TODO - Translation
'title' => 'ארכוב',
'ttl' => 'אין לרענן אוטומטית יותר מ',
),
@ -21,6 +29,7 @@ return array(
'publication_date' => 'תאריך הפרסום',
'related_tags' => 'תגיות קשורות', //TODO - Translation
'sharing' => 'שיתוף',
'display_authors' => 'Authors', //TODO - Translation
'top_line' => 'שורה עליונה',
),
'language' => 'שפה',
@ -45,6 +54,7 @@ return array(
'_' => 'Account deletion', //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_form' => 'סיסמה<br /><small>(לשימוש בטפוס ההרשמה)</small>',
'password_format' => 'At least 7 characters', //TODO - Translation
@ -133,7 +143,6 @@ return array(
'diaspora' => 'Diaspora*',
'email' => 'דואר אלקטרוני',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'מידע נוסף',
'print' => 'הדפסה',
'remove' => 'Remove sharing method', //TODO - Translation

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

@ -7,7 +7,7 @@ return array(
'bugs_reports' => 'דיווח באגים',
'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.',
'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>',
'license' => 'רישיון',
'project_website' => 'אתר',
@ -15,6 +15,9 @@ return array(
'version' => 'גרסה',
'website' => 'אתר',
),
'tos' => array(
'title' => 'Terms of Service', // TODO - Translation
),
'feed' => array(
'add' => 'ניתן להוסיף הזנות חדשות.',
'empty' => 'אין מאמר להצגה.',

@ -13,9 +13,12 @@ return array(
'category' => array(
'_' => 'קטגוריה',
'add' => 'הוספת קטגוריה',
'archiving' => 'ארכוב',
'empty' => 'Empty category', //TODO - Translation
'information' => 'מידע',
'new' => 'קטגוריה חדשה',
'position' => 'Display position', //TODO - Translation
'position_help' => 'To control category sort order', //TODO - Translation
'title' => 'כותרת',
),
'feed' => array(
@ -40,7 +43,7 @@ return array(
'help' => 'Write one search filter per line.', //TODO - Translation
),
'information' => 'מידע',
'keep_history' => 'מסםר מינימלי של מאמרים לשמור',
'keep_min' => 'מסםר מינימלי של מאמרים לשמור',
'moved_category_deleted' => 'כאשר הקטגוריה נמחקת ההזנות שבתוכה אוטומטית מקוטלגות תחת <em>%s</em>.',
'mute' => 'mute', //TODO - Translation
'no_selected' => 'אף הזנה לא נבחרה.',
@ -72,6 +75,7 @@ return 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
'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
),
'import_export' => array(

@ -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
),
),
);

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

@ -3,13 +3,21 @@
return array(
'archiving' => array(
'_' => 'Archiviazione',
'advanced' => 'Avanzate',
'delete_after' => 'Rimuovi articoli dopo',
'exception' => 'Purge exception', //TODO - Translation
'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_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',
'keep_max' => 'Maximum number of articles to keep', //TODO - Translation
'keep_period' => 'Maximum age of articles to keep', //TODO - Translation
'title' => 'Archiviazione',
'ttl' => 'Non effettuare aggiornamenti per più di',
),
@ -21,6 +29,7 @@ return array(
'publication_date' => 'Data di pubblicazione',
'related_tags' => 'Tags correlati', //TODO - Translation
'sharing' => 'Condivisione',
'display_authors' => 'Authors', //TODO - Translation
'top_line' => 'Barra in alto',
),
'language' => 'Lingua',
@ -45,6 +54,7 @@ return array(
'_' => 'Cancellazione account',
'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_form' => 'Password<br /><small>(per il login classico)</small>',
'password_format' => 'Almeno 7 caratteri',
@ -133,7 +143,6 @@ return array(
'diaspora' => 'Diaspora*',
'email' => 'Email',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'Ulteriori informazioni',
'print' => 'Stampa',
'remove' => 'Remove sharing method', //TODO - Translation

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

@ -7,7 +7,7 @@ return array(
'bugs_reports' => 'Bugs',
'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.',
'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>',
'license' => 'Licenza',
'project_website' => 'Sito del progetto',
@ -15,6 +15,9 @@ return array(
'version' => 'Versione',
'website' => 'Sito',
),
'tos' => array(
'title' => 'Terms of Service', // TODO - Translation
),
'feed' => array(
'add' => 'Aggiungi un Feed RSS',
'empty' => 'Non ci sono articoli da mostrare.',

@ -13,9 +13,12 @@ return array(
'category' => array(
'_' => 'Categoria',
'add' => 'Aggiungi una categoria',
'archiving' => 'Archiviazione',
'empty' => 'Categoria vuota',
'information' => 'Informazioni',
'new' => 'Nuova categoria',
'position' => 'Display position', //TODO - Translation
'position_help' => 'To control category sort order', //TODO - Translation
'title' => 'Titolo',
),
'feed' => array(
@ -40,7 +43,7 @@ return array(
'help' => 'Write one search filter per line.', //TODO - Translation
),
'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>.',
'mute' => 'mute', //TODO - Translation
'no_selected' => 'Nessun feed selezionato.',
@ -72,6 +75,7 @@ return 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
'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
),
'import_export' => array(

@ -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
),
),
);

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save