Ver Fonte

Merge pull request #2049 from FreshRSS/dev

FreshRSS 1.12.0
Alexandre Alapetite há 7 anos atrás
pai
commit
e04804d0f6
100 ficheiros alterados com 1383 adições e 395 exclusões
  1. 41 0
      CHANGELOG.md
  2. 5 0
      CREDITS.md
  3. 1 1
      Docker/entrypoint.sh
  4. 4 9
      README.fr.md
  5. 4 9
      README.md
  6. 4 4
      app/Controllers/categoryController.php
  7. 6 4
      app/Controllers/configureController.php
  8. 17 0
      app/Controllers/entryController.php
  9. 9 3
      app/Controllers/feedController.php
  10. 24 11
      app/Controllers/indexController.php
  11. 4 1
      app/Controllers/javascriptController.php
  12. 1 1
      app/Controllers/statsController.php
  13. 2 1
      app/Controllers/subscriptionController.php
  14. 80 0
      app/Controllers/tagController.php
  15. 7 1
      app/Controllers/updateController.php
  16. 17 6
      app/Controllers/userController.php
  17. 0 1
      app/FreshRSS.php
  18. 3 3
      app/Models/Category.php
  19. 12 5
      app/Models/CategoryDAO.php
  20. 39 7
      app/Models/Context.php
  21. 66 23
      app/Models/DatabaseDAO.php
  22. 17 21
      app/Models/DatabaseDAOPGSQL.php
  23. 12 2
      app/Models/DatabaseDAOSQLite.php
  24. 32 21
      app/Models/Entry.php
  25. 86 17
      app/Models/EntryDAO.php
  26. 7 2
      app/Models/EntryDAOPGSQL.php
  27. 47 1
      app/Models/EntryDAOSQLite.php
  28. 16 0
      app/Models/Factory.php
  29. 27 7
      app/Models/Feed.php
  30. 6 3
      app/Models/FeedDAO.php
  31. 29 8
      app/Models/Search.php
  32. 76 0
      app/Models/Tag.php
  33. 315 0
      app/Models/TagDAO.php
  34. 9 0
      app/Models/TagDAOPGSQL.php
  35. 19 0
      app/Models/TagDAOSQLite.php
  36. 9 3
      app/Models/Themes.php
  37. 3 4
      app/Models/UserDAO.php
  38. 25 1
      app/Models/UserQuery.php
  39. 67 41
      app/SQL/install.sql.mysql.php
  40. 40 16
      app/SQL/install.sql.pgsql.php
  41. 55 69
      app/SQL/install.sql.sqlite.php
  42. 1 1
      app/i18n/cz/conf.php
  43. 1 1
      app/i18n/cz/gen.php
  44. 3 2
      app/i18n/cz/index.php
  45. 1 0
      app/i18n/cz/sub.php
  46. 2 2
      app/i18n/de/admin.php
  47. 2 2
      app/i18n/de/conf.php
  48. 2 2
      app/i18n/de/feedback.php
  49. 2 1
      app/i18n/de/gen.php
  50. 4 3
      app/i18n/de/index.php
  51. 2 2
      app/i18n/de/install.php
  52. 3 2
      app/i18n/de/sub.php
  53. 1 1
      app/i18n/en/conf.php
  54. 1 1
      app/i18n/en/gen.php
  55. 3 2
      app/i18n/en/index.php
  56. 1 0
      app/i18n/en/sub.php
  57. 1 1
      app/i18n/es/conf.php
  58. 1 1
      app/i18n/es/gen.php
  59. 3 2
      app/i18n/es/index.php
  60. 1 0
      app/i18n/es/sub.php
  61. 1 1
      app/i18n/fr/conf.php
  62. 1 1
      app/i18n/fr/gen.php
  63. 3 2
      app/i18n/fr/index.php
  64. 1 0
      app/i18n/fr/sub.php
  65. 1 1
      app/i18n/he/conf.php
  66. 1 1
      app/i18n/he/gen.php
  67. 3 2
      app/i18n/he/index.php
  68. 1 0
      app/i18n/he/sub.php
  69. 1 1
      app/i18n/it/conf.php
  70. 1 1
      app/i18n/it/gen.php
  71. 3 2
      app/i18n/it/index.php
  72. 1 0
      app/i18n/it/sub.php
  73. 1 1
      app/i18n/kr/conf.php
  74. 1 1
      app/i18n/kr/gen.php
  75. 3 2
      app/i18n/kr/index.php
  76. 1 0
      app/i18n/kr/sub.php
  77. 1 1
      app/i18n/nl/conf.php
  78. 1 1
      app/i18n/nl/gen.php
  79. 2 2
      app/i18n/nl/index.php
  80. 1 0
      app/i18n/nl/sub.php
  81. 1 1
      app/i18n/pt-br/conf.php
  82. 1 1
      app/i18n/pt-br/gen.php
  83. 2 2
      app/i18n/pt-br/index.php
  84. 1 0
      app/i18n/pt-br/sub.php
  85. 1 1
      app/i18n/ru/conf.php
  86. 1 1
      app/i18n/ru/gen.php
  87. 2 2
      app/i18n/ru/index.php
  88. 1 0
      app/i18n/ru/sub.php
  89. 1 1
      app/i18n/tr/conf.php
  90. 1 1
      app/i18n/tr/gen.php
  91. 3 2
      app/i18n/tr/index.php
  92. 1 0
      app/i18n/tr/sub.php
  93. 1 1
      app/i18n/zh-cn/conf.php
  94. 1 1
      app/i18n/zh-cn/gen.php
  95. 3 2
      app/i18n/zh-cn/index.php
  96. 1 0
      app/i18n/zh-cn/sub.php
  97. 3 3
      app/install.php
  98. 35 0
      app/layout/aside_feed.phtml
  99. 13 21
      app/layout/layout.phtml
  100. 3 3
      app/views/configure/display.phtml

+ 41 - 0
CHANGELOG.md

@@ -1,5 +1,46 @@
 # FreshRSS changelog
 # FreshRSS changelog
 
 
+## 2018-10-28 FreshRSS 1.12.0
+
+* Features
+	* Ability to add *labels* (custom tags) to articles [#928](https://github.com/FreshRSS/FreshRSS/issues/928)
+		* Also available through Google Reader API (full support in News+, partial in FeedMe, EasyRSS). No support in Fever API.
+	* Handle article tags containing spaces, as well as comma-separated tags [#2023](https://github.com/FreshRSS/FreshRSS/pull/2023)
+	* Handle authors containing spaces, as well as comma or semi-colon separated authors [#2025](https://github.com/FreshRSS/FreshRSS/pull/2025)
+	* Searches by tag, author, etc. accept Unicode characters [#2025](https://github.com/FreshRSS/FreshRSS/pull/2025)
+	* New option to disable cache for feeds with invalid HTTP caching [#2052](https://github.com/FreshRSS/FreshRSS/pull/2052)
+* UI
+	* New theme *Swage* [#2069](https://github.com/FreshRSS/FreshRSS/pull/2069)
+	* Click on authors to initiate a search by author [#2025](https://github.com/FreshRSS/FreshRSS/pull/2025)
+	* Fix CSS for button alignments in older Chrome versions [#2020](https://github.com/FreshRSS/FreshRSS/pull/2020)
+	* Updated to jQuery 3.3.1 [#2021](https://github.com/FreshRSS/FreshRSS/pull/2021)
+	* Updated to bcrypt.js 2.4.4 [#2022](https://github.com/FreshRSS/FreshRSS/pull/2022)
+* Security
+	* Improved flow for password change (avoid error 403) [#2056](https://github.com/FreshRSS/FreshRSS/issues/2056)
+	* Allow dot `.` in username (best to avoid, though) [#2061](https://github.com/FreshRSS/FreshRSS/issues/2061)
+* Performance
+	* Remove some counterproductive preload / prefetch rules [#2040](https://github.com/FreshRSS/FreshRSS/pull/2040)
+	* Improved fast flush (earlier transfer, fetching of resources, and rendering) [#2045](https://github.com/FreshRSS/FreshRSS/pull/2045)
+		* Only available for Apache running PHP as module (not for NGINX, or PHP as CGI / FPM) because we want to keep compression
+* Deployment
+	* Fix Docker bug with some cron values [#2032](https://github.com/FreshRSS/FreshRSS/pull/2032)
+	* Perform `git clean -f -d -f` (removes unknown files and folders) before git auto-update method [#2036](https://github.com/FreshRSS/FreshRSS/pull/2036)
+* Bug fixing
+	* Make article GUIDs case-sensitive also with MySQL [#2077](https://github.com/FreshRSS/FreshRSS/issues/2077)
+	* Ask confirmation for important configuration actions [#2048](https://github.com/FreshRSS/FreshRSS/pull/2048)
+	* Fix database size in the Web UI for users about to be deleted [#2047](https://github.com/FreshRSS/FreshRSS/pull/2047)
+	* Fix actualize bug after install [#2044](https://github.com/FreshRSS/FreshRSS/pull/2044)
+	* Fix manual / Web actualize for which the final commit coud be done too early [#2081](https://github.com/FreshRSS/FreshRSS/pull/2081)
+	* Fix regression from version 1.11.2, which might have wrongly believed that the server address was private [#2084](https://github.com/FreshRSS/FreshRSS/pull/2084)
+		* Please check in `data/config.php` that you have `'pubsubhubbub_enabled' => true,` if your server has a public address
+* Extensions
+	* Update built-in extension to again fix Tumblr feeds from European Union due to GDPR [#2053](https://github.com/FreshRSS/FreshRSS/pull/2053)
+* I18n
+	* Fix missing German translations, e.g. for *Sharing with Known* [#2059](https://github.com/FreshRSS/FreshRSS/pull/2059)
+* Misc.
+	* Better port detection behind a proxy [#2031](https://github.com/FreshRSS/FreshRSS/issues/2031)
+
+
 ## 2018-09-09 FreshRSS 1.11.2
 ## 2018-09-09 FreshRSS 1.11.2
 
 
 * Features
 * Features

+ 5 - 0
CREDITS.md

@@ -13,6 +13,7 @@ People are sorted by name so please keep this order.
 * [Amaury Carrade](https://github.com/AmauryCarrade): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=AmauryCarrade), [Web](https://amaury.carrade.eu/)
 * [Amaury Carrade](https://github.com/AmauryCarrade): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=AmauryCarrade), [Web](https://amaury.carrade.eu/)
 * [Anton Smirnov](https://github.com/sandfoxme): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:sandfoxme), [Web](http://sandfox.me/)
 * [Anton Smirnov](https://github.com/sandfoxme): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:sandfoxme), [Web](http://sandfox.me/)
 * [ASMfreaK](https://github.com/ASMfreaK): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ASMfreaK)
 * [ASMfreaK](https://github.com/ASMfreaK): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ASMfreaK)
+* [chemical1979](https://github.com/chemical1979): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=chemical1979)
 * [Craig Andrews](https://github.com/candrews): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:candrews), [Web](http://candrews.integralblue.com/)
 * [Craig Andrews](https://github.com/candrews): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:candrews), [Web](http://candrews.integralblue.com/)
 * [Crupuk](https://github.com/Crupuk): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Crupuk)
 * [Crupuk](https://github.com/Crupuk): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Crupuk)
 * [Damstre](https://github.com/Damstre): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Damstre)
 * [Damstre](https://github.com/Damstre): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Damstre)
@@ -25,6 +26,7 @@ People are sorted by name so please keep this order.
 * [gsongsong](https://github.com/gsongsong): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gsongsong)
 * [gsongsong](https://github.com/gsongsong): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gsongsong)
 * [Guillaume Fillon](https://github.com/kokaz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kokaz), [Web](http://www.guillaume-fillon.com/)
 * [Guillaume Fillon](https://github.com/kokaz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kokaz), [Web](http://www.guillaume-fillon.com/)
 * [Guillaume Hayot](https://github.com/postblue): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:postblue), [Web](https://postblue.info/)
 * [Guillaume Hayot](https://github.com/postblue): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:postblue), [Web](https://postblue.info/)
+* [Gregor Nathanael Meyer](https://github.com/spackmat): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:spackmat), [Web](https://der-meyer.de)
 * [hckweb](https://github.com/hckweb): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=hckweb)
 * [hckweb](https://github.com/hckweb): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=hckweb)
 * [hoilc](https://github.com/hoilc): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hoilc)
 * [hoilc](https://github.com/hoilc): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hoilc)
 * [Jan van den Berg](https://github.com/jan-vandenberg): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jan-vandenberg), [Web](https://j11g.com/)
 * [Jan van den Berg](https://github.com/jan-vandenberg): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jan-vandenberg), [Web](https://j11g.com/)
@@ -45,6 +47,7 @@ People are sorted by name so please keep this order.
 * [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/commits?author=nicosomb), [Web](http://www.loeuillet.org/)
 * [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/commits?author=nicosomb), [Web](http://www.loeuillet.org/)
 * [Nicola Spanti](https://github.com/RyDroid): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:RyDroid), [Web](http://www.nicola-spanti.info/)
 * [Nicola Spanti](https://github.com/RyDroid): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:RyDroid), [Web](http://www.nicola-spanti.info/)
 * [Olivier Dossmann](https://github.com/blankoworld): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=blankoworld), [Web](https://olivier.dossmann.net)
 * [Olivier Dossmann](https://github.com/blankoworld): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=blankoworld), [Web](https://olivier.dossmann.net)
+* [Patrick Crandol](https://github.com/pattems): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:pattems)
 * [perrinjerome](https://github.com/perrinjerome): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:perrinjerome)
 * [perrinjerome](https://github.com/perrinjerome): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:perrinjerome)
 * [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=plopoyop)
 * [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=plopoyop)
 * [Paulius Šukys](https://github.com/psukys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:psukys), [Web](http://sukys.eu)
 * [Paulius Šukys](https://github.com/psukys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:psukys), [Web](http://sukys.eu)
@@ -53,9 +56,11 @@ People are sorted by name so please keep this order.
 * [Quentin Dufour](https://github.com/superboum): [contributions](https://github.com/FreshRSS/documentation/commits?author=superboum), [Web](http://quentin.dufour.io/)
 * [Quentin Dufour](https://github.com/superboum): [contributions](https://github.com/FreshRSS/documentation/commits?author=superboum), [Web](http://quentin.dufour.io/)
 * [Ramón Cutanda](https://github.com/rcutanda): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rcutanda)
 * [Ramón Cutanda](https://github.com/rcutanda): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rcutanda)
 * [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/commits/dev?author=romibi)
 * [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/commits/dev?author=romibi)
+* [sirideain](https://github.com/sirideain): [contributions](https://github.com/FreshRSS/FreshRSS/commits/dev?author=sirideain)
 * [subic](https://github.com/subic): [contributions](https://github.com/FreshRSS/documentation/commits?author=subic)
 * [subic](https://github.com/subic): [contributions](https://github.com/FreshRSS/documentation/commits?author=subic)
 * [Tets42](https://github.com/Tets42): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Tets42)
 * [Tets42](https://github.com/Tets42): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Tets42)
 * [Thomas Citharel](https://github.com/tcitworld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:tomgue), [Web](https://www.tcit.fr/)
 * [Thomas Citharel](https://github.com/tcitworld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:tomgue), [Web](https://www.tcit.fr/)
 * [tomgue](https://github.com/tomgue): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=tomgue)
 * [tomgue](https://github.com/tomgue): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=tomgue)
+* [Twilek-de](https://github.com/Twilek-de): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Twilek-de)
 * [Uncovery](https://github.com/uncovery): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:uncovery)
 * [Uncovery](https://github.com/uncovery): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:uncovery)
 * [Wanabo](https://github.com/Wanabo): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Wanabo)
 * [Wanabo](https://github.com/Wanabo): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Wanabo)

+ 1 - 1
Docker/entrypoint.sh

@@ -6,7 +6,7 @@ chown -R :www-data .
 chmod -R g+r . && chmod -R g+w ./data/
 chmod -R g+r . && chmod -R g+w ./data/
 
 
 if [ -n "$CRON_MIN" ]; then
 if [ -n "$CRON_MIN" ]; then
-	sed -r -i "/FreshRSS/s/^[^ ]+ /$CRON_MIN /" /var/spool/cron/crontabs/root
+	sed -r -i "\#FreshRSS#s#^[^ ]+ #$CRON_MIN #" /var/spool/cron/crontabs/root
 fi
 fi
 
 
 exec "$@"
 exec "$@"

+ 4 - 9
README.fr.md

@@ -4,12 +4,12 @@
 * [English version](README.md)
 * [English version](README.md)
 
 
 # FreshRSS
 # FreshRSS
-FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](http://projet.idleman.fr/leed/) ou de [Kriss Feed](https://tontof.net/kriss/feed/).
+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/).
 
 
 Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
 Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
 
 
 Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme.
 Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme.
-Il supporte [PubSubHubbub](https://github.com/pubsubhubbub/PubSubHubbub) pour des notifications instantanées depuis les sites compatibles.
+Il supporte les étiquettes personnalisées, et [PubSubHubbub](https://github.com/pubsubhubbub/PubSubHubbub) pour des notifications instantanées depuis les sites compatibles.
 Il y a une API pour les clients (mobiles), ainsi qu’une [interface en ligne de commande](cli/README.md).
 Il y a une API pour les clients (mobiles), ainsi qu’une [interface en ligne de commande](cli/README.md).
 Enfin, il permet l’ajout d’[extensions](#extensions) pour encore plus de personnalisation.
 Enfin, il permet l’ajout d’[extensions](#extensions) pour encore plus de personnalisation.
 
 
@@ -70,13 +70,7 @@ Plus d’informations sur l’installation et la configuration serveur peuvent 
 sudo apt-get install apache2
 sudo apt-get install apache2
 sudo a2enmod headers expires rewrite ssl	#Modules Apache
 sudo a2enmod headers expires rewrite ssl	#Modules Apache
 
 
-# Pour Ubuntu <= 15.10, Debian <= 8 Jessie
-sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
-sudo apt-get install libapache2-mod-php5	#Pour Apache
-sudo apt-get install mysql-server mysql-client php5-mysql	#Base de données MySQL optionnelle
-sudo apt-get install postgresql php5-pgsql	#Base de données PostgreSQL optionnelle
-
-# Pour Ubuntu >= 16.04, Debian >= 9 Stretch
+# Exemple pour Ubuntu >= 16.04, Debian >= 9 Stretch
 sudo apt install php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
 sudo apt install php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
 sudo apt install libapache2-mod-php	#Pour Apache
 sudo apt install libapache2-mod-php	#Pour Apache
 sudo apt install mysql-server mysql-client php-mysql	#Base de données MySQL optionnelle
 sudo apt install mysql-server mysql-client php-mysql	#Base de données MySQL optionnelle
@@ -187,6 +181,7 @@ Tout client supportant une API de type Fever ; Sélection :
 * iOS
 * iOS
 	* [Fiery Feeds](https://itunes.apple.com/app/fiery-feeds-rss-reader/id1158763303) (Propriétaire)
 	* [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)
 	* [Unread](https://itunes.apple.com/app/unread-rss-reader/id1252376153) (Propriétaire)
+	* [Reeder-3](https://itunes.apple.com/app/reeder-3/id697846300) (Propriétaire)
 * MacOS
 * MacOS
 	* [Readkit](https://itunes.apple.com/app/readkit/id588726889) (Propriétaire)
 	* [Readkit](https://itunes.apple.com/app/readkit/id588726889) (Propriétaire)
 
 

+ 4 - 9
README.md

@@ -4,12 +4,12 @@
 * [Version française](README.fr.md)
 * [Version française](README.fr.md)
 
 
 # FreshRSS
 # FreshRSS
-FreshRSS is a self-hosted RSS feed aggregator such as [Leed](http://projet.idleman.fr/leed/) or [Kriss Feed](https://tontof.net/kriss/feed/).
+FreshRSS is a self-hosted RSS feed aggregator such as [Leed](http://leed.idleman.fr/) or [Kriss Feed](https://tontof.net/kriss/feed/).
 
 
 It is at the same time lightweight, easy to work with, powerful and customizable.
 It is at the same time lightweight, easy to work with, powerful and customizable.
 
 
 It is a multi-user application with an anonymous reading mode.
 It is a multi-user application with an anonymous reading mode.
-It supports [PubSubHubbub](https://github.com/pubsubhubbub/PubSubHubbub) for instant notifications from compatible Web sites.
+It supports custom tags, and [PubSubHubbub](https://github.com/pubsubhubbub/PubSubHubbub) for instant notifications from compatible Web sites.
 There is an API for (mobile) clients, and a [Command-Line Interface](cli/README.md).
 There is an API for (mobile) clients, and a [Command-Line Interface](cli/README.md).
 Finally, it supports [extensions](#extensions) for further tuning.
 Finally, it supports [extensions](#extensions) for further tuning.
 
 
@@ -70,13 +70,7 @@ More information about installation and server configuration can be found in [ou
 sudo apt-get install apache2
 sudo apt-get install apache2
 sudo a2enmod headers expires rewrite ssl	#Apache modules
 sudo a2enmod headers expires rewrite ssl	#Apache modules
 
 
-# For Ubuntu <= 15.10, Debian <= 8 Jessie
-sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
-sudo apt-get install libapache2-mod-php5	#For Apache
-sudo apt-get install mysql-server mysql-client php5-mysql	#Optional MySQL database
-sudo apt-get install postgresql php5-pgsql	#Optional PostgreSQL database
-
-# For Ubuntu >= 16.04, Debian >= 9 Stretch
+# 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 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 libapache2-mod-php	#For Apache
 sudo apt install mysql-server mysql-client php-mysql	#Optional MySQL database
 sudo apt install mysql-server mysql-client php-mysql	#Optional MySQL database
@@ -187,6 +181,7 @@ Supported clients are:
 * iOS
 * iOS
 	* [Fiery Feeds](https://itunes.apple.com/app/fiery-feeds-rss-reader/id1158763303) (Closed source)
 	* [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)
 	* [Unread](https://itunes.apple.com/app/unread-rss-reader/id1252376153) (Closed source)
+	* [Reeder-3](https://itunes.apple.com/app/reeder-3/id697846300) (Closed source)
 * MacOS
 * MacOS
 	* [Readkit](https://itunes.apple.com/app/readkit/id588726889) (Closed source)
 	* [Readkit](https://itunes.apple.com/app/readkit/id588726889) (Closed source)
 
 

+ 4 - 4
app/Controllers/categoryController.php

@@ -16,7 +16,7 @@ class FreshRSS_category_Controller extends Minz_ActionController {
 			Minz_Error::error(403);
 			Minz_Error::error(403);
 		}
 		}
 
 
-		$catDAO = new FreshRSS_CategoryDAO();
+		$catDAO = FreshRSS_Factory::createCategoryDao();
 		$catDAO->checkDefault();
 		$catDAO->checkDefault();
 	}
 	}
 
 
@@ -27,7 +27,7 @@ class FreshRSS_category_Controller extends Minz_ActionController {
 	 *   - new-category
 	 *   - new-category
 	 */
 	 */
 	public function createAction() {
 	public function createAction() {
-		$catDAO = new FreshRSS_CategoryDAO();
+		$catDAO = FreshRSS_Factory::createCategoryDao();
 		$url_redirect = array('c' => 'subscription', 'a' => 'index');
 		$url_redirect = array('c' => 'subscription', 'a' => 'index');
 
 
 		$limits = FreshRSS_Context::$system_conf->limits;
 		$limits = FreshRSS_Context::$system_conf->limits;
@@ -75,7 +75,7 @@ class FreshRSS_category_Controller extends Minz_ActionController {
 	 *   - name
 	 *   - name
 	 */
 	 */
 	public function updateAction() {
 	public function updateAction() {
-		$catDAO = new FreshRSS_CategoryDAO();
+		$catDAO = FreshRSS_Factory::createCategoryDao();
 		$url_redirect = array('c' => 'subscription', 'a' => 'index');
 		$url_redirect = array('c' => 'subscription', 'a' => 'index');
 
 
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
@@ -116,7 +116,7 @@ class FreshRSS_category_Controller extends Minz_ActionController {
 	 */
 	 */
 	public function deleteAction() {
 	public function deleteAction() {
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
-		$catDAO = new FreshRSS_CategoryDAO();
+		$catDAO = FreshRSS_Factory::createCategoryDao();
 		$url_redirect = array('c' => 'subscription', 'a' => 'index');
 		$url_redirect = array('c' => 'subscription', 'a' => 'index');
 
 
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {

+ 6 - 4
app/Controllers/configureController.php

@@ -243,8 +243,9 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 	 * checking if categories and feeds are still in use.
 	 * checking if categories and feeds are still in use.
 	 */
 	 */
 	public function queriesAction() {
 	public function queriesAction() {
-		$category_dao = new FreshRSS_CategoryDAO();
+		$category_dao = FreshRSS_Factory::createCategoryDao();
 		$feed_dao = FreshRSS_Factory::createFeedDao();
 		$feed_dao = FreshRSS_Factory::createFeedDao();
+		$tag_dao = FreshRSS_Factory::createTagDao();
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
 			$params = Minz_Request::param('queries', array());
 			$params = Minz_Request::param('queries', array());
 
 
@@ -277,16 +278,17 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 	 * lean data.
 	 * lean data.
 	 */
 	 */
 	public function addQueryAction() {
 	public function addQueryAction() {
-		$category_dao = new FreshRSS_CategoryDAO();
+		$category_dao = FreshRSS_Factory::createCategoryDao();
 		$feed_dao = FreshRSS_Factory::createFeedDao();
 		$feed_dao = FreshRSS_Factory::createFeedDao();
+		$tag_dao = FreshRSS_Factory::createTagDao();
 		$queries = array();
 		$queries = array();
 		foreach (FreshRSS_Context::$user_conf->queries as $key => $query) {
 		foreach (FreshRSS_Context::$user_conf->queries as $key => $query) {
-			$queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao);
+			$queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao);
 		}
 		}
 		$params = Minz_Request::fetchGET();
 		$params = Minz_Request::fetchGET();
 		$params['url'] = Minz_Url::display(array('params' => $params));
 		$params['url'] = Minz_Url::display(array('params' => $params));
 		$params['name'] = _t('conf.query.number', count($queries) + 1);
 		$params['name'] = _t('conf.query.number', count($queries) + 1);
-		$queries[] = new FreshRSS_UserQuery($params, $feed_dao, $category_dao);
+		$queries[] = new FreshRSS_UserQuery($params, $feed_dao, $category_dao, $tag_dao);
 
 
 		FreshRSS_Context::$user_conf->queries = $queries;
 		FreshRSS_Context::$user_conf->queries = $queries;
 		FreshRSS_Context::$user_conf->save();
 		FreshRSS_Context::$user_conf->save();

+ 17 - 0
app/Controllers/entryController.php

@@ -53,6 +53,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 		}
 		}
 
 
 		$params = array();
 		$params = array();
+		$this->view->tags = array();
 
 
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 		if ($id === false) {
 		if ($id === false) {
@@ -81,6 +82,12 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 				case 'a':
 				case 'a':
 					$entryDAO->markReadEntries($id_max, false, 0, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
 					$entryDAO->markReadEntries($id_max, false, 0, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
 					break;
 					break;
+				case 't':
+					$entryDAO->markReadTag($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
+					break;
+				case 'T':
+					$entryDAO->markReadTag('', $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
+					break;
 				}
 				}
 
 
 				if ($next_get !== 'a') {
 				if ($next_get !== 'a') {
@@ -91,6 +98,13 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 			}
 			}
 		} else {
 		} else {
 			$entryDAO->markRead($id, $is_read);
 			$entryDAO->markRead($id, $is_read);
+
+			$tagDAO = FreshRSS_Factory::createTagDao();
+			foreach ($tagDAO->getTagsForEntry($id) as $tag) {
+				if (!empty($tag['checked'])) {
+					$this->view->tags[] = $tag['id'];
+				}
+			}
 		}
 		}
 
 
 		if (!$this->ajax) {
 		if (!$this->ajax) {
@@ -193,6 +207,9 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 
 
 		$feedDAO->updateCachedValues();
 		$feedDAO->updateCachedValues();
 
 
+		$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
+		$databaseDAO->minorDbMaintenance();
+
 		invalidateHttpCache();
 		invalidateHttpCache();
 		Minz_Request::good(_t('feedback.sub.purge_completed', $nb_total), array(
 		Minz_Request::good(_t('feedback.sub.purge_completed', $nb_total), array(
 			'c' => 'configure',
 			'c' => 'configure',

+ 9 - 3
app/Controllers/feedController.php

@@ -43,7 +43,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		FreshRSS_UserDAO::touch();
 		FreshRSS_UserDAO::touch();
 		@set_time_limit(300);
 		@set_time_limit(300);
 
 
-		$catDAO = new FreshRSS_CategoryDAO();
+		$catDAO = FreshRSS_Factory::createCategoryDao();
 
 
 		$url = trim($url);
 		$url = trim($url);
 
 
@@ -192,7 +192,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			// GET request: we must ask confirmation to user before adding feed.
 			// GET request: we must ask confirmation to user before adding feed.
 			Minz_View::prependTitle(_t('sub.feed.title_add') . ' · ');
 			Minz_View::prependTitle(_t('sub.feed.title_add') . ' · ');
 
 
-			$this->catDAO = new FreshRSS_CategoryDAO();
+			$this->catDAO = FreshRSS_Factory::createCategoryDao();
 			$this->view->categories = $this->catDAO->listCategories(false);
 			$this->view->categories = $this->catDAO->listCategories(false);
 			$this->view->feed = new FreshRSS_Feed($url);
 			$this->view->feed = new FreshRSS_Feed($url);
 			try {
 			try {
@@ -481,6 +481,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			if ($entryDAO->inTransaction()) {
 			if ($entryDAO->inTransaction()) {
 				$entryDAO->commit();
 				$entryDAO->commit();
 			}
 			}
+
+			$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
+			$databaseDAO->minorDbMaintenance();
 		}
 		}
 		return array($updated_feeds, reset($feeds), $nb_new_articles);
 		return array($updated_feeds, reset($feeds), $nb_new_articles);
 	}
 	}
@@ -511,6 +514,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$entryDAO->commitNewEntries();
 			$entryDAO->commitNewEntries();
 			$feedDAO->updateCachedValues();
 			$feedDAO->updateCachedValues();
 			$entryDAO->commit();
 			$entryDAO->commit();
+
+			$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
+			$databaseDAO->minorDbMaintenance();
 		} else {
 		} else {
 			list($updated_feeds, $feed, $nb_new_articles) = self::actualizeFeed($id, $url, $force, null, false, $noCommit);
 			list($updated_feeds, $feed, $nb_new_articles) = self::actualizeFeed($id, $url, $force, null, false, $noCommit);
 		}
 		}
@@ -556,7 +562,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 		FreshRSS_UserDAO::touch();
 		FreshRSS_UserDAO::touch();
 
 
-		$catDAO = new FreshRSS_CategoryDAO();
+		$catDAO = FreshRSS_Factory::createCategoryDao();
 		if ($cat_id > 0) {
 		if ($cat_id > 0) {
 			$cat = $catDAO->searchById($cat_id);
 			$cat = $catDAO->searchById($cat_id);
 			$cat_id = $cat == null ? 0 : $cat->id();
 			$cat_id = $cat == null ? 0 : $cat->id();

+ 24 - 11
app/Controllers/indexController.php

@@ -32,7 +32,29 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 			Minz_Error::error(404);
 			Minz_Error::error(404);
 		}
 		}
 
 
-		$this->view->callbackBeforeContent = function($view) {
+		$this->view->categories = FreshRSS_Context::$categories;
+
+		$this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title();
+		$title = FreshRSS_Context::$name;
+		if (FreshRSS_Context::$get_unread > 0) {
+			$title = '(' . FreshRSS_Context::$get_unread . ') ' . $title;
+		}
+		Minz_View::prependTitle($title . ' · ');
+
+		$this->view->callbackBeforeFeeds = function ($view) {
+			try {
+				$tagDAO = FreshRSS_Factory::createTagDao();
+				$view->tags = $tagDAO->listTags(true);
+				$view->nbUnreadTags = 0;
+				foreach ($view->tags as $tag) {
+					$view->nbUnreadTags += $tag->nbUnread();
+				}
+			} catch (Exception $e) {
+				Minz_Log::notice($e->getMessage());
+			}
+		};
+
+		$this->view->callbackBeforeEntries = function ($view) {
 			try {
 			try {
 				FreshRSS_Context::$number++;	//+1 for pagination
 				FreshRSS_Context::$number++;	//+1 for pagination
 				$entries = FreshRSS_index_Controller::listEntriesByContext();
 				$entries = FreshRSS_index_Controller::listEntriesByContext();
@@ -60,15 +82,6 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 				Minz_Log::notice($e->getMessage());
 				Minz_Log::notice($e->getMessage());
 				Minz_Error::error(404);
 				Minz_Error::error(404);
 			}
 			}
-
-			$view->categories = FreshRSS_Context::$categories;
-
-			$view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title();
-			$title = FreshRSS_Context::$name;
-			if (FreshRSS_Context::$get_unread > 0) {
-				$title = '(' . FreshRSS_Context::$get_unread . ') ' . $title;
-			}
-			Minz_View::prependTitle($title . ' · ');
 		};
 		};
 	}
 	}
 
 
@@ -158,7 +171,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 	 */
 	 */
 	private function updateContext() {
 	private function updateContext() {
 		if (empty(FreshRSS_Context::$categories)) {
 		if (empty(FreshRSS_Context::$categories)) {
-			$catDAO = new FreshRSS_CategoryDAO();
+			$catDAO = FreshRSS_Factory::createCategoryDao();
 			FreshRSS_Context::$categories = $catDAO->listCategories();
 			FreshRSS_Context::$categories = $catDAO->listCategories();
 		}
 		}
 
 

+ 4 - 1
app/Controllers/javascriptController.php

@@ -7,14 +7,17 @@ class FreshRSS_javascript_Controller extends Minz_ActionController {
 
 
 	public function actualizeAction() {
 	public function actualizeAction() {
 		header('Content-Type: application/json; charset=UTF-8');
 		header('Content-Type: application/json; charset=UTF-8');
+		Minz_Session::_param('actualize_feeds', false);
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
 		$this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
 	}
 	}
 
 
 	public function nbUnreadsPerFeedAction() {
 	public function nbUnreadsPerFeedAction() {
 		header('Content-Type: application/json; charset=UTF-8');
 		header('Content-Type: application/json; charset=UTF-8');
-		$catDAO = new FreshRSS_CategoryDAO();
+		$catDAO = FreshRSS_Factory::createCategoryDao();
 		$this->view->categories = $catDAO->listCategories(true, false);
 		$this->view->categories = $catDAO->listCategories(true, false);
+		$tagDAO = FreshRSS_Factory::createTagDao();
+		$this->view->tags = $tagDAO->listTags(true);
 	}
 	}
 
 
 	//For Web-form login
 	//For Web-form login

+ 1 - 1
app/Controllers/statsController.php

@@ -131,7 +131,7 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
 	 */
 	 */
 	public function repartitionAction() {
 	public function repartitionAction() {
 		$statsDAO = FreshRSS_Factory::createStatsDAO();
 		$statsDAO = FreshRSS_Factory::createStatsDAO();
-		$categoryDAO = new FreshRSS_CategoryDAO();
+		$categoryDAO = FreshRSS_Factory::createCategoryDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
 		Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
 		$id = Minz_Request::param('id', null);
 		$id = Minz_Request::param('id', null);

+ 2 - 1
app/Controllers/subscriptionController.php

@@ -14,7 +14,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 			Minz_Error::error(403);
 			Minz_Error::error(403);
 		}
 		}
 
 
-		$catDAO = new FreshRSS_CategoryDAO();
+		$catDAO = FreshRSS_Factory::createCategoryDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 
 
 		$catDAO->checkDefault();
 		$catDAO->checkDefault();
@@ -98,6 +98,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 
 
 			$feed->_attributes('mark_updated_article_unread', Minz_Request::paramTernary('mark_updated_article_unread'));
 			$feed->_attributes('mark_updated_article_unread', Minz_Request::paramTernary('mark_updated_article_unread'));
 			$feed->_attributes('read_upon_reception', Minz_Request::paramTernary('read_upon_reception'));
 			$feed->_attributes('read_upon_reception', Minz_Request::paramTernary('read_upon_reception'));
+			$feed->_attributes('clear_cache', Minz_Request::paramTernary('clear_cache'));
 
 
 			if (FreshRSS_Auth::hasAccess('admin')) {
 			if (FreshRSS_Auth::hasAccess('admin')) {
 				$feed->_attributes('ssl_verify', Minz_Request::paramTernary('ssl_verify'));
 				$feed->_attributes('ssl_verify', Minz_Request::paramTernary('ssl_verify'));

+ 80 - 0
app/Controllers/tagController.php

@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * Controller to handle every tag actions.
+ */
+class FreshRSS_tag_Controller extends Minz_ActionController {
+	/**
+	 * This action is called before every other action in that class. It is
+	 * the common boiler plate for every action. It is triggered by the
+	 * underlying framework.
+	 */
+	public function firstAction() {
+		if (!FreshRSS_Auth::hasAccess()) {
+			Minz_Error::error(403);
+		}
+		// If ajax request, we do not print layout
+		$this->ajax = Minz_Request::param('ajax');
+		if ($this->ajax) {
+			$this->view->_useLayout(false);
+			Minz_Request::_param('ajax');
+		}
+	}
+
+	/**
+	 * This action adds (checked=true) or removes (checked=false) a tag to an entry.
+	 */
+	public function tagEntryAction() {
+		if (Minz_Request::isPost()) {
+			$id_tag = Minz_Request::param('id_tag');
+			$name_tag = trim(Minz_Request::param('name_tag'));
+			$id_entry = Minz_Request::param('id_entry');
+			$checked = Minz_Request::paramTernary('checked');
+			if ($id_entry != false) {
+				$tagDAO = FreshRSS_Factory::createTagDao();
+				if ($id_tag == 0 && $name_tag != '' && $checked) {
+					//Create new tag
+					$id_tag = $tagDAO->addTag(array('name' => $name_tag));
+				}
+				if ($id_tag != 0) {
+					$tagDAO->tagEntry($id_tag, $id_entry, $checked);
+				}
+			}
+		} else {
+			Minz_Error::error(405);
+		}
+		if (!$this->ajax) {
+			Minz_Request::forward(array(
+				'c' => 'index',
+				'a' => 'index',
+			), true);
+		}
+	}
+
+	public function deleteAction() {
+		if (Minz_Request::isPost()) {
+			$id_tag = Minz_Request::param('id_tag');
+			if ($id_tag != false) {
+				$tagDAO = FreshRSS_Factory::createTagDao();
+				$tagDAO->deleteTag($id_tag);
+			}
+		} else {
+			Minz_Error::error(405);
+		}
+		if (!$this->ajax) {
+			Minz_Request::forward(array(
+				'c' => 'index',
+				'a' => 'index',
+			), true);
+		}
+	}
+
+	public function getTagsForEntryAction() {
+		$this->view->_useLayout(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);
+		$tagDAO = FreshRSS_Factory::createTagDao();
+		$this->view->tags = $tagDAO->getTagsForEntry($id_entry);
+	}
+}

+ 7 - 1
app/Controllers/updateController.php

@@ -32,7 +32,13 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 		$output = array();
 		$output = array();
 		$return = 1;
 		$return = 1;
 		try {
 		try {
-			exec('git pull --ff-only', $output, $return);
+			exec('git clean -f -d -f', $output, $return);
+			if ($return == 0) {
+				exec('git pull --ff-only', $output, $return);
+			} else {
+				$line = is_array($output) ? implode('; ', $output) : '' . $output;
+				Minz_Log::warning('git clean warning:' . $line);
+			}
 		} catch (Exception $e) {
 		} catch (Exception $e) {
 			Minz_Log::warning('git pull error:' . $e->getMessage());
 			Minz_Log::warning('git pull error:' . $e->getMessage());
 		}
 		}

+ 17 - 6
app/Controllers/userController.php

@@ -38,7 +38,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	 * The username is also used as folder name, file name, and part of SQL table name.
 	 * The username is also used as folder name, file name, and part of SQL table name.
 	 * '_' is a reserved internal username.
 	 * '_' is a reserved internal username.
 	 */
 	 */
-	const USERNAME_PATTERN = '[0-9a-zA-Z_]{2,38}|[0-9a-zA-Z]';
+	const USERNAME_PATTERN = '[0-9a-zA-Z_][0-9a-zA-Z_.]{1,38}|[0-9a-zA-Z]';
 
 
 	public static function checkUsername($username) {
 	public static function checkUsername($username) {
 		return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1;
 		return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1;
@@ -91,6 +91,10 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	}
 	}
 
 
 	public function updateAction() {
 	public function updateAction() {
+		if (!FreshRSS_Auth::hasAccess('admin')) {
+			Minz_Error::error(403);
+		}
+
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
 			$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
 			$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
 			Minz_Request::_param('newPasswordPlain');	//Discard plain-text password ASAP
 			Minz_Request::_param('newPasswordPlain');	//Discard plain-text password ASAP
@@ -104,8 +108,12 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			));
 			));
 
 
 			if ($ok) {
 			if ($ok) {
-				Minz_Request::good(_t('feedback.user.updated', $username),
-				                   array('c' => 'user', 'a' => 'manage'));
+				$isSelfUpdate = Minz_Session::param('currentUser', '_') === $username;
+				if ($passwordPlain == '' || !$isSelfUpdate) {
+					Minz_Request::good(_t('feedback.user.updated', $username), array('c' => 'user', 'a' => 'manage'));
+				} else {
+					Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'index', 'a' => 'index'));
+				}
 			} else {
 			} else {
 				Minz_Request::bad(_t('feedback.user.updated.error', $username),
 				Minz_Request::bad(_t('feedback.user.updated.error', $username),
 				                  array('c' => 'user', 'a' => 'manage'));
 				                  array('c' => 'user', 'a' => 'manage'));
@@ -138,8 +146,11 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash);
 			Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash);
 
 
 			if ($ok) {
 			if ($ok) {
-				Minz_Request::good(_t('feedback.profile.updated'),
-				                   array('c' => 'user', 'a' => 'profile'));
+				if ($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'));
+				}
 			} else {
 			} else {
 				Minz_Request::bad(_t('feedback.profile.error'),
 				Minz_Request::bad(_t('feedback.profile.error'),
 				                  array('c' => 'user', 'a' => 'profile'));
 				                  array('c' => 'user', 'a' => 'profile'));
@@ -166,7 +177,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			$entryDAO = FreshRSS_Factory::createEntryDao($this->view->current_user);
 			$entryDAO = FreshRSS_Factory::createEntryDao($this->view->current_user);
 			$this->view->nb_articles = $entryDAO->count();
 			$this->view->nb_articles = $entryDAO->count();
 
 
-			$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
+			$databaseDAO = FreshRSS_Factory::createDatabaseDAO($this->view->current_user);
 			$this->view->size_user = $databaseDAO->size();
 			$this->view->size_user = $databaseDAO->size();
 		}
 		}
 	}
 	}

+ 0 - 1
app/FreshRSS.php

@@ -90,7 +90,6 @@ class FreshRSS extends Minz_FrontController {
 				}
 				}
 				$filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename);
 				$filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename);
 				$url = '/themes/' . $theme_id . '/' . $filename . '?' . $filetime;
 				$url = '/themes/' . $theme_id . '/' . $filename . '?' . $filetime;
-				header('Link: <' . Minz_Url::display($url, '', 'root') . '>;rel=preload', false);	//HTTP2
 				Minz_View::prependStyle(Minz_Url::display($url));
 				Minz_View::prependStyle(Minz_Url::display($url));
 			}
 			}
 		}
 		}

+ 3 - 3
app/Models/Category.php

@@ -30,7 +30,7 @@ class FreshRSS_Category extends Minz_Model {
 	}
 	}
 	public function nbFeed() {
 	public function nbFeed() {
 		if ($this->nbFeed < 0) {
 		if ($this->nbFeed < 0) {
-			$catDAO = new FreshRSS_CategoryDAO();
+			$catDAO = FreshRSS_Factory::createCategoryDao();
 			$this->nbFeed = $catDAO->countFeed($this->id());
 			$this->nbFeed = $catDAO->countFeed($this->id());
 		}
 		}
 
 
@@ -38,7 +38,7 @@ class FreshRSS_Category extends Minz_Model {
 	}
 	}
 	public function nbNotRead() {
 	public function nbNotRead() {
 		if ($this->nbNotRead < 0) {
 		if ($this->nbNotRead < 0) {
-			$catDAO = new FreshRSS_CategoryDAO();
+			$catDAO = FreshRSS_Factory::createCategoryDao();
 			$this->nbNotRead = $catDAO->countNotRead($this->id());
 			$this->nbNotRead = $catDAO->countNotRead($this->id());
 		}
 		}
 
 
@@ -68,7 +68,7 @@ class FreshRSS_Category extends Minz_Model {
 		$this->id = $value;
 		$this->id = $value;
 	}
 	}
 	public function _name($value) {
 	public function _name($value) {
-		$this->name = mb_strcut(trim($value), 0, 255, 'UTF-8');
+		$this->name = trim($value);
 	}
 	}
 	public function _feeds($values) {
 	public function _feeds($values) {
 		if (!is_array($values)) {
 		if (!is_array($values)) {

+ 12 - 5
app/Models/CategoryDAO.php

@@ -5,11 +5,15 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 	const DEFAULTCATEGORYID = 1;
 	const DEFAULTCATEGORYID = 1;
 
 
 	public function addCategory($valuesTmp) {
 	public function addCategory($valuesTmp) {
-		$sql = 'INSERT INTO `' . $this->prefix . 'category`(name) VALUES(?)';
+		$sql = 'INSERT INTO `' . $this->prefix . 'category`(name) '
+		     . '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);
 		$stm = $this->bd->prepare($sql);
 
 
+		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
 		$values = array(
 		$values = array(
-			mb_strcut($valuesTmp['name'], 0, 255, 'UTF-8'),
+			$valuesTmp['name'],
+			$valuesTmp['name'],
 		);
 		);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
@@ -35,12 +39,15 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 	}
 	}
 
 
 	public function updateCategory($id, $valuesTmp) {
 	public function updateCategory($id, $valuesTmp) {
-		$sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=?';
+		$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);
 		$stm = $this->bd->prepare($sql);
 
 
+		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
 		$values = array(
 		$values = array(
 			$valuesTmp['name'],
 			$valuesTmp['name'],
-			$id
+			$id,
+			$valuesTmp['name'],
 		);
 		);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
@@ -151,7 +158,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 			$sql = 'INSERT INTO `' . $this->prefix . 'category`(id, name) VALUES(?, ?)';
 			$sql = 'INSERT INTO `' . $this->prefix . 'category`(id, name) VALUES(?, ?)';
 			if (parent::$sharedDbType === 'pgsql') {
 			if (parent::$sharedDbType === 'pgsql') {
 				//Force call to nextval()
 				//Force call to nextval()
-				$sql .= " RETURNING nextval('" . $this->prefix . "category_id_seq');";
+				$sql .= ' RETURNING nextval(\'"' . $this->prefix . 'category_id_seq"\');';
 			}
 			}
 			$stm = $this->bd->prepare($sql);
 			$stm = $this->bd->prepare($sql);
 
 

+ 39 - 7
app/Models/Context.php

@@ -8,6 +8,7 @@ class FreshRSS_Context {
 	public static $user_conf = null;
 	public static $user_conf = null;
 	public static $system_conf = null;
 	public static $system_conf = null;
 	public static $categories = array();
 	public static $categories = array();
+	public static $tags = array();
 
 
 	public static $name = '';
 	public static $name = '';
 	public static $description = '';
 	public static $description = '';
@@ -25,6 +26,8 @@ class FreshRSS_Context {
 		'starred' => false,
 		'starred' => false,
 		'feed' => false,
 		'feed' => false,
 		'category' => false,
 		'category' => false,
+		'tag' => false,
+		'tags' => false,
 	);
 	);
 	public static $next_get = 'a';
 	public static $next_get = 'a';
 
 
@@ -91,6 +94,14 @@ class FreshRSS_Context {
 			} else {
 			} else {
 				return 'c_' . self::$current_get['category'];
 				return 'c_' . self::$current_get['category'];
 			}
 			}
+		} elseif (self::$current_get['tag']) {
+			if ($array) {
+				return array('t', self::$current_get['tag']);
+			} else {
+				return 't_' . self::$current_get['tag'];
+			}
+		} elseif (self::$current_get['tags']) {
+			return 'T';
 		}
 		}
 	}
 	}
 
 
@@ -117,6 +128,10 @@ class FreshRSS_Context {
 			return self::$current_get['feed'] == $id;
 			return self::$current_get['feed'] == $id;
 		case 'c':
 		case 'c':
 			return self::$current_get['category'] == $id;
 			return self::$current_get['category'] == $id;
+		case 't':
+			return self::$current_get['tag'] == $id;
+		case 'T':
+			return self::$current_get['tags'] || self::$current_get['tag'];
 		default:
 		default:
 			return false;
 			return false;
 		}
 		}
@@ -130,6 +145,7 @@ class FreshRSS_Context {
 	 *   - s
 	 *   - s
 	 *   - f_<feed id>
 	 *   - f_<feed id>
 	 *   - c_<category id>
 	 *   - c_<category id>
+	 *   - t_<tag id>
 	 *
 	 *
 	 * $name and $get_unread attributes are also updated as $next_get
 	 * $name and $get_unread attributes are also updated as $next_get
 	 * Raise an exception if id or $get is invalid.
 	 * Raise an exception if id or $get is invalid.
@@ -140,7 +156,7 @@ class FreshRSS_Context {
 		$nb_unread = 0;
 		$nb_unread = 0;
 
 
 		if (empty(self::$categories)) {
 		if (empty(self::$categories)) {
-			$catDAO = new FreshRSS_CategoryDAO();
+			$catDAO = FreshRSS_Factory::createCategoryDao();
 			self::$categories = $catDAO->listCategories();
 			self::$categories = $catDAO->listCategories();
 		}
 		}
 
 
@@ -166,12 +182,10 @@ class FreshRSS_Context {
 			if ($feed === null) {
 			if ($feed === null) {
 				$feedDAO = FreshRSS_Factory::createFeedDao();
 				$feedDAO = FreshRSS_Factory::createFeedDao();
 				$feed = $feedDAO->searchById($id);
 				$feed = $feedDAO->searchById($id);
-
 				if (!$feed) {
 				if (!$feed) {
 					throw new FreshRSS_Context_Exception('Invalid feed: ' . $id);
 					throw new FreshRSS_Context_Exception('Invalid feed: ' . $id);
 				}
 				}
 			}
 			}
-
 			self::$current_get['feed'] = $id;
 			self::$current_get['feed'] = $id;
 			self::$current_get['category'] = $feed->category();
 			self::$current_get['category'] = $feed->category();
 			self::$name = $feed->name();
 			self::$name = $feed->name();
@@ -182,19 +196,37 @@ class FreshRSS_Context {
 			// We try to find the corresponding category.
 			// We try to find the corresponding category.
 			self::$current_get['category'] = $id;
 			self::$current_get['category'] = $id;
 			if (!isset(self::$categories[$id])) {
 			if (!isset(self::$categories[$id])) {
-				$catDAO = new FreshRSS_CategoryDAO();
+				$catDAO = FreshRSS_Factory::createCategoryDao();
 				$cat = $catDAO->searchById($id);
 				$cat = $catDAO->searchById($id);
-
 				if (!$cat) {
 				if (!$cat) {
 					throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
 					throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
 				}
 				}
 			} else {
 			} else {
 				$cat = self::$categories[$id];
 				$cat = self::$categories[$id];
 			}
 			}
-
 			self::$name = $cat->name();
 			self::$name = $cat->name();
 			self::$get_unread = $cat->nbNotRead();
 			self::$get_unread = $cat->nbNotRead();
 			break;
 			break;
+		case 't':
+			// We try to find the corresponding tag.
+			self::$current_get['tag'] = $id;
+			if (!isset(self::$tags[$id])) {
+				$tagDAO = FreshRSS_Factory::createTagDao();
+				$tag = $tagDAO->searchById($id);
+				if (!$tag) {
+					throw new FreshRSS_Context_Exception('Invalid tag: ' . $id);
+				}
+			} else {
+				$tag = self::$tags[$id];
+			}
+			self::$name = $tag->name();
+			self::$get_unread = $tag->nbUnread();
+			break;
+		case 'T':
+			self::$current_get['tags'] = true;
+			self::$name = _t('index.menu.tags');
+			self::$get_unread = 0;
+			break;
 		default:
 		default:
 			throw new FreshRSS_Context_Exception('Invalid getter: ' . $get);
 			throw new FreshRSS_Context_Exception('Invalid getter: ' . $get);
 		}
 		}
@@ -211,7 +243,7 @@ class FreshRSS_Context {
 		self::$next_get = $get;
 		self::$next_get = $get;
 
 
 		if (empty(self::$categories)) {
 		if (empty(self::$categories)) {
-			$catDAO = new FreshRSS_CategoryDAO();
+			$catDAO = FreshRSS_Factory::createCategoryDao();
 			self::$categories = $catDAO->listCategories();
 			self::$categories = $catDAO->listCategories();
 		}
 		}
 
 

+ 66 - 23
app/Models/DatabaseDAO.php

@@ -4,6 +4,16 @@
  * This class is used to test database is well-constructed.
  * This class is used to test database is well-constructed.
  */
  */
 class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 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';
+
+	//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 tablesAreCorrect() {
 	public function tablesAreCorrect() {
 		$sql = 'SHOW TABLES';
 		$sql = 'SHOW TABLES';
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
@@ -14,6 +24,9 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 			$this->prefix . 'category' => false,
 			$this->prefix . 'category' => false,
 			$this->prefix . 'feed' => false,
 			$this->prefix . 'feed' => false,
 			$this->prefix . 'entry' => false,
 			$this->prefix . 'entry' => false,
+			$this->prefix . 'entrytmp' => false,
+			$this->prefix . 'tag' => false,
+			$this->prefix . 'entrytag' => false,
 		);
 		);
 		foreach ($res as $value) {
 		foreach ($res as $value) {
 			$tables[array_pop($value)] = true;
 			$tables[array_pop($value)] = true;
@@ -43,7 +56,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 
 
 	public function categoryIsCorrect() {
 	public function categoryIsCorrect() {
 		return $this->checkTable('category', array(
 		return $this->checkTable('category', array(
-			'id', 'name'
+			'id', 'name',
 		));
 		));
 	}
 	}
 
 
@@ -51,14 +64,33 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 		return $this->checkTable('feed', array(
 		return $this->checkTable('feed', array(
 			'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate',
 			'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate',
 			'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', 'attributes',
 			'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', 'attributes',
-			'cache_nbEntries', 'cache_nbUnreads'
+			'cache_nbEntries', 'cache_nbUnreads',
 		));
 		));
 	}
 	}
 
 
 	public function entryIsCorrect() {
 	public function entryIsCorrect() {
 		return $this->checkTable('entry', array(
 		return $this->checkTable('entry', array(
-			'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'is_read',
-			'is_favorite', 'id_feed', 'tags'
+			'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'lastSeen', 'hash', 'is_read',
+			'is_favorite', 'id_feed', 'tags',
+		));
+	}
+
+	public function entrytmpIsCorrect() {
+		return $this->checkTable('entrytmp', array(
+			'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'lastSeen', 'hash', 'is_read',
+			'is_favorite', 'id_feed', 'tags',
+		));
+	}
+
+	public function tagIsCorrect() {
+		return $this->checkTable('tag', array(
+			'id', 'name', 'attributes',
+		));
+	}
+
+	public function entrytagIsCorrect() {
+		return $this->checkTable('entrytag', array(
+			'id_tag', 'id_entry',
 		));
 		));
 	}
 	}
 
 
@@ -97,28 +129,39 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 
 
 	public function optimize() {
 	public function optimize() {
 		$ok = true;
 		$ok = true;
-
-		$sql = 'OPTIMIZE TABLE `' . $this->prefix . 'entry`';	//MySQL
-		$stm = $this->bd->prepare($sql);
-		$ok &= $stm != false;
-		if ($stm) {
-			$ok &= $stm->execute();
-		}
-
-		$sql = 'OPTIMIZE TABLE `' . $this->prefix . 'feed`';	//MySQL
-		$stm = $this->bd->prepare($sql);
-		$ok &= $stm != false;
-		if ($stm) {
-			$ok &= $stm->execute();
+		$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();
+			}
 		}
 		}
+		return $ok;
+	}
 
 
-		$sql = 'OPTIMIZE TABLE `' . $this->prefix . 'category`';	//MySQL
-		$stm = $this->bd->prepare($sql);
-		$ok &= $stm != false;
-		if ($stm) {
-			$ok &= $stm->execute();
+	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
+				try {
+					$sql = sprintf(SQL_UPDATE_GUID_LATIN1_BIN, $this->prefix);
+					$stm = $this->bd->prepare($sql);
+					$ok = $stm->execute();
+				} catch (Exception $e) {
+					$ok = false;
+					Minz_Log::error('FreshRSS_DatabaseDAO::ensureCaseInsensitiveGuids error: ' . $e->getMessage());
+				}
+			}
 		}
 		}
-
 		return $ok;
 		return $ok;
 	}
 	}
+
+	public function minorDbMaintenance() {
+		$this->ensureCaseInsensitiveGuids();
+	}
 }
 }

+ 17 - 21
app/Models/DatabaseDAOPGSQL.php

@@ -3,7 +3,12 @@
 /**
 /**
  * This class is used to test database is well-constructed.
  * This class is used to test database is well-constructed.
  */
  */
-class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO {
+class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
+
+	//PostgreSQL error codes
+	const UNDEFINED_COLUMN = '42703';
+	const UNDEFINED_TABLE = '42P01';
+
 	public function tablesAreCorrect() {
 	public function tablesAreCorrect() {
 		$db = FreshRSS_Context::$system_conf->db;
 		$db = FreshRSS_Context::$system_conf->db;
 		$dbowner = $db['user'];
 		$dbowner = $db['user'];
@@ -17,6 +22,9 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO {
 			$this->prefix . 'category' => false,
 			$this->prefix . 'category' => false,
 			$this->prefix . 'feed' => false,
 			$this->prefix . 'feed' => false,
 			$this->prefix . 'entry' => false,
 			$this->prefix . 'entry' => false,
+			$this->prefix . 'entrytmp' => false,
+			$this->prefix . 'tag' => false,
+			$this->prefix . 'entrytag' => false,
 		);
 		);
 		foreach ($res as $value) {
 		foreach ($res as $value) {
 			$tables[array_pop($value)] = true;
 			$tables[array_pop($value)] = true;
@@ -53,28 +61,16 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO {
 
 
 	public function optimize() {
 	public function optimize() {
 		$ok = true;
 		$ok = true;
+		$tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag');
 
 
-		$sql = 'VACUUM `' . $this->prefix . 'entry`';
-		$stm = $this->bd->prepare($sql);
-		$ok &= $stm != false;
-		if ($stm) {
-			$ok &= $stm->execute();
-		}
-
-		$sql = 'VACUUM `' . $this->prefix . 'feed`';
-		$stm = $this->bd->prepare($sql);
-		$ok &= $stm != false;
-		if ($stm) {
-			$ok &= $stm->execute();
+		foreach ($tables as $table) {
+			$sql = 'VACUUM `' . $this->prefix . $table . '`';
+			$stm = $this->bd->prepare($sql);
+			$ok &= $stm != false;
+			if ($stm) {
+				$ok &= $stm->execute();
+			}
 		}
 		}
-
-		$sql = 'VACUUM `' . $this->prefix . 'category`';
-		$stm = $this->bd->prepare($sql);
-		$ok &= $stm != false;
-		if ($stm) {
-			$ok &= $stm->execute();
-		}
-
 		return $ok;
 		return $ok;
 	}
 	}
 }
 }

+ 12 - 2
app/Models/DatabaseDAOSQLite.php

@@ -14,6 +14,9 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 			'category' => false,
 			'category' => false,
 			'feed' => false,
 			'feed' => false,
 			'entry' => false,
 			'entry' => false,
+			'entrytmp' => false,
+			'tag' => false,
+			'entrytag' => false,
 		);
 		);
 		foreach ($res as $value) {
 		foreach ($res as $value) {
 			$tables[$value['name']] = true;
 			$tables[$value['name']] = true;
@@ -32,8 +35,15 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 
 
 	public function entryIsCorrect() {
 	public function entryIsCorrect() {
 		return $this->checkTable('entry', array(
 		return $this->checkTable('entry', array(
-			'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'is_read',
-			'is_favorite', 'id_feed', 'tags'
+			'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'lastSeen', 'hash', 'is_read',
+			'is_favorite', 'id_feed', 'tags',
+		));
+	}
+
+	public function entrytmpIsCorrect() {
+		return $this->checkTable('entrytmp', array(
+			'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'lastSeen', 'hash', 'is_read',
+			'is_favorite', 'id_feed', 'tags',
 		));
 		));
 	}
 	}
 
 

+ 32 - 21
app/Models/Entry.php

@@ -10,7 +10,7 @@ class FreshRSS_Entry extends Minz_Model {
 	private $id = 0;
 	private $id = 0;
 	private $guid;
 	private $guid;
 	private $title;
 	private $title;
-	private $author;
+	private $authors;
 	private $content;
 	private $content;
 	private $link;
 	private $link;
 	private $date;
 	private $date;
@@ -21,18 +21,17 @@ class FreshRSS_Entry extends Minz_Model {
 	private $feed;
 	private $feed;
 	private $tags;
 	private $tags;
 
 
-	public function __construct($feedId = '', $guid = '', $title = '', $author = '', $content = '',
+	public function __construct($feedId = '', $guid = '', $title = '', $authors = '', $content = '',
 	                            $link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') {
 	                            $link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') {
 		$this->_title($title);
 		$this->_title($title);
-		$this->_author($author);
+		$this->_authors($authors);
 		$this->_content($content);
 		$this->_content($content);
 		$this->_link($link);
 		$this->_link($link);
 		$this->_date($pubdate);
 		$this->_date($pubdate);
 		$this->_isRead($is_read);
 		$this->_isRead($is_read);
 		$this->_isFavorite($is_favorite);
 		$this->_isFavorite($is_favorite);
 		$this->_feedId($feedId);
 		$this->_feedId($feedId);
-		$tags = mb_strcut($tags, 0, 1023, 'UTF-8');
-		$this->_tags(preg_split('/[\s#]/', $tags));
+		$this->_tags($tags);
 		$this->_guid($guid);
 		$this->_guid($guid);
 	}
 	}
 
 
@@ -46,7 +45,15 @@ class FreshRSS_Entry extends Minz_Model {
 		return $this->title;
 		return $this->title;
 	}
 	}
 	public function author() {
 	public function author() {
-		return $this->author === null ? '' : $this->author;
+		//Deprecated
+		return $this->authors(true);
+	}
+	public function authors($asString = false) {
+		if ($asString) {
+			return $this->authors == null ? '' : ';' . implode('; ', $this->authors);
+		} else {
+			return $this->authors;
+		}
 	}
 	}
 	public function content() {
 	public function content() {
 		return $this->content;
 		return $this->content;
@@ -86,9 +93,9 @@ class FreshRSS_Entry extends Minz_Model {
 			return $this->feedId;
 			return $this->feedId;
 		}
 		}
 	}
 	}
-	public function tags($inString = false) {
-		if ($inString) {
-			return empty($this->tags) ? '' : '#' . implode(' #', $this->tags);
+	public function tags($asString = false) {
+		if ($asString) {
+			return $this->tags == null ? '' : '#' . implode(' #', $this->tags);
 		} else {
 		} else {
 			return $this->tags;
 			return $this->tags;
 		}
 		}
@@ -97,7 +104,7 @@ class FreshRSS_Entry extends Minz_Model {
 	public function hash() {
 	public function hash() {
 		if ($this->hash === null) {
 		if ($this->hash === null) {
 			//Do not include $this->date because it may be automatically generated when lacking
 			//Do not include $this->date because it may be automatically generated when lacking
-			$this->hash = md5($this->link . $this->title . $this->author . $this->content . $this->tags(true));
+			$this->hash = md5($this->link . $this->title . $this->authors(true) . $this->content . $this->tags(true));
 		}
 		}
 		return $this->hash;
 		return $this->hash;
 	}
 	}
@@ -124,11 +131,22 @@ class FreshRSS_Entry extends Minz_Model {
 	}
 	}
 	public function _title($value) {
 	public function _title($value) {
 		$this->hash = null;
 		$this->hash = null;
-		$this->title = mb_strcut($value, 0, 255, 'UTF-8');
+		$this->title = $value;
 	}
 	}
 	public function _author($value) {
 	public function _author($value) {
+		//Deprecated
+		$this->_authors($value);
+	}
+	public function _authors($value) {
 		$this->hash = null;
 		$this->hash = null;
-		$this->author = mb_strcut($value, 0, 255, 'UTF-8');
+		if (!is_array($value)) {
+			if (strpos($value, ';') !== false) {
+				$value = preg_split('/\s*[;]\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
+			} else {
+				$value = preg_split('/\s*[,]\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
+			}
+		}
+		$this->authors = $value;
 	}
 	}
 	public function _content($value) {
 	public function _content($value) {
 		$this->hash = null;
 		$this->hash = null;
@@ -162,15 +180,8 @@ class FreshRSS_Entry extends Minz_Model {
 	public function _tags($value) {
 	public function _tags($value) {
 		$this->hash = null;
 		$this->hash = null;
 		if (!is_array($value)) {
 		if (!is_array($value)) {
-			$value = array($value);
-		}
-
-		foreach ($value as $key => $t) {
-			if (!$t) {
-				unset($value[$key]);
-			}
+			$value = preg_split('/\s*[#,]\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
 		}
 		}
-
 		$this->tags = $value;
 		$this->tags = $value;
 	}
 	}
 
 
@@ -287,7 +298,7 @@ class FreshRSS_Entry extends Minz_Model {
 			'id' => $this->id(),
 			'id' => $this->id(),
 			'guid' => $this->guid(),
 			'guid' => $this->guid(),
 			'title' => $this->title(),
 			'title' => $this->title(),
-			'author' => $this->author(),
+			'author' => $this->authors(true),
 			'content' => $this->content(),
 			'content' => $this->content(),
 			'link' => $this->link(),
 			'link' => $this->link(),
 			'date' => $this->date(true),
 			'date' => $this->date(true),

+ 86 - 17
app/Models/EntryDAO.php

@@ -18,6 +18,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return 'hex(' . $x . ')';
 		return 'hex(' . $x . ')';
 	}
 	}
 
 
+	//TODO: Move the database auto-updates to DatabaseDAO
 	protected function addColumn($name) {
 	protected function addColumn($name) {
 		Minz_Log::warning('FreshRSS_EntryDAO::addColumn: ' . $name);
 		Minz_Log::warning('FreshRSS_EntryDAO::addColumn: ' . $name);
 		$hasTransaction = false;
 		$hasTransaction = false;
@@ -56,6 +57,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 	private $triedUpdateToUtf8mb4 = false;
 	private $triedUpdateToUtf8mb4 = false;
 
 
+	//TODO: Move the database auto-updates to DatabaseDAO
 	protected function updateToUtf8mb4() {
 	protected function updateToUtf8mb4() {
 		if ($this->triedUpdateToUtf8mb4) {
 		if ($this->triedUpdateToUtf8mb4) {
 			return false;
 			return false;
@@ -65,7 +67,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if ($db['type'] === 'mysql') {
 		if ($db['type'] === 'mysql') {
 			include_once(APP_PATH . '/SQL/install.sql.mysql.php');
 			include_once(APP_PATH . '/SQL/install.sql.mysql.php');
 			if (defined('SQL_UPDATE_UTF8MB4')) {
 			if (defined('SQL_UPDATE_UTF8MB4')) {
-				Minz_Log::warning('Updating MySQL to UTF8MB4...');
+				Minz_Log::warning('Updating MySQL to UTF8MB4...');	//v1.5.0
 				$hadTransaction = $this->bd->inTransaction();
 				$hadTransaction = $this->bd->inTransaction();
 				if ($hadTransaction) {
 				if ($hadTransaction) {
 					$this->bd->commit();
 					$this->bd->commit();
@@ -88,6 +90,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return false;
 		return false;
 	}
 	}
 
 
+	//TODO: Move the database auto-updates to DatabaseDAO
 	protected function createEntryTempTable() {
 	protected function createEntryTempTable() {
 		$ok = false;
 		$ok = false;
 		$hadTransaction = $this->bd->inTransaction();
 		$hadTransaction = $this->bd->inTransaction();
@@ -120,22 +123,28 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $ok;
 		return $ok;
 	}
 	}
 
 
+	//TODO: Move the database auto-updates to DatabaseDAO
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
 		if (isset($errorInfo[0])) {
 		if (isset($errorInfo[0])) {
-			if ($errorInfo[0] === '42S22') {	//ER_BAD_FIELD_ERROR
+			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR) {
 				//autoAddColumn
 				//autoAddColumn
 				foreach (array('lastSeen', 'hash') as $column) {
 				foreach (array('lastSeen', 'hash') as $column) {
 					if (stripos($errorInfo[2], $column) !== false) {
 					if (stripos($errorInfo[2], $column) !== false) {
 						return $this->addColumn($column);
 						return $this->addColumn($column);
 					}
 					}
 				}
 				}
-			} elseif ($errorInfo[0] === '42S02' && stripos($errorInfo[2], 'entrytmp') !== false) {	//ER_BAD_TABLE_ERROR
-				return $this->createEntryTempTable();	//v1.7
+			} elseif ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR) {
+				if (stripos($errorInfo[2], 'tag') !== false) {
+					$tagDAO = FreshRSS_Factory::createTagDao();
+					return $tagDAO->createTagTable();	//v1.12.0
+				} elseif (stripos($errorInfo[2], 'entrytmp') !== false) {
+					return $this->createEntryTempTable();	//v1.7.0
+				}
 			}
 			}
 		}
 		}
 		if (isset($errorInfo[1])) {
 		if (isset($errorInfo[1])) {
-			if ($errorInfo[1] == '1366') {	//ER_TRUNCATED_WRONG_VALUE_FOR_FIELD
-				return $this->updateToUtf8mb4();
+			if ($errorInfo[1] == FreshRSS_DatabaseDAO::ER_TRUNCATED_WRONG_VALUE_FOR_FIELD) {
+				return $this->updateToUtf8mb4();	//v1.5.0
 			}
 			}
 		}
 		}
 		return false;
 		return false;
@@ -560,11 +569,52 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $affected;
 		return $affected;
 	}
 	}
 
 
+	/**
+	 * Mark all the articles in a tag as read.
+	 * @param integer $id tag ID, or empty for targetting any tag
+	 * @param integer $idMax max article ID
+	 * @return integer affected rows
+	 */
+	public function markReadTag($id = '', $idMax = 0, $filters = null, $state = 0, $is_read = true) {
+		FreshRSS_UserDAO::touch();
+		if ($idMax == 0) {
+			$idMax = time() . '000000';
+			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 '
+			 . 'SET e.is_read = ? '
+			 . 'WHERE '
+			 . ($id == '' ? '' : 'et.id_tag = ? AND ')
+			 . 'e.is_read <> ? AND e.id <= ?';
+		$values = array($is_read ? 1 : 0);
+		if ($id != '') {
+			$values[] = $id;
+		}
+		$values[] = $is_read ? 1 : 0;
+		$values[] = $idMax;
+
+		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
+
+		$stm = $this->bd->prepare($sql . $search);
+		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::error('SQL error markReadTag: ' . $info[2]);
+			return false;
+		}
+		$affected = $stm->rowCount();
+		if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+			return false;
+		}
+		return $affected;
+	}
+
 	public function cleanOldEntries($id_feed, $date_min, $keep = 15) {	//Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
 	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` '
 		$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
 		     . 'WHERE id_feed=:id_feed AND id<=:id_max '
 		     . 'WHERE id_feed=:id_feed AND id<=:id_max '
 		     . 'AND is_favorite=0 '	//Do not remove favourites
 		     . 'AND is_favorite=0 '	//Do not remove favourites
 		     . 'AND `lastSeen` < (SELECT maxLastSeen FROM (SELECT (MAX(e3.`lastSeen`)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) '	//Do not remove the most newly seen articles, plus a few seconds of tolerance
 		     . 'AND `lastSeen` < (SELECT maxLastSeen FROM (SELECT (MAX(e3.`lastSeen`)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) '	//Do not remove the most newly seen articles, plus a few seconds of tolerance
+		     . 'AND id NOT IN (SELECT id_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'
 		     . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)';	//Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery'
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 
 
@@ -770,24 +820,31 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$joinFeed = false;
 		$joinFeed = false;
 		$values = array();
 		$values = array();
 		switch ($type) {
 		switch ($type) {
-		case 'a':
+		case 'a':	//All PRIORITY_MAIN_STREAM
 			$where .= 'f.priority > ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
 			$where .= 'f.priority > ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
 			break;
 			break;
-		case 's':	//Deprecated: use $state instead
+		case 'A':	//All except PRIORITY_ARCHIVED
+			$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
+			break;
+		case 's':	//Starred. Deprecated: use $state instead
 			$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
 			$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
 			$where .= 'AND e.is_favorite=1 ';
 			$where .= 'AND e.is_favorite=1 ';
 			break;
 			break;
-		case 'c':
+		case 'c':	//Category
 			$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
 			$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
 			$where .= 'AND f.category=? ';
 			$where .= 'AND f.category=? ';
 			$values[] = intval($id);
 			$values[] = intval($id);
 			break;
 			break;
-		case 'f':
+		case 'f':	//Feed
 			$where .= 'e.id_feed=? ';
 			$where .= 'e.id_feed=? ';
 			$values[] = intval($id);
 			$values[] = intval($id);
 			break;
 			break;
-		case 'A':
-			$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
+		case 't':	//Tag
+			$where .= 'et.id_tag=? ';
+			$values[] = intval($id);
+			break;
+		case 'T':	//Any tag
+			$where .= '1=1 ';
 			break;
 			break;
 		default:
 		default:
 			throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
 			throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
@@ -796,8 +853,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state, $order, $firstId, $date_min);
 		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state, $order, $firstId, $date_min);
 
 
 		return array(array_merge($values, $searchValues),
 		return array(array_merge($values, $searchValues),
-			'SELECT e.id FROM `' . $this->prefix . 'entry` e '
+			'SELECT '
+			. ($type === 'T' ? 'DISTINCT ' : '')
+			. 'e.id FROM `' . $this->prefix . 'entry` e '
 			. 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
 			. '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 ' : '')
 			. 'WHERE ' . $where
 			. 'WHERE ' . $where
 			. $search
 			. $search
 			. 'ORDER BY e.id ' . $order
 			. 'ORDER BY e.id ' . $order
@@ -817,13 +877,22 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			. 'ORDER BY e0.id ' . $order;
 			. 'ORDER BY e0.id ' . $order;
 
 
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
-		$stm->execute($values);
-		return $stm;
+		if ($stm && $stm->execute($values)) {
+			return $stm;
+		} else {
+			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::error('SQL error listWhereRaw: ' . $info[2]);
+			return false;
+		}
 	}
 	}
 
 
 	public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
 	public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
 		$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
 		$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
-		return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
+		if ($stm) {
+			return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
+		} else {
+			return false;
+		}
 	}
 	}
 
 
 	public function listByIds($ids, $order = 'DESC') {
 	public function listByIds($ids, $order = 'DESC') {
@@ -923,7 +992,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 		$stm->execute();
 		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
-		return $res[0];
+		return isset($res[0]) ? $res[0] : 0;
 	}
 	}
 	public function countNotRead($minPriority = null) {
 	public function countNotRead($minPriority = null) {
 		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e';
 		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e';

+ 7 - 2
app/Models/EntryDAOPGSQL.php

@@ -12,8 +12,13 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
 
 
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
 		if (isset($errorInfo[0])) {
 		if (isset($errorInfo[0])) {
-			if ($errorInfo[0] === '42P01' && stripos($errorInfo[2], 'entrytmp') !== false) {	//undefined_table
-				return $this->createEntryTempTable();
+			if ($errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_TABLE) {
+				if (stripos($errorInfo[2], 'tag') !== false) {
+					$tagDAO = FreshRSS_Factory::createTagDao();
+					return $tagDAO->createTagTable();	//v1.12.0
+				} elseif (stripos($errorInfo[2], 'entrytmp') !== false) {
+					return $this->createEntryTempTable();	//v1.7.0
+				}
 			}
 			}
 		}
 		}
 		return false;
 		return false;

+ 47 - 1
app/Models/EntryDAOSQLite.php

@@ -7,10 +7,17 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 	}
 	}
 
 
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
+		if ($tableInfo = $this->bd->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->bd->query("SELECT sql FROM sqlite_master where name='entrytmp'")) {
 			$showCreate = $tableInfo->fetchColumn();
 			$showCreate = $tableInfo->fetchColumn();
 			if (stripos($showCreate, 'entrytmp') === false) {
 			if (stripos($showCreate, 'entrytmp') === false) {
-				return $this->createEntryTempTable();
+				return $this->createEntryTempTable();	//v1.7.0
 			}
 			}
 		}
 		}
 		if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) {
 		if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) {
@@ -228,4 +235,43 @@ DROP TABLE IF EXISTS `tmp`;
 		}
 		}
 		return $affected;
 		return $affected;
 	}
 	}
+
+	/**
+	 * Mark all the articles in a tag as read.
+	 * @param integer $id tag ID, or empty for targetting any tag
+	 * @param integer $idMax max article ID
+	 * @return integer affected rows
+	 */
+	public function markReadTag($id = '', $idMax = 0, $filters = null, $state = 0, $is_read = true) {
+		FreshRSS_UserDAO::touch();
+		if ($idMax == 0) {
+			$idMax = time() . '000000';
+			Minz_Log::debug('Calling markReadTag(0) is deprecated!');
+		}
+
+		$sql = 'UPDATE `' . $this->prefix . '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 '
+			 . ($id == '' ? '' : 'WHERE et.id = ?')
+			 . ')';
+		$values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax);
+		if ($id != '') {
+			$values[] = $id;
+		}
+
+		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
+
+		$stm = $this->bd->prepare($sql . $search);
+		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::error('SQL error markReadTag: ' . $info[2]);
+			return false;
+		}
+		$affected = $stm->rowCount();
+		if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+			return false;
+		}
+		return $affected;
+	}
 }
 }

+ 16 - 0
app/Models/Factory.php

@@ -2,6 +2,10 @@
 
 
 class FreshRSS_Factory {
 class FreshRSS_Factory {
 
 
+	public static function createCategoryDao($username = null) {
+		return new FreshRSS_CategoryDAO($username);
+	}
+
 	public static function createFeedDao($username = null) {
 	public static function createFeedDao($username = null) {
 		$conf = Minz_Configuration::get('system');
 		$conf = Minz_Configuration::get('system');
 		switch ($conf->db['type']) {
 		switch ($conf->db['type']) {
@@ -24,6 +28,18 @@ class FreshRSS_Factory {
 		}
 		}
 	}
 	}
 
 
+	public static function createTagDao($username = null) {
+		$conf = Minz_Configuration::get('system');
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_TagDAOSQLite($username);
+			case 'pgsql':
+				return new FreshRSS_TagDAOPGSQL($username);
+			default:
+				return new FreshRSS_TagDAO($username);
+		}
+	}
+
 	public static function createStatsDAO($username = null) {
 	public static function createStatsDAO($username = null) {
 		$conf = Minz_Configuration::get('system');
 		$conf = Minz_Configuration::get('system');
 		switch ($conf->db['type']) {
 		switch ($conf->db['type']) {

+ 27 - 7
app/Models/Feed.php

@@ -286,6 +286,10 @@ class FreshRSS_Feed extends Minz_Model {
 				if (!$loadDetails) {	//Only activates auto-discovery when adding a new feed
 				if (!$loadDetails) {	//Only activates auto-discovery when adding a new feed
 					$feed->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE);
 					$feed->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE);
 				}
 				}
+				if ($this->attributes('clear_cache')) {
+					// Do not use `$simplePie->enable_cache(false);` as it would prevent caching in multiuser context
+					$this->clearCache();
+				}
 				Minz_ExtensionManager::callHook('simplepie_before_init', $feed, $this);
 				Minz_ExtensionManager::callHook('simplepie_before_init', $feed, $this);
 				$mtime = $feed->init();
 				$mtime = $feed->init();
 
 
@@ -345,13 +349,21 @@ class FreshRSS_Feed extends Minz_Model {
 			$link = $item->get_permalink();
 			$link = $item->get_permalink();
 			$date = @strtotime($item->get_date());
 			$date = @strtotime($item->get_date());
 
 
-			// gestion des tags (catégorie == tag)
-			$tags_tmp = $item->get_categories();
+			//Tag processing (tag == category)
+			$categories = $item->get_categories();
 			$tags = array();
 			$tags = array();
-			if ($tags_tmp !== null) {
-				foreach ($tags_tmp as $tag) {
-					$tags[] = html_only_entity_decode($tag->get_label());
+			if (is_array($categories)) {
+				foreach ($categories as $category) {
+					$text = html_only_entity_decode($category->get_label());
+					//Some feeds use a single category with comma-separated tags
+					$labels = explode(',', $text);
+					if (is_array($labels)) {
+						foreach ($labels as $label) {
+							$tags[] = trim($label);
+						}
+					}
 				}
 				}
+				$tags = array_unique($tags);
 			}
 			}
 
 
 			$content = html_only_entity_decode($item->get_content());
 			$content = html_only_entity_decode($item->get_content());
@@ -412,7 +424,7 @@ class FreshRSS_Feed extends Minz_Model {
 			$author_names = '';
 			$author_names = '';
 			if (is_array($authors)) {
 			if (is_array($authors)) {
 				foreach ($authors as $author) {
 				foreach ($authors as $author) {
-					$author_names .= html_only_entity_decode(strip_tags($author->name == '' ? $author->email : $author->name)) . ', ';
+					$author_names .= html_only_entity_decode(strip_tags($author->name == '' ? $author->email : $author->name)) . '; ';
 				}
 				}
 			}
 			}
 			$author_names = substr($author_names, 0, -2);
 			$author_names = substr($author_names, 0, -2);
@@ -457,8 +469,16 @@ class FreshRSS_Feed extends Minz_Model {
 		$this->entries = $entries;
 		$this->entries = $entries;
 	}
 	}
 
 
+	protected function cacheFilename() {
+		return CACHE_PATH . '/' . md5($this->url) . '.spc';
+	}
+
+	public function clearCache() {
+		return @unlink($this->cacheFilename());
+	}
+
 	public function cacheModifiedTime() {
 	public function cacheModifiedTime() {
-		return @filemtime(CACHE_PATH . '/' . md5($this->url) . '.spc');
+		return @filemtime($this->cacheFilename());
 	}
 	}
 
 
 	public function lock() {
 	public function lock() {

+ 6 - 3
app/Models/FeedDAO.php

@@ -17,7 +17,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
 		if (isset($errorInfo[0])) {
 		if (isset($errorInfo[0])) {
-			if ($errorInfo[0] === '42S22' || $errorInfo[0] === '42703') {	//ER_BAD_FIELD_ERROR (Mysql), undefined_column (PostgreSQL)
+			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
 				foreach (array('attributes') as $column) {
 				foreach (array('attributes') as $column) {
 					if (stripos($errorInfo[2], $column) !== false) {
 					if (stripos($errorInfo[2], $column) !== false) {
 						return $this->addColumn($column);
 						return $this->addColumn($column);
@@ -55,7 +55,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$values = array(
 		$values = array(
 			substr($valuesTmp['url'], 0, 511),
 			substr($valuesTmp['url'], 0, 511),
 			$valuesTmp['category'],
 			$valuesTmp['category'],
-			mb_strcut($valuesTmp['name'], 0, 255, 'UTF-8'),
+			mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'),
 			substr($valuesTmp['website'], 0, 255),
 			substr($valuesTmp['website'], 0, 255),
 			mb_strcut($valuesTmp['description'], 0, 1023, 'UTF-8'),
 			mb_strcut($valuesTmp['description'], 0, 1023, 'UTF-8'),
 			$valuesTmp['lastUpdate'],
 			$valuesTmp['lastUpdate'],
@@ -109,6 +109,9 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function updateFeed($id, $valuesTmp) {
 	public function updateFeed($id, $valuesTmp) {
+		if (isset($valuesTmp['name'])) {
+			$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
+		}
 		if (isset($valuesTmp['url'])) {
 		if (isset($valuesTmp['url'])) {
 			$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
 			$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
 		}
 		}
@@ -180,7 +183,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 	}
 
 
 	public function changeCategory($idOldCat, $idNewCat) {
 	public function changeCategory($idOldCat, $idNewCat) {
-		$catDAO = new FreshRSS_CategoryDAO();
+		$catDAO = FreshRSS_Factory::createCategoryDao();
 		$newCat = $catDAO->searchById($idNewCat);
 		$newCat = $catDAO->searchById($idNewCat);
 		if (!$newCat) {
 		if (!$newCat) {
 			$newCat = $catDAO->getDefault();
 			$newCat = $catDAO->getDefault();

+ 29 - 8
app/Models/Search.php

@@ -40,7 +40,7 @@ class FreshRSS_Search {
 		$input = $this->parseNotIntitleSearch($input);
 		$input = $this->parseNotIntitleSearch($input);
 		$input = $this->parseNotAuthorSearch($input);
 		$input = $this->parseNotAuthorSearch($input);
 		$input = $this->parseNotInurlSearch($input);
 		$input = $this->parseNotInurlSearch($input);
-		$input = $this->parseNotTagsSeach($input);
+		$input = $this->parseNotTagsSearch($input);
 
 
 		$input = $this->parsePubdateSearch($input);
 		$input = $this->parsePubdateSearch($input);
 		$input = $this->parseDateSearch($input);
 		$input = $this->parseDateSearch($input);
@@ -48,7 +48,7 @@ class FreshRSS_Search {
 		$input = $this->parseIntitleSearch($input);
 		$input = $this->parseIntitleSearch($input);
 		$input = $this->parseAuthorSearch($input);
 		$input = $this->parseAuthorSearch($input);
 		$input = $this->parseInurlSearch($input);
 		$input = $this->parseInurlSearch($input);
-		$input = $this->parseTagsSeach($input);
+		$input = $this->parseTagsSearch($input);
 
 
 		$input = $this->parseNotSearch($input);
 		$input = $this->parseNotSearch($input);
 		$input = $this->parseSearch($input);
 		$input = $this->parseSearch($input);
@@ -117,6 +117,17 @@ class FreshRSS_Search {
 		return is_array($anArray) ? array_filter($anArray, function($value) { return $value !== ''; }) : array();
 		return is_array($anArray) ? array_filter($anArray, function($value) { return $value !== ''; }) : array();
 	}
 	}
 
 
+	private static function decodeSpaces($value) {
+		if (is_array($value)) {
+			for ($i = count($value) - 1; $i >= 0; $i--) {
+				$value[$i] = self::decodeSpaces($value[$i]);
+			}
+		} else {
+			$value = trim(str_replace('+', ' ', $value));
+		}
+		return $value;
+	}
+
 	/**
 	/**
 	 * Parse the search string to find intitle keyword and the search related
 	 * Parse the search string to find intitle keyword and the search related
 	 * to it.
 	 * to it.
@@ -130,11 +141,12 @@ class FreshRSS_Search {
 			$this->intitle = $matches['search'];
 			$this->intitle = $matches['search'];
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
-		if (preg_match_all('/\bintitle:(?P<search>\w*)/', $input, $matches)) {
+		if (preg_match_all('/\bintitle:(?P<search>[^\s"]*)/', $input, $matches)) {
 			$this->intitle = array_merge($this->intitle ? $this->intitle : array(), $matches['search']);
 			$this->intitle = array_merge($this->intitle ? $this->intitle : array(), $matches['search']);
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
 		$this->intitle = self::removeEmptyValues($this->intitle);
 		$this->intitle = self::removeEmptyValues($this->intitle);
+		$this->intitle = self::decodeSpaces($this->intitle);
 		return $input;
 		return $input;
 	}
 	}
 
 
@@ -143,11 +155,12 @@ class FreshRSS_Search {
 			$this->not_intitle = $matches['search'];
 			$this->not_intitle = $matches['search'];
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
-		if (preg_match_all('/[!-]intitle:(?P<search>\w*)/', $input, $matches)) {
+		if (preg_match_all('/[!-]intitle:(?P<search>[^\s"]*)/', $input, $matches)) {
 			$this->not_intitle = array_merge($this->not_intitle ? $this->not_intitle : array(), $matches['search']);
 			$this->not_intitle = array_merge($this->not_intitle ? $this->not_intitle : array(), $matches['search']);
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
 		$this->not_intitle = self::removeEmptyValues($this->not_intitle);
 		$this->not_intitle = self::removeEmptyValues($this->not_intitle);
+		$this->not_intitle = self::decodeSpaces($this->not_intitle);
 		return $input;
 		return $input;
 	}
 	}
 
 
@@ -166,11 +179,12 @@ class FreshRSS_Search {
 			$this->author = $matches['search'];
 			$this->author = $matches['search'];
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
-		if (preg_match_all('/\bauthor:(?P<search>\w*)/', $input, $matches)) {
+		if (preg_match_all('/\bauthor:(?P<search>[^\s"]*)/', $input, $matches)) {
 			$this->author = array_merge($this->author ? $this->author : array(), $matches['search']);
 			$this->author = array_merge($this->author ? $this->author : array(), $matches['search']);
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
 		$this->author = self::removeEmptyValues($this->author);
 		$this->author = self::removeEmptyValues($this->author);
+		$this->author = self::decodeSpaces($this->author);
 		return $input;
 		return $input;
 	}
 	}
 
 
@@ -179,11 +193,12 @@ class FreshRSS_Search {
 			$this->not_author = $matches['search'];
 			$this->not_author = $matches['search'];
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
-		if (preg_match_all('/[!-]author:(?P<search>\w*)/', $input, $matches)) {
+		if (preg_match_all('/[!-]author:(?P<search>[^\s"]*)/', $input, $matches)) {
 			$this->not_author = array_merge($this->not_author ? $this->not_author : array(), $matches['search']);
 			$this->not_author = array_merge($this->not_author ? $this->not_author : array(), $matches['search']);
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
 		$this->not_author = self::removeEmptyValues($this->not_author);
 		$this->not_author = self::removeEmptyValues($this->not_author);
+		$this->not_author = self::decodeSpaces($this->not_author);
 		return $input;
 		return $input;
 	}
 	}
 
 
@@ -201,6 +216,7 @@ class FreshRSS_Search {
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
 		$this->inurl = self::removeEmptyValues($this->inurl);
 		$this->inurl = self::removeEmptyValues($this->inurl);
+		$this->inurl = self::decodeSpaces($this->inurl);
 		return $input;
 		return $input;
 	}
 	}
 
 
@@ -210,6 +226,7 @@ class FreshRSS_Search {
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
 		$this->not_inurl = self::removeEmptyValues($this->not_inurl);
 		$this->not_inurl = self::removeEmptyValues($this->not_inurl);
+		$this->not_inurl = self::decodeSpaces($this->not_inurl);
 		return $input;
 		return $input;
 	}
 	}
 
 
@@ -259,21 +276,23 @@ class FreshRSS_Search {
 	 * @param string $input
 	 * @param string $input
 	 * @return string
 	 * @return string
 	 */
 	 */
-	private function parseTagsSeach($input) {
+	private function parseTagsSearch($input) {
 		if (preg_match_all('/#(?P<search>[^\s]+)/', $input, $matches)) {
 		if (preg_match_all('/#(?P<search>[^\s]+)/', $input, $matches)) {
 			$this->tags = $matches['search'];
 			$this->tags = $matches['search'];
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
 		$this->tags = self::removeEmptyValues($this->tags);
 		$this->tags = self::removeEmptyValues($this->tags);
+		$this->tags = self::decodeSpaces($this->tags);
 		return $input;
 		return $input;
 	}
 	}
 
 
-	private function parseNotTagsSeach($input) {
+	private function parseNotTagsSearch($input) {
 		if (preg_match_all('/[!-]#(?P<search>[^\s]+)/', $input, $matches)) {
 		if (preg_match_all('/[!-]#(?P<search>[^\s]+)/', $input, $matches)) {
 			$this->not_tags = $matches['search'];
 			$this->not_tags = $matches['search'];
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
 		$this->not_tags = self::removeEmptyValues($this->not_tags);
 		$this->not_tags = self::removeEmptyValues($this->not_tags);
+		$this->not_tags = self::decodeSpaces($this->not_tags);
 		return $input;
 		return $input;
 	}
 	}
 
 
@@ -303,6 +322,7 @@ class FreshRSS_Search {
 		} else {
 		} else {
 			$this->search = explode(' ', $input);
 			$this->search = explode(' ', $input);
 		}
 		}
+		$this->search = self::decodeSpaces($this->search);
 	}
 	}
 
 
 	private function parseNotSearch($input) {
 	private function parseNotSearch($input) {
@@ -322,6 +342,7 @@ class FreshRSS_Search {
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
 		$this->not_search = self::removeEmptyValues($this->not_search);
 		$this->not_search = self::removeEmptyValues($this->not_search);
+		$this->not_search = self::decodeSpaces($this->not_search);
 		return $input;
 		return $input;
 	}
 	}
 
 

+ 76 - 0
app/Models/Tag.php

@@ -0,0 +1,76 @@
+<?php
+
+class FreshRSS_Tag extends Minz_Model {
+	private $id = 0;
+	private $name;
+	private $attributes = array();
+	private $nbEntries = -1;
+	private $nbUnread = -1;
+
+	public function __construct($name = '') {
+		$this->_name($name);
+	}
+
+	public function id() {
+		return $this->id;
+	}
+
+	public function _id($value) {
+		$this->id = (int)$value;
+	}
+
+	public function name() {
+		return $this->name;
+	}
+
+	public function _name($value) {
+		$this->name = trim($value);
+	}
+
+	public function attributes($key = '') {
+		if ($key == '') {
+			return $this->attributes;
+		} else {
+			return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
+		}
+	}
+
+	public function _attributes($key, $value) {
+		if ($key == '') {
+			if (is_string($value)) {
+				$value = json_decode($value, true);
+			}
+			if (is_array($value)) {
+				$this->attributes = $value;
+			}
+		} elseif ($value === null) {
+			unset($this->attributes[$key]);
+		} else {
+			$this->attributes[$key] = $value;
+		}
+	}
+
+	public function nbEntries() {
+		if ($this->nbEntries < 0) {
+			$tagDAO = FreshRSS_Factory::createTagDao();
+			$this->nbEntries = $tagDAO->countEntries($this->id());
+		}
+		return $this->nbFeed;
+	}
+
+	public function _nbEntries($value) {
+		$this->nbEntries = (int)$value;
+	}
+
+	public function nbUnread() {
+		if ($this->nbUnread < 0) {
+			$tagDAO = FreshRSS_Factory::createTagDao();
+			$this->nbUnread = $tagDAO->countNotRead($this->id());
+		}
+		return $this->nbUnread;
+	}
+
+	public function _nbUnread($value) {
+		$this->nbUnread = (int)$value;
+	}
+}

+ 315 - 0
app/Models/TagDAO.php

@@ -0,0 +1,315 @@
+<?php
+
+class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
+
+	public function sqlIgnore() {
+		return 'IGNORE';
+	}
+
+	public function createTagTable() {
+		$ok = false;
+		$hadTransaction = $this->bd->inTransaction();
+		if ($hadTransaction) {
+			$this->bd->commit();
+		}
+		try {
+			$db = FreshRSS_Context::$system_conf->db;
+			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.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();
+				}
+			}
+		} catch (Exception $e) {
+			Minz_Log::error('FreshRSS_EntryDAO::createTagTable error: ' . $e->getMessage());
+		}
+		if ($hadTransaction) {
+			$this->bd->beginTransaction();
+		}
+		return $ok;
+	}
+
+	protected function autoUpdateDb($errorInfo) {
+		if (isset($errorInfo[0])) {
+			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_TABLE) {
+				if (stripos($errorInfo[2], 'tag') !== false) {
+					return $this->createTagTable();	//v1.12.0
+				}
+			}
+		}
+		return false;
+	}
+
+	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);
+
+		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8');
+		$values = array(
+			$valuesTmp['name'],
+			isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
+			$valuesTmp['name'],
+		);
+
+		if ($stm && $stm->execute($values)) {
+			return $this->bd->lastInsertId('"' . $this->prefix . 'tag_id_seq"');
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::error('SQL error addTag: ' . $info[2]);
+			return false;
+		}
+	}
+
+	public function addTagObject($tag) {
+		$tag = $this->searchByName($tag->name());
+		if (!$tag) {
+			$values = array(
+				'name' => $tag->name(),
+				'attributes' => $tag->attributes(),
+			);
+			return $this->addTag($values);
+		}
+		return $tag->id();
+	}
+
+	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);
+
+		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8');
+		$values = array(
+			$valuesTmp['name'],
+			isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
+			$id,
+			$valuesTmp['name'],
+		);
+
+		if ($stm && $stm->execute($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::error('SQL error updateTag: ' . $info[2]);
+			return false;
+		}
+	}
+
+	public function updateTagAttribute($tag, $key, $value) {
+		if ($tag instanceof FreshRSS_Tag) {
+			$tag->_attributes($key, $value);
+			return $this->updateFeed(
+					$tag->id(),
+					array('attributes' => $feed->attributes())
+				);
+		}
+		return false;
+	}
+
+	public function deleteTag($id) {
+		if ($id <= 0) {
+			return false;
+		}
+		$sql = 'DELETE FROM `' . $this->prefix . 'tag` WHERE id=?';
+		$stm = $this->bd->prepare($sql);
+
+		$values = array($id);
+
+		if ($stm && $stm->execute($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::error('SQL error deleteTag: ' . $info[2]);
+			return false;
+		}
+	}
+
+	public function searchById($id) {
+		$sql = 'SELECT * FROM `' . $this->prefix . 'tag` WHERE id=?';
+		$stm = $this->bd->prepare($sql);
+		$values = array($id);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$tag = self::daoToTag($res);
+		return isset($tag[0]) ? $tag[0] : null;
+	}
+
+	public function searchByName($name) {
+		$sql = 'SELECT * FROM `' . $this->prefix . 'tag` WHERE name=?';
+		$stm = $this->bd->prepare($sql);
+		$values = array($name);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$tag = self::daoToTag($res);
+		return isset($tag[0]) ? $tag[0] : null;
+	}
+
+	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 '
+				 . 'GROUP BY t.id '
+				 . 'ORDER BY t.name';
+		} else {
+			$sql = 'SELECT * FROM `' . $this->prefix . 'tag` ORDER BY name';
+		}
+
+		$stm = $this->bd->prepare($sql);
+		if ($stm && $stm->execute()) {
+			return self::daoToTag($stm->fetchAll(PDO::FETCH_ASSOC));
+		} else {
+			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			if ($this->autoUpdateDb($info)) {
+				return $this->listTags($precounts);
+			}
+			Minz_Log::error('SQL error listTags: ' . $info[2]);
+			return false;
+		}
+	}
+
+	public function count() {
+		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'tag`';
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		return $res[0]['count'];
+	}
+
+	public function countEntries($id) {
+		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entrytag` WHERE id_tag=?';
+		$stm = $this->bd->prepare($sql);
+		$values = array($id);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		return $res[0]['count'];
+	}
+
+	public function countNotRead($id) {
+		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entrytag` et '
+			 . 'INNER JOIN `' . $this->prefix . 'entry` e ON et.id_entry=e.id '
+			 . 'WHERE et.id_tag=? AND e.is_read=0';
+		$stm = $this->bd->prepare($sql);
+		$values = array($id);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		return $res[0]['count'];
+	}
+
+	public function tagEntry($id_tag, $id_entry, $checked = true) {
+		if ($checked) {
+			$sql = 'INSERT ' . $this->sqlIgnore() . ' INTO `' . $this->prefix . 'entrytag`(id_tag, id_entry) VALUES(?, ?)';
+		} else {
+			$sql = 'DELETE FROM `' . $this->prefix . 'entrytag` WHERE id_tag=? AND id_entry=?';
+		}
+		$stm = $this->bd->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();
+			Minz_Log::error('SQL error tagEntry: ' . $info[2]);
+			return false;
+		}
+	}
+
+	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=? '
+			 . 'ORDER BY t.name';
+
+		$stm = $this->bd->prepare($sql);
+		$values = array($id_entry);
+
+		if ($stm && $stm->execute($values)) {
+			$lines = $stm->fetchAll(PDO::FETCH_ASSOC);
+			for ($i = count($lines) - 1; $i >= 0; $i--) {
+				$lines[$i]['id'] = intval($lines[$i]['id']);
+				$lines[$i]['checked'] = !empty($lines[$i]['checked']);
+			}
+			return $lines;
+		} else {
+			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			if ($this->autoUpdateDb($info)) {
+				return $this->getTagsForEntry($id_entry);
+			}
+			Minz_Log::error('SQL error getTagsForEntry: ' . $info[2]);
+			return false;
+		}
+	}
+
+	//For API
+	public function getEntryIdsTagNames($entries) {
+		$sql = 'SELECT et.id_entry, t.name '
+			 . 'FROM `' . $this->prefix . 'tag` t '
+			 . 'INNER JOIN `' . $this->prefix . 'entrytag` et ON et.id_tag = t.id';
+
+		$values = array();
+		if (is_array($entries) && count($entries) > 0) {
+			$sql .= ' AND et.id_entry IN (' . str_repeat('?,', count($entries) - 1). '?)';
+			foreach ($entries as $entry) {
+				$values[] = $entry->id();
+			}
+		}
+		$stm = $this->bd->prepare($sql);
+
+		if ($stm && $stm->execute($values)) {
+			$result = array();
+			foreach ($stm->fetchAll(PDO::FETCH_ASSOC) as $line) {
+				$entryId = 'e_' . $line['id_entry'];
+				$tagName = $line['name'];
+				if (empty($result[$entryId])) {
+					$result[$entryId] = array();
+				}
+				$result[$entryId][] = $tagName;
+			}
+			return $result;
+		} else {
+			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			if ($this->autoUpdateDb($info)) {
+				return $this->getTagNamesEntryIds($id_entry);
+			}
+			Minz_Log::error('SQL error getTagNamesEntryIds: ' . $info[2]);
+			return false;
+		}
+	}
+
+	public static function daoToTag($listDAO) {
+		$list = array();
+		if (!is_array($listDAO)) {
+			$listDAO = array($listDAO);
+		}
+		foreach ($listDAO as $key => $dao) {
+			$tag = new FreshRSS_Tag(
+				$dao['name']
+			);
+			$tag->_id($dao['id']);
+			if (!empty($dao['attributes'])) {
+				$tag->_attributes('', $dao['attributes']);
+			}
+			if (isset($dao['unreads'])) {
+				$tag->_nbUnread($dao['unreads']);
+			}
+			$list[$key] = $tag;
+		}
+		return $list;
+	}
+}

+ 9 - 0
app/Models/TagDAOPGSQL.php

@@ -0,0 +1,9 @@
+<?php
+
+class FreshRSS_TagDAOPGSQL extends FreshRSS_TagDAO {
+
+	public function sqlIgnore() {
+		return '';	//TODO
+	}
+
+}

+ 19 - 0
app/Models/TagDAOSQLite.php

@@ -0,0 +1,19 @@
+<?php
+
+class FreshRSS_TagDAOSQLite extends FreshRSS_TagDAO {
+
+	public function sqlIgnore() {
+		return 'OR IGNORE';
+	}
+
+	protected function autoUpdateDb($errorInfo) {
+		if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='tag'")) {
+			$showCreate = $tableInfo->fetchColumn();
+			if (stripos($showCreate, 'tag') === false) {
+				return $this->createTagTable();	//v1.12.0
+			}
+		}
+		return false;
+	}
+
+}

+ 9 - 3
app/Models/Themes.php

@@ -68,7 +68,7 @@ class FreshRSS_Themes extends Minz_Model {
 		return $infos;
 		return $infos;
 	}
 	}
 
 
-	public static function icon($name, $urlOnly = false) {
+	public static function alt($name) {
 		static $alts = array(
 		static $alts = array(
 			'add' => '✚',
 			'add' => '✚',
 			'all' => '☰',
 			'all' => '☰',
@@ -84,6 +84,7 @@ class FreshRSS_Themes extends Minz_Model {
 			'icon' => '⊚',
 			'icon' => '⊚',
 			'import' => '⤓',
 			'import' => '⤓',
 			'key' => '⚿',
 			'key' => '⚿',
+			'label' => '🏷️',
 			'link' => '↗',
 			'link' => '↗',
 			'login' => '🔒',
 			'login' => '🔒',
 			'logout' => '🔓',
 			'logout' => '🔓',
@@ -104,13 +105,18 @@ class FreshRSS_Themes extends Minz_Model {
 			'view-global' => '☷',
 			'view-global' => '☷',
 			'view-reader' => '☕',
 			'view-reader' => '☕',
 		);
 		);
-		if (!isset($alts[$name])) {
+		return isset($name) ? $alts[$name] : '';
+	}
+
+	public static function icon($name, $urlOnly = false, $altOnly = false) {
+		$alt = self::alt($name);
+		if ($alt == '') {
 			return '';
 			return '';
 		}
 		}
 
 
 		$url = $name . '.svg';
 		$url = $name . '.svg';
 		$url = isset(self::$themeIcons[$url]) ? (self::$themeIconsUrl . $url) : (self::$defaultIconsUrl . $url);
 		$url = isset(self::$themeIcons[$url]) ? (self::$themeIconsUrl . $url) : (self::$defaultIconsUrl . $url);
 
 
-		return $urlOnly ? Minz_Url::display($url) : '<img class="icon" src="' . Minz_Url::display($url) . '" alt="' . $alts[$name] . '" />';
+		return $urlOnly ? Minz_Url::display($url) : '<img class="icon" src="' . Minz_Url::display($url) . '" alt="' . $alt . '" />';
 	}
 	}
 }
 }

+ 3 - 4
app/Models/UserDAO.php

@@ -14,14 +14,13 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 			$ok = false;
 			$ok = false;
 			$bd_prefix_user = $db['prefix'] . $username . '_';
 			$bd_prefix_user = $db['prefix'] . $username . '_';
 			if (defined('SQL_CREATE_TABLES')) {	//E.g. MySQL
 			if (defined('SQL_CREATE_TABLES')) {	//E.g. MySQL
-				$sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP, $bd_prefix_user, _t('gen.short.default_category'));
+				$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);
 				$stm = $userPDO->bd->prepare($sql);
 				$ok = $stm && $stm->execute();
 				$ok = $stm && $stm->execute();
 			} else {	//E.g. SQLite
 			} else {	//E.g. SQLite
-				global $SQL_CREATE_TABLES;
-				global $SQL_CREATE_TABLE_ENTRYTMP;
+				global $SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS;
 				if (is_array($SQL_CREATE_TABLES)) {
 				if (is_array($SQL_CREATE_TABLES)) {
-					$instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP);
+					$instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS);
 					$ok = !empty($instructions);
 					$ok = !empty($instructions);
 					foreach ($instructions as $instruction) {
 					foreach ($instructions as $instruction) {
 						$sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category'));
 						$sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category'));

+ 25 - 1
app/Models/UserQuery.php

@@ -19,15 +19,17 @@ class FreshRSS_UserQuery {
 	private $url;
 	private $url;
 	private $feed_dao;
 	private $feed_dao;
 	private $category_dao;
 	private $category_dao;
+	private $tag_dao;
 
 
 	/**
 	/**
 	 * @param array $query
 	 * @param array $query
 	 * @param FreshRSS_Searchable $feed_dao
 	 * @param FreshRSS_Searchable $feed_dao
 	 * @param FreshRSS_Searchable $category_dao
 	 * @param FreshRSS_Searchable $category_dao
 	 */
 	 */
-	public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null) {
+	public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null, FreshRSS_Searchable $tag_dao = null) {
 		$this->category_dao = $category_dao;
 		$this->category_dao = $category_dao;
 		$this->feed_dao = $feed_dao;
 		$this->feed_dao = $feed_dao;
+		$this->tag_dao = $tag_dao;
 		if (isset($query['get'])) {
 		if (isset($query['get'])) {
 			$this->parseGet($query['get']);
 			$this->parseGet($query['get']);
 		}
 		}
@@ -88,6 +90,9 @@ class FreshRSS_UserQuery {
 				case 's':
 				case 's':
 					$this->parseFavorite();
 					$this->parseFavorite();
 					break;
 					break;
+				case 't':
+					$this->parseTag($matches['id']);
+					break;
 			}
 			}
 		}
 		}
 	}
 	}
@@ -138,6 +143,25 @@ class FreshRSS_UserQuery {
 		$this->get_type = 'feed';
 		$this->get_type = 'feed';
 	}
 	}
 
 
+	/**
+	 * Parse the query string when it is a "tag" query
+	 *
+	 * @param integer $id
+	 * @throws FreshRSS_DAO_Exception
+	 */
+	private function parseTag($id) {
+		if ($this->tag_dao == null) {
+			throw new FreshRSS_DAO_Exception('Tag DAO is not loaded in UserQuery');
+		}
+		$category = $this->category_dao->searchById($id);
+		if ($tag) {
+			$this->get_name = $tag->name();
+		} else {
+			$this->deprecated = true;
+		}
+		$this->get_type = 'tag';
+	}
+
 	/**
 	/**
 	 * Parse the query string when it is a "favorite" query
 	 * Parse the query string when it is a "favorite" query
 	 */
 	 */

+ 67 - 41
app/SQL/install.sql.mysql.php

@@ -1,10 +1,10 @@
 <?php
 <?php
-define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS `%1$s` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
 
 
 define('SQL_CREATE_TABLES', '
 define('SQL_CREATE_TABLES', '
 CREATE TABLE IF NOT EXISTS `%1$scategory` (
 CREATE TABLE IF NOT EXISTS `%1$scategory` (
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
-	`name` varchar(191) NOT NULL,
+	`name` VARCHAR(' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') NOT NULL,	-- Max index length for Unicode is 191 characters (767 bytes)
 	PRIMARY KEY (`id`),
 	PRIMARY KEY (`id`),
 	UNIQUE KEY (`name`)	-- v0.7
 	UNIQUE KEY (`name`)	-- v0.7
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
@@ -12,21 +12,21 @@ ENGINE = INNODB;
 
 
 CREATE TABLE IF NOT EXISTS `%1$sfeed` (
 CREATE TABLE IF NOT EXISTS `%1$sfeed` (
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
-	`url` varchar(511) CHARACTER SET latin1 NOT NULL,
+	`url` VARCHAR(511) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
 	`category` SMALLINT DEFAULT 0,	-- v0.7
 	`category` SMALLINT DEFAULT 0,	-- v0.7
-	`name` varchar(191) NOT NULL,
-	`website` varchar(255) CHARACTER SET latin1,
-	`description` text,
-	`lastUpdate` int(11) DEFAULT 0,	-- Until year 2038
-	`priority` tinyint(2) NOT NULL DEFAULT 10,
-	`pathEntries` varchar(511) DEFAULT NULL,
-	`httpAuth` varchar(511) DEFAULT NULL,
-	`error` boolean DEFAULT 0,
+	`name` VARCHAR(' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') NOT NULL,
+	`website` VARCHAR(255) CHARACTER SET latin1 COLLATE latin1_bin,
+	`description` TEXT,
+	`lastUpdate` INT(11) DEFAULT 0,	-- Until year 2038
+	`priority` TINYINT(2) NOT NULL DEFAULT 10,
+	`pathEntries` VARCHAR(511) DEFAULT NULL,
+	`httpAuth` VARCHAR(511) DEFAULT NULL,
+	`error` BOOLEAN DEFAULT 0,
 	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,	-- v0.7
 	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,	-- v0.7
 	`ttl` INT NOT NULL DEFAULT 0,	-- v0.7.3
 	`ttl` INT NOT NULL DEFAULT 0,	-- v0.7.3
 	`attributes` TEXT,	-- v1.11.0
 	`attributes` TEXT,	-- v1.11.0
-	`cache_nbEntries` int DEFAULT 0,	-- v0.7
-	`cache_nbUnreads` int DEFAULT 0,	-- v0.7
+	`cache_nbEntries` INT DEFAULT 0,	-- v0.7
+	`cache_nbUnreads` INT DEFAULT 0,	-- v0.7
 	PRIMARY KEY (`id`),
 	PRIMARY KEY (`id`),
 	FOREIGN KEY (`category`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
 	FOREIGN KEY (`category`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
 	UNIQUE KEY (`url`),	-- v0.7
 	UNIQUE KEY (`url`),	-- v0.7
@@ -37,19 +37,19 @@ CREATE TABLE IF NOT EXISTS `%1$sfeed` (
 ENGINE = INNODB;
 ENGINE = INNODB;
 
 
 CREATE TABLE IF NOT EXISTS `%1$sentry` (
 CREATE TABLE IF NOT EXISTS `%1$sentry` (
-	`id` bigint NOT NULL,	-- v0.7
-	`guid` varchar(760) CHARACTER SET latin1 NOT NULL,	-- Maximum for UNIQUE is 767B
-	`title` varchar(255) NOT NULL,
-	`author` varchar(255),
-	`content_bin` blob,	-- v0.7
-	`link` varchar(1023) CHARACTER SET latin1 NOT NULL,
-	`date` int(11),	-- Until year 2038
+	`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
+	`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
 	`lastSeen` INT(11) DEFAULT 0,	-- v1.1.1, Until year 2038
 	`hash` BINARY(16),	-- v1.1.1
 	`hash` BINARY(16),	-- v1.1.1
-	`is_read` boolean NOT NULL DEFAULT 0,
-	`is_favorite` boolean NOT NULL DEFAULT 0,
+	`is_read` BOOLEAN NOT NULL DEFAULT 0,
+	`is_favorite` BOOLEAN NOT NULL DEFAULT 0,
 	`id_feed` SMALLINT,	-- v0.7
 	`id_feed` SMALLINT,	-- v0.7
-	`tags` varchar(1023),
+	`tags` VARCHAR(1023),
 	PRIMARY KEY (`id`),
 	PRIMARY KEY (`id`),
 	FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	UNIQUE KEY (`id_feed`,`guid`),	-- v0.7
 	UNIQUE KEY (`id_feed`,`guid`),	-- v0.7
@@ -65,19 +65,19 @@ INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
 
 
 define('SQL_CREATE_TABLE_ENTRYTMP', '
 define('SQL_CREATE_TABLE_ENTRYTMP', '
 CREATE TABLE IF NOT EXISTS `%1$sentrytmp` (	-- v1.7
 CREATE TABLE IF NOT EXISTS `%1$sentrytmp` (	-- v1.7
-	`id` bigint NOT NULL,
-	`guid` varchar(760) CHARACTER SET latin1 NOT NULL,
-	`title` varchar(255) NOT NULL,
-	`author` varchar(255),
-	`content_bin` blob,
-	`link` varchar(1023) CHARACTER SET latin1 NOT NULL,
-	`date` int(11),
+	`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,
+	`link` VARCHAR(1023) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
+	`date` INT(11),
 	`lastSeen` INT(11) DEFAULT 0,
 	`lastSeen` INT(11) DEFAULT 0,
 	`hash` BINARY(16),
 	`hash` BINARY(16),
-	`is_read` boolean NOT NULL DEFAULT 0,
-	`is_favorite` boolean NOT NULL DEFAULT 0,
+	`is_read` BOOLEAN NOT NULL DEFAULT 0,
+	`is_favorite` BOOLEAN NOT NULL DEFAULT 0,
 	`id_feed` SMALLINT,
 	`id_feed` SMALLINT,
-	`tags` varchar(1023),
+	`tags` VARCHAR(1023),
 	PRIMARY KEY (`id`),
 	PRIMARY KEY (`id`),
 	FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	UNIQUE KEY (`id_feed`,`guid`),
 	UNIQUE KEY (`id_feed`,`guid`),
@@ -88,25 +88,46 @@ ENGINE = INNODB;
 CREATE INDEX `entry_feed_read_index` ON `%1$sentry`(`id_feed`,`is_read`);	-- v1.7 Located here to be auto-added
 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
+	`id` SMALLINT NOT NULL AUTO_INCREMENT,
+	`name` VARCHAR(63) NOT NULL,
+	`attributes` TEXT,
+	PRIMARY KEY (`id`),
+	UNIQUE KEY (`name`)
+) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
+ENGINE = INNODB;
+
+CREATE TABLE IF NOT EXISTS `%1$sentrytag` (	-- 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,
+	INDEX (`id_entry`)
+) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
+ENGINE = INNODB;
+');
+
 define('SQL_INSERT_FEEDS', '
 define('SQL_INSERT_FEEDS', '
-INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);
+INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("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);
 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$sentrytmp`, `%1$sentry`, `%1$sfeed`, `%1$scategory`');
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `%1$sentrytag`, `%1$stag`, `%1$sentrytmp`, `%1$sentry`, `%1$sfeed`, `%1$scategory`');
 
 
 define('SQL_UPDATE_UTF8MB4', '
 define('SQL_UPDATE_UTF8MB4', '
-ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;	-- v1.5.0
 
 
 ALTER TABLE `%1$scategory` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
 ALTER TABLE `%1$scategory` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-UPDATE `%1$scategory` SET name=SUBSTRING(name,1,190) WHERE LENGTH(name) > 191;
-ALTER TABLE `%1$scategory` MODIFY `name` VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
+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`;
 OPTIMIZE TABLE `%1$scategory`;
 
 
 ALTER TABLE `%1$sfeed` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
 ALTER TABLE `%1$sfeed` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-UPDATE `%1$sfeed` SET name=SUBSTRING(name,1,190) WHERE LENGTH(name) > 191;
-ALTER TABLE `%1$sfeed` MODIFY `name` VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
-ALTER TABLE `%1$sfeed` MODIFY `description` text 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`;
 OPTIMIZE TABLE `%1$sfeed`;
 
 
 ALTER TABLE `%1$sentry` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
 ALTER TABLE `%1$sentry` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@@ -115,3 +136,8 @@ ALTER TABLE `%1$sentry` MODIFY `author` VARCHAR(255) CHARACTER SET utf8mb4 COLLA
 ALTER TABLE `%1$sentry` MODIFY `tags` VARCHAR(1023) 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`;
 OPTIMIZE TABLE `%1$sentry`;
 ');
 ');
+
+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;
+');

+ 40 - 16
app/SQL/install.sql.pgsql.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-define('SQL_CREATE_DB', 'CREATE DATABASE %1$s ENCODING \'UTF8\';');
+define('SQL_CREATE_DB', 'CREATE DATABASE "%1$s" ENCODING \'UTF8\';');
 
 
 global $SQL_CREATE_TABLES;
 global $SQL_CREATE_TABLES;
 $SQL_CREATE_TABLES = array(
 $SQL_CREATE_TABLES = array(
@@ -10,16 +10,16 @@ $SQL_CREATE_TABLES = array(
 
 
 'CREATE TABLE IF NOT EXISTS "%1$sfeed" (
 'CREATE TABLE IF NOT EXISTS "%1$sfeed" (
 	"id" SERIAL PRIMARY KEY,
 	"id" SERIAL PRIMARY KEY,
-	"url" varchar(511) UNIQUE NOT NULL,
+	"url" VARCHAR(511) UNIQUE NOT NULL,
 	"category" SMALLINT DEFAULT 0,
 	"category" SMALLINT DEFAULT 0,
 	"name" VARCHAR(255) NOT NULL,
 	"name" VARCHAR(255) NOT NULL,
 	"website" VARCHAR(255),
 	"website" VARCHAR(255),
-	"description" text,
+	"description" TEXT,
 	"lastUpdate" INT DEFAULT 0,
 	"lastUpdate" INT DEFAULT 0,
 	"priority" SMALLINT NOT NULL DEFAULT 10,
 	"priority" SMALLINT NOT NULL DEFAULT 10,
 	"pathEntries" VARCHAR(511) DEFAULT NULL,
 	"pathEntries" VARCHAR(511) DEFAULT NULL,
 	"httpAuth" VARCHAR(511) DEFAULT NULL,
 	"httpAuth" VARCHAR(511) DEFAULT NULL,
-	"error" smallint DEFAULT 0,
+	"error" SMALLINT DEFAULT 0,
 	"keep_history" INT NOT NULL DEFAULT -2,
 	"keep_history" INT NOT NULL DEFAULT -2,
 	"ttl" INT NOT NULL DEFAULT 0,
 	"ttl" INT NOT NULL DEFAULT 0,
 	"attributes" TEXT,	-- v1.11.0
 	"attributes" TEXT,	-- v1.11.0
@@ -27,9 +27,9 @@ $SQL_CREATE_TABLES = array(
 	"cache_nbUnreads" INT DEFAULT 0,
 	"cache_nbUnreads" INT DEFAULT 0,
 	FOREIGN KEY ("category") REFERENCES "%1$scategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE
 	FOREIGN KEY ("category") REFERENCES "%1$scategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE
 );',
 );',
-'CREATE INDEX %1$sname_index ON "%1$sfeed" ("name");',
-'CREATE INDEX %1$spriority_index ON "%1$sfeed" ("priority");',
-'CREATE INDEX %1$skeep_history_index ON "%1$sfeed" ("keep_history");',
+'CREATE INDEX "%1$sname_index" ON "%1$sfeed" ("name");',
+'CREATE INDEX "%1$spriority_index" ON "%1$sfeed" ("priority");',
+'CREATE INDEX "%1$skeep_history_index" ON "%1$sfeed" ("keep_history");',
 
 
 'CREATE TABLE IF NOT EXISTS "%1$sentry" (
 'CREATE TABLE IF NOT EXISTS "%1$sentry" (
 	"id" BIGINT NOT NULL PRIMARY KEY,
 	"id" BIGINT NOT NULL PRIMARY KEY,
@@ -48,11 +48,14 @@ $SQL_CREATE_TABLES = array(
 	FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
 	FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
 	UNIQUE ("id_feed","guid")
 	UNIQUE ("id_feed","guid")
 );',
 );',
-'CREATE INDEX %1$sis_favorite_index ON "%1$sentry" ("is_favorite");',
-'CREATE INDEX %1$sis_read_index ON "%1$sentry" ("is_read");',
-'CREATE INDEX %1$sentry_lastSeen_index ON "%1$sentry" ("lastSeen");',
+'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\');',
+'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"\');',
 );
 );
 
 
 global $SQL_CREATE_TABLE_ENTRYTMP;
 global $SQL_CREATE_TABLE_ENTRYTMP;
@@ -74,15 +77,36 @@ $SQL_CREATE_TABLE_ENTRYTMP = array(
 	FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
 	FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
 	UNIQUE ("id_feed","guid")
 	UNIQUE ("id_feed","guid")
 );',
 );',
-'CREATE INDEX %1$sentrytmp_date_index ON "%1$sentrytmp" ("date");',
+'CREATE INDEX "%1$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 "%1$sentry_feed_read_index" ON "%1$sentry" ("id_feed","is_read");',	//v1.7
+);
+
+global $SQL_CREATE_TABLE_TAGS;
+$SQL_CREATE_TABLE_TAGS = array(
+'CREATE TABLE IF NOT EXISTS "%1$stag" (	-- v1.12
+	"id" SERIAL PRIMARY KEY,
+	"name" VARCHAR(63) UNIQUE NOT NULL,
+	"attributes" TEXT
+);',
+'CREATE TABLE IF NOT EXISTS "%1$sentrytag" (
+	"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");',
 );
 );
 
 
 global $SQL_INSERT_FEEDS;
 global $SQL_INSERT_FEEDS;
 $SQL_INSERT_FEEDS = array(
 $SQL_INSERT_FEEDS = array(
-'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'http://freshrss.org/feeds/all.atom.xml\', 1, \'FreshRSS.org\', \'http://freshrss.org/\', \'FreshRSS, a free, self-hostable aggregator…\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'http://freshrss.org/feeds/all.atom.xml\');',
-'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'https://github.com/FreshRSS/FreshRSS/releases.atom\', 1, \'FreshRSS @ GitHub\', \'https://github.com/FreshRSS/FreshRSS/\', \'FreshRSS releases @ GitHub\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'https://github.com/FreshRSS/FreshRSS/releases.atom\');',
+'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\');',
 );
 );
 
 
-define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentrytmp", "%1$sentry", "%1$sfeed", "%1$scategory"');
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentrytag", "%1$stag", "%1$sentrytmp", "%1$sentry", "%1$sfeed", "%1$scategory"');

+ 55 - 69
app/SQL/install.sql.sqlite.php

@@ -3,27 +3,27 @@ global $SQL_CREATE_TABLES;
 $SQL_CREATE_TABLES = array(
 $SQL_CREATE_TABLES = array(
 'CREATE TABLE IF NOT EXISTS `category` (
 'CREATE TABLE IF NOT EXISTS `category` (
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-	`name` varchar(255) NOT NULL,
+	`name` VARCHAR(255) NOT NULL,
 	UNIQUE (`name`)
 	UNIQUE (`name`)
 );',
 );',
 
 
 'CREATE TABLE IF NOT EXISTS `feed` (
 'CREATE TABLE IF NOT EXISTS `feed` (
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-	`url` varchar(511) NOT NULL,
+	`url` VARCHAR(511) NOT NULL,
 	`category` SMALLINT DEFAULT 0,
 	`category` SMALLINT DEFAULT 0,
-	`name` varchar(255) NOT NULL,
-	`website` varchar(255),
-	`description` text,
-	`lastUpdate` int(11) DEFAULT 0,	-- Until year 2038
-	`priority` tinyint(2) NOT NULL DEFAULT 10,
-	`pathEntries` varchar(511) DEFAULT NULL,
-	`httpAuth` varchar(511) DEFAULT NULL,
-	`error` boolean DEFAULT 0,
+	`name` VARCHAR(255) NOT NULL,
+	`website` VARCHAR(255),
+	`description` TEXT,
+	`lastUpdate` INT(11) DEFAULT 0,	-- Until year 2038
+	`priority` TINYINT(2) NOT NULL DEFAULT 10,
+	`pathEntries` VARCHAR(511) DEFAULT NULL,
+	`httpAuth` VARCHAR(511) DEFAULT NULL,
+	`error` BOOLEAN DEFAULT 0,
 	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,
 	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,
 	`ttl` INT NOT NULL DEFAULT 0,
 	`ttl` INT NOT NULL DEFAULT 0,
 	`attributes` TEXT,	-- v1.11.0
 	`attributes` TEXT,	-- v1.11.0
-	`cache_nbEntries` int DEFAULT 0,
-	`cache_nbUnreads` int DEFAULT 0,
+	`cache_nbEntries` INT DEFAULT 0,
+	`cache_nbUnreads` INT DEFAULT 0,
 	FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
 	FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
 	UNIQUE (`url`)
 	UNIQUE (`url`)
 );',
 );',
@@ -32,19 +32,19 @@ $SQL_CREATE_TABLES = array(
 'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `feed`(`keep_history`);',
 'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `feed`(`keep_history`);',
 
 
 '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,
-	`author` varchar(255),
-	`content` text,
-	`link` varchar(1023) NOT NULL,
-	`date` int(11),	-- Until year 2038
+	`id` BIGINT NOT NULL,
+	`guid` VARCHAR(760) NOT NULL,
+	`title` VARCHAR(255) NOT NULL,
+	`author` VARCHAR(255),
+	`content` TEXT,
+	`link` VARCHAR(1023) NOT NULL,
+	`date` INT(11),	-- Until year 2038
 	`lastSeen` INT(11) DEFAULT 0,	-- v1.1.1, Until year 2038
 	`lastSeen` INT(11) DEFAULT 0,	-- v1.1.1, Until year 2038
 	`hash` BINARY(16),	-- v1.1.1
 	`hash` BINARY(16),	-- v1.1.1
-	`is_read` boolean NOT NULL DEFAULT 0,
-	`is_favorite` boolean NOT NULL DEFAULT 0,
+	`is_read` BOOLEAN NOT NULL DEFAULT 0,
+	`is_favorite` BOOLEAN NOT NULL DEFAULT 0,
 	`id_feed` SMALLINT,
 	`id_feed` SMALLINT,
-	`tags` varchar(1023),
+	`tags` VARCHAR(1023),
 	PRIMARY KEY (`id`),
 	PRIMARY KEY (`id`),
 	FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	UNIQUE (`id_feed`,`guid`)
 	UNIQUE (`id_feed`,`guid`)
@@ -59,19 +59,19 @@ $SQL_CREATE_TABLES = array(
 global $SQL_CREATE_TABLE_ENTRYTMP;
 global $SQL_CREATE_TABLE_ENTRYTMP;
 $SQL_CREATE_TABLE_ENTRYTMP = array(
 $SQL_CREATE_TABLE_ENTRYTMP = array(
 'CREATE TABLE IF NOT EXISTS `entrytmp` (	-- v1.7
 'CREATE TABLE IF NOT EXISTS `entrytmp` (	-- v1.7
-	`id` bigint NOT NULL,
-	`guid` varchar(760) NOT NULL,
-	`title` varchar(255) NOT NULL,
-	`author` varchar(255),
-	`content` text,
-	`link` varchar(1023) NOT NULL,
-	`date` int(11),
+	`id` BIGINT NOT NULL,
+	`guid` VARCHAR(760) NOT NULL,
+	`title` VARCHAR(255) NOT NULL,
+	`author` VARCHAR(255),
+	`content` TEXT,
+	`link` VARCHAR(1023) NOT NULL,
+	`date` INT(11),
 	`lastSeen` INT(11) DEFAULT 0,
 	`lastSeen` INT(11) DEFAULT 0,
 	`hash` BINARY(16),
 	`hash` BINARY(16),
-	`is_read` boolean NOT NULL DEFAULT 0,
-	`is_favorite` boolean NOT NULL DEFAULT 0,
+	`is_read` BOOLEAN NOT NULL DEFAULT 0,
+	`is_favorite` BOOLEAN NOT NULL DEFAULT 0,
 	`id_feed` SMALLINT,
 	`id_feed` SMALLINT,
-	`tags` varchar(1023),
+	`tags` VARCHAR(1023),
 	PRIMARY KEY (`id`),
 	PRIMARY KEY (`id`),
 	FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	UNIQUE (`id_feed`,`guid`)
 	UNIQUE (`id_feed`,`guid`)
@@ -81,44 +81,30 @@ $SQL_CREATE_TABLE_ENTRYTMP = array(
 'CREATE INDEX IF NOT EXISTS `entry_feed_read_index` ON `entry`(`id_feed`,`is_read`);',	//v1.7
 'CREATE INDEX IF NOT EXISTS `entry_feed_read_index` ON `entry`(`id_feed`,`is_read`);',	//v1.7
 );
 );
 
 
+global $SQL_CREATE_TABLE_TAGS;
+$SQL_CREATE_TABLE_TAGS = array(
+'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` (
+	`id_tag` SMALLINT,
+	`id_entry` SMALLINT,
+	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`);',
+);
+
 global $SQL_INSERT_FEEDS;
 global $SQL_INSERT_FEEDS;
 $SQL_INSERT_FEEDS = array(
 $SQL_INSERT_FEEDS = array(
-'INSERT OR IGNORE INTO `feed`
-	(
-		url,
-		category,
-		name,
-		website,
-		description,
-		ttl
-	)
-	VALUES
-	(
-		"http://freshrss.org/feeds/all.atom.xml",
-		1,
-		"FreshRSS.org",
-		"http://freshrss.org/",
-		"FreshRSS, a free, self-hostable aggregator…",
-		86400
-	);',
-'INSERT OR IGNORE INTO `feed`
-	(
-		url,
-		category,
-		name,
-		website,
-		description,
-		ttl
-	)
-	VALUES
-	(
-		"https://github.com/FreshRSS/FreshRSS/releases.atom",
-		1,
-		"FreshRSS releases",
-		"https://github.com/FreshRSS/FreshRSS/",
-		"FreshRSS releases @ GitHub",
-		86400
-	);',
+'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);',
 );
 );
 
 
-define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `entrytmp`, `entry`, `feed`, `category`');
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `entrytag`, `tag`, `entrytmp`, `entry`, `feed`, `category`');

+ 1 - 1
app/i18n/cz/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => 'Spodní řádek',
 			'bottom_line' => 'Spodní řádek',
 			'entry' => 'Ikony článků',
 			'entry' => 'Ikony článků',
 			'publication_date' => 'Datum vydání',
 			'publication_date' => 'Datum vydání',
-			'related_tags' => 'Související tagy',
+			'related_tags' => 'Související tagy',	//TODO
 			'sharing' => 'Sdílení',
 			'sharing' => 'Sdílení',
 			'top_line' => 'Horní řádek',
 			'top_line' => 'Horní řádek',
 		),
 		),

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

@@ -181,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => 'Upozornění!',
 		'attention' => 'Upozornění!',
 		'blank_to_disable' => 'Zakázat - ponechte prázdné',
 		'blank_to_disable' => 'Zakázat - ponechte prázdné',
-		'by_author' => 'Od <em>%s</em>',
+		'by_author' => 'Od:',
 		'by_default' => 'Výchozí',
 		'by_default' => 'Výchozí',
 		'damn' => 'Sakra!',
 		'damn' => 'Sakra!',
 		'default_category' => 'Nezařazeno',
 		'default_category' => 'Nezařazeno',

+ 3 - 2
app/i18n/cz/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Hlášení chyb',
 		'bugs_reports' => 'Hlášení chyb',
 		'credits' => 'Poděkování',
 		'credits' => 'Poděkování',
 		'credits_content' => 'Některé designové prvky pocházejí z <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, FreshRSS ale tuto platformu nevyužívá. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Ikony</a> pocházejí z <a href="https://www.gnome.org/">GNOME projektu</a>. Font <em>Open Sans</em> vytvořil <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS je založen na PHP framework <a href="https://github.com/marienfressinaud/MINZ">Minz</a>.',
 		'credits_content' => 'Některé designové prvky pocházejí z <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, FreshRSS ale tuto platformu nevyužívá. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Ikony</a> pocházejí z <a href="https://www.gnome.org/">GNOME projektu</a>. Font <em>Open Sans</em> vytvořil <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS je založen na PHP framework <a href="https://github.com/marienfressinaud/MINZ">Minz</a>.',
-		'freshrss_description' => 'FreshRSS je čtečka RSS kanálů určená k provozu na vlastním serveru, podobná <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> nebo <a href="http://projet.idleman.fr/leed/">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="http://leed.idleman.fr/">Leed</a>. Je to nenáročný a jednoduchý, zároveň ale mocný a konfigurovatelný nástroj.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">na Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">na Github</a>',
 		'license' => 'Licence',
 		'license' => 'Licence',
 		'project_website' => 'Stránka projektu',
 		'project_website' => 'Stránka projektu',
@@ -53,10 +53,11 @@ return array(
 		'starred' => 'Zobrazit oblíbené',
 		'starred' => 'Zobrazit oblíbené',
 		'stats' => 'Statistika',
 		'stats' => 'Statistika',
 		'subscription' => 'Správa subskripcí',
 		'subscription' => 'Správa subskripcí',
+		'tags' => 'My labels',	//TODO
 		'unread' => 'Zobrazovat nepřečtené',
 		'unread' => 'Zobrazovat nepřečtené',
 	),
 	),
 	'share' => 'Sdílet',
 	'share' => 'Sdílet',
 	'tag' => array(
 	'tag' => array(
-		'related' => 'Související tagy',
+		'related' => 'Související tagy',	//TODO
 	),
 	),
 );
 );

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

@@ -27,6 +27,7 @@ return array(
 			'password' => 'Heslo',
 			'password' => 'Heslo',
 			'username' => 'Přihlašovací jméno',
 			'username' => 'Přihlašovací jméno',
 		),
 		),
+		'clear_cache' => 'Always clear cache',	//TODO
 		'css_help' => 'Stáhne zkrácenou verzi RSS kanálů (pozor, náročnější na čas!)',
 		'css_help' => 'Stáhne zkrácenou verzi RSS kanálů (pozor, náročnější na čas!)',
 		'css_path' => 'Původní CSS soubor článku z webových stránek',
 		'css_path' => 'Původní CSS soubor článku z webových stránek',
 		'description' => 'Popis',
 		'description' => 'Popis',

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

@@ -67,8 +67,8 @@ return array(
 			'ok' => 'Sie haben die JSON-Erweiterung.',
 			'ok' => 'Sie haben die JSON-Erweiterung.',
 		),
 		),
 		'mbstring' => array(
 		'mbstring' => array(
-			'nok' => 'Cannot find the recommended library mbstring for Unicode.',	//TODO
-			'ok' => 'You have the recommended library mbstring for Unicode.',	//TODO
+			'nok' => 'Ihnen fehlt die mbstring-Bibliothek für Unicode.',	//TODO
+			'ok' => 'Sie haben die empfohlene mbstring-Bliothek für Unicode.',	//TODO
 		),
 		),
 		'minz' => array(
 		'minz' => array(
 			'nok' => 'Ihnen fehlt das Minz-Framework.',
 			'nok' => 'Ihnen fehlt das Minz-Framework.',

+ 2 - 2
app/i18n/de/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => 'Fußzeile',
 			'bottom_line' => 'Fußzeile',
 			'entry' => 'Artikel-Symbole',
 			'entry' => 'Artikel-Symbole',
 			'publication_date' => 'Datum der Veröffentlichung',
 			'publication_date' => 'Datum der Veröffentlichung',
-			'related_tags' => 'Verwandte Tags',
+			'related_tags' => 'Verwandte Tags',	//TODO
 			'sharing' => 'Teilen',
 			'sharing' => 'Teilen',
 			'top_line' => 'Kopfzeile',
 			'top_line' => 'Kopfzeile',
 		),
 		),
@@ -102,7 +102,7 @@ return array(
 		'read' => array(
 		'read' => array(
 			'article_open_on_website' => 'wenn der Artikel auf der Original-Webseite geöffnet wird',
 			'article_open_on_website' => 'wenn der Artikel auf der Original-Webseite geöffnet wird',
 			'article_viewed' => 'wenn der Artikel angesehen wird',
 			'article_viewed' => 'wenn der Artikel angesehen wird',
-			'scroll' => 'beim Blättern',
+			'scroll' => 'beim Scrollen bzw. Überspringen',
 			'upon_reception' => 'beim Empfang des Artikels',
 			'upon_reception' => 'beim Empfang des Artikels',
 			'when' => 'Artikel als gelesen markieren…',
 			'when' => 'Artikel als gelesen markieren…',
 		),
 		),

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

@@ -53,8 +53,8 @@ return array(
 	'sub' => array(
 	'sub' => array(
 		'actualize' => 'Aktualisieren',
 		'actualize' => 'Aktualisieren',
 		'articles' => array(
 		'articles' => array(
-			'marked_read' => 'The selected articles have been marked as read.',	//TODO
-			'marked_unread' => 'The articles have been marked as unread.',	//TODO
+			'marked_read' => 'Die ausgewählten Artikel wurden als gelesen markiert.',
+			'marked_unread' => 'Die ausgewählten Artikel wurden als ungelesen markiert.',
 		),
 		),
 		'category' => array(
 		'category' => array(
 			'created' => 'Die Kategorie %s ist erstellt worden.',
 			'created' => 'Die Kategorie %s ist erstellt worden.',

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

@@ -167,6 +167,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'gnusocial' => 'GNU social',
 		'gnusocial' => 'GNU social',
 		'jdh' => 'Journal du hacker',
 		'jdh' => 'Journal du hacker',
+		'Known' => 'Known-Seite (https://withknown.com)',
 		'linkedin' => 'LinkedIn',
 		'linkedin' => 'LinkedIn',
 		'mastodon' => 'Mastodon',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
 		'movim' => 'Movim',
@@ -180,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => 'Achtung!',
 		'attention' => 'Achtung!',
 		'blank_to_disable' => 'Zum Deaktivieren frei lassen',
 		'blank_to_disable' => 'Zum Deaktivieren frei lassen',
-		'by_author' => 'Von <em>%s</em>',
+		'by_author' => 'Von:',
 		'by_default' => 'standardmäßig',
 		'by_default' => 'standardmäßig',
 		'damn' => 'Verdammt!',
 		'damn' => 'Verdammt!',
 		'default_category' => 'Unkategorisiert',
 		'default_category' => 'Unkategorisiert',

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

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Fehlerberichte',
 		'bugs_reports' => 'Fehlerberichte',
 		'credits' => 'Credits',
 		'credits' => 'Credits',
 		'credits_content' => 'Einige Designelemente stammen von <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, obwohl FreshRSS dieses Framework nicht nutzt. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> stammen vom <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> Font wurde von <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> erstellt. FreshRSS basiert auf <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, einem PHP-Framework.',
 		'credits_content' => 'Einige Designelemente stammen von <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, obwohl FreshRSS dieses Framework nicht nutzt. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> stammen vom <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> Font wurde von <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> erstellt. FreshRSS basiert auf <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, einem PHP-Framework.',
-		'freshrss_description' => 'FreshRSS ist ein RSS-Feedsaggregator zum selbst hosten wie zum Beispiel <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> oder <a href="http://projet.idleman.fr/leed/">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="http://leed.idleman.fr/">Leed</a>. Er ist leicht und einfach zu handhaben und gleichzeitig ein leistungsstarkes und konfigurierbares Werkzeug.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
 		'license' => 'Lizenz',
 		'license' => 'Lizenz',
 		'project_website' => 'Projekt-Webseite',
 		'project_website' => 'Projekt-Webseite',
@@ -40,7 +40,7 @@ return array(
 		'mark_all_read' => 'Alle als gelesen markieren',
 		'mark_all_read' => 'Alle als gelesen markieren',
 		'mark_cat_read' => 'Kategorie als gelesen markieren',
 		'mark_cat_read' => 'Kategorie als gelesen markieren',
 		'mark_feed_read' => 'Feed als gelesen markieren',
 		'mark_feed_read' => 'Feed als gelesen markieren',
-		'mark_selection_unread' => 'Mark selection as unread',	//TODO
+		'mark_selection_unread' => 'Auswahl als ungelesen markieren',
 		'newer_first' => 'Neuere zuerst',
 		'newer_first' => 'Neuere zuerst',
 		'non-starred' => 'Alle außer Favoriten zeigen',
 		'non-starred' => 'Alle außer Favoriten zeigen',
 		'normal_view' => 'Normale Ansicht',
 		'normal_view' => 'Normale Ansicht',
@@ -53,10 +53,11 @@ return array(
 		'starred' => 'Nur Favoriten zeigen',
 		'starred' => 'Nur Favoriten zeigen',
 		'stats' => 'Statistiken',
 		'stats' => 'Statistiken',
 		'subscription' => 'Abonnementverwaltung',
 		'subscription' => 'Abonnementverwaltung',
+		'tags' => 'My labels',	//TODO
 		'unread' => 'Nur ungelesene zeigen',
 		'unread' => 'Nur ungelesene zeigen',
 	),
 	),
 	'share' => 'Teilen',
 	'share' => 'Teilen',
 	'tag' => array(
 	'tag' => array(
-		'related' => 'Verwandte Tags',
+		'related' => 'Verwandte Tags',	//TODO
 	),
 	),
 );
 );

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

@@ -69,8 +69,8 @@ return array(
 			'ok' => 'Sie haben eine empfohlene Bibliothek um JSON zu parsen.',
 			'ok' => 'Sie haben eine empfohlene Bibliothek um JSON zu parsen.',
 		),
 		),
 		'mbstring' => array(
 		'mbstring' => array(
-			'nok' => 'Cannot find the recommended library mbstring for Unicode.',	//TODO
-			'ok' => 'You have the recommended library mbstring for Unicode.',	//TODO
+			'nok' => 'Es fehlt die empfohlene mbstring-Bibliothek für Unicode.',
+			'ok' => 'Sie haben die empfohlene mbstring-Bibliothek für Unicode.',
 		),
 		),
 		'minz' => array(
 		'minz' => array(
 			'nok' => 'Ihnen fehlt das Minz-Framework.',
 			'nok' => 'Ihnen fehlt das Minz-Framework.',

+ 3 - 2
app/i18n/de/sub.php

@@ -27,6 +27,7 @@ return array(
 			'password' => 'HTTP-Passwort',
 			'password' => 'HTTP-Passwort',
 			'username' => 'HTTP-Nutzername',
 			'username' => 'HTTP-Nutzername',
 		),
 		),
+		'clear_cache' => 'Nicht cachen (für defekte Feeds)',
 		'css_help' => 'Ruft gekürzte RSS-Feeds ab (Achtung, benötigt mehr Zeit!)',
 		'css_help' => 'Ruft gekürzte RSS-Feeds ab (Achtung, benötigt mehr Zeit!)',
 		'css_path' => 'Pfad zur CSS-Datei des Artikels auf der Original-Webseite',
 		'css_path' => 'Pfad zur CSS-Datei des Artikels auf der Original-Webseite',
 		'description' => 'Beschreibung',
 		'description' => 'Beschreibung',
@@ -44,10 +45,10 @@ return array(
 			'main_stream' => 'In Haupt-Feeds zeigen',
 			'main_stream' => 'In Haupt-Feeds zeigen',
 			'normal' => 'Zeige in eigener Kategorie',
 			'normal' => 'Zeige in eigener Kategorie',
 		),
 		),
-		'ssl_verify' => 'Verify SSL security',	//TODO
+		'ssl_verify' => 'Überprüfe SSL Sicherheit',
 		'stats' => 'Statistiken',
 		'stats' => 'Statistiken',
 		'think_to_add' => 'Sie können Feeds hinzufügen.',
 		'think_to_add' => 'Sie können Feeds hinzufügen.',
-		'timeout' => 'Timeout in seconds',	//TODO
+		'timeout' => 'Zeitlimit in Sekunden',
 		'title' => 'Titel',
 		'title' => 'Titel',
 		'title_add' => 'Einen RSS-Feed hinzufügen',
 		'title_add' => 'Einen RSS-Feed hinzufügen',
 		'ttl' => 'Aktualisiere automatisch nicht öfter als',
 		'ttl' => 'Aktualisiere automatisch nicht öfter als',

+ 1 - 1
app/i18n/en/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => 'Bottom line',
 			'bottom_line' => 'Bottom line',
 			'entry' => 'Article icons',
 			'entry' => 'Article icons',
 			'publication_date' => 'Date of publication',
 			'publication_date' => 'Date of publication',
-			'related_tags' => 'Related tags',
+			'related_tags' => 'Article tags',
 			'sharing' => 'Sharing',
 			'sharing' => 'Sharing',
 			'top_line' => 'Top line',
 			'top_line' => 'Top line',
 		),
 		),

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

@@ -181,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => 'Warning!',
 		'attention' => 'Warning!',
 		'blank_to_disable' => 'Leave blank to disable',
 		'blank_to_disable' => 'Leave blank to disable',
-		'by_author' => 'By <em>%s</em>',
+		'by_author' => 'By:',
 		'by_default' => 'By default',
 		'by_default' => 'By default',
 		'damn' => 'Blast!',
 		'damn' => 'Blast!',
 		'default_category' => 'Uncategorized',
 		'default_category' => 'Uncategorized',

+ 3 - 2
app/i18n/en/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Bugs reports',
 		'bugs_reports' => 'Bugs reports',
 		'credits' => 'Credits',
 		'credits' => 'Credits',
 		'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn’t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police has been created by <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
 		'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn’t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police has been created by <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
-		'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="http://projet.idleman.fr/leed/">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="http://leed.idleman.fr/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
 		'license' => 'License',
 		'license' => 'License',
 		'project_website' => 'Project website',
 		'project_website' => 'Project website',
@@ -53,10 +53,11 @@ return array(
 		'starred' => 'Show favourites',
 		'starred' => 'Show favourites',
 		'stats' => 'Statistics',
 		'stats' => 'Statistics',
 		'subscription' => 'Subscriptions management',
 		'subscription' => 'Subscriptions management',
+		'tags' => 'My labels',
 		'unread' => 'Show unread',
 		'unread' => 'Show unread',
 	),
 	),
 	'share' => 'Share',
 	'share' => 'Share',
 	'tag' => array(
 	'tag' => array(
-		'related' => 'Related tags',
+		'related' => 'Article tags',	//TODO
 	),
 	),
 );
 );

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

@@ -27,6 +27,7 @@ return array(
 			'password' => 'HTTP password',
 			'password' => 'HTTP password',
 			'username' => 'HTTP username',
 			'username' => 'HTTP username',
 		),
 		),
+		'clear_cache' => 'Always clear cache',
 		'css_help' => 'Retrieves truncated RSS feeds (caution, requires more time!)',
 		'css_help' => 'Retrieves truncated RSS feeds (caution, requires more time!)',
 		'css_path' => 'Articles CSS path on original website',
 		'css_path' => 'Articles CSS path on original website',
 		'description' => 'Description',
 		'description' => 'Description',

+ 1 - 1
app/i18n/es/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => 'Línea inferior',
 			'bottom_line' => 'Línea inferior',
 			'entry' => 'Iconos de artículos',
 			'entry' => 'Iconos de artículos',
 			'publication_date' => 'Fecha de publicación',
 			'publication_date' => 'Fecha de publicación',
-			'related_tags' => 'Etiquetas relacionadas',
+			'related_tags' => 'Etiquetas relacionadas',	//TODO
 			'sharing' => 'Compartir',
 			'sharing' => 'Compartir',
 			'top_line' => 'Línea superior',
 			'top_line' => 'Línea superior',
 		),
 		),

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

@@ -181,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => '¡Aviso!',
 		'attention' => '¡Aviso!',
 		'blank_to_disable' => 'Deja en blanco para desactivar',
 		'blank_to_disable' => 'Deja en blanco para desactivar',
-		'by_author' => 'Por <em>%s</em>',
+		'by_author' => 'Por:',
 		'by_default' => 'Por defecto',
 		'by_default' => 'Por defecto',
 		'damn' => '¡Córcholis!',
 		'damn' => '¡Córcholis!',
 		'default_category' => 'Sin categorizar',
 		'default_category' => 'Sin categorizar',

+ 3 - 2
app/i18n/es/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Informe de fallos',
 		'bugs_reports' => 'Informe de fallos',
 		'credits' => 'Créditos',
 		'credits' => 'Créditos',
 		'credits_content' => 'Aunque FreshRSS no usa ese entorno, algunos elementos del diseño están obtenidos de <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>. Los <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Iconos</a> han sido obtenidos del <a href="https://www.gnome.org/">proyecto GNOME</a>. La fuente <em>Open Sans</em> es una creación de <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS usa el entorno PHP <a href="https://github.com/marienfressinaud/MINZ">Minz</a>.',
 		'credits_content' => 'Aunque FreshRSS no usa ese entorno, algunos elementos del diseño están obtenidos de <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>. Los <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Iconos</a> han sido obtenidos del <a href="https://www.gnome.org/">proyecto GNOME</a>. La fuente <em>Open Sans</em> es una creación de <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS usa el entorno PHP <a href="https://github.com/marienfressinaud/MINZ">Minz</a>.',
-		'freshrss_description' => 'FreshRSS es un agregador de fuentes RSS de alojamiento privado al estilo de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> o <a href="http://projet.idleman.fr/leed/">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="http://leed.idleman.fr/">Leed</a>. Es una herramienta potente, pero ligera y fácil de usar y configurar.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">en Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">en Github</a>',
 		'license' => 'Licencia',
 		'license' => 'Licencia',
 		'project_website' => 'Web del proyecto',
 		'project_website' => 'Web del proyecto',
@@ -53,10 +53,11 @@ return array(
 		'starred' => 'Mostrar solo los favoritos',
 		'starred' => 'Mostrar solo los favoritos',
 		'stats' => 'Estadísticas',
 		'stats' => 'Estadísticas',
 		'subscription' => 'Administración de suscripciones',
 		'subscription' => 'Administración de suscripciones',
+		'tags' => 'My labels',	//TODO
 		'unread' => 'Mostar solo no leídos',
 		'unread' => 'Mostar solo no leídos',
 	),
 	),
 	'share' => 'Compartir',
 	'share' => 'Compartir',
 	'tag' => array(
 	'tag' => array(
-		'related' => 'Etiquetas relacionadas',
+		'related' => 'Etiquetas relacionadas',	//TODO
 	),
 	),
 );
 );

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

@@ -22,6 +22,7 @@ return array(
 			'password' => 'Contraseña HTTP',
 			'password' => 'Contraseña HTTP',
 			'username' => 'Nombre de usuario HTTP',
 			'username' => 'Nombre de usuario HTTP',
 		),
 		),
+		'clear_cache' => 'Always clear cache',	//TODO
 		'css_help' => 'Recibir fuentes RSS truncadas (aviso, ¡necesita más tiempo!)',
 		'css_help' => 'Recibir fuentes RSS truncadas (aviso, ¡necesita más tiempo!)',
 		'css_path' => 'Ruta a la CSS de los artículos en la web original',
 		'css_path' => 'Ruta a la CSS de los artículos en la web original',
 		'description' => 'Descripción',
 		'description' => 'Descripción',

+ 1 - 1
app/i18n/fr/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => 'Ligne du bas',
 			'bottom_line' => 'Ligne du bas',
 			'entry' => 'Icônes d’article',
 			'entry' => 'Icônes d’article',
 			'publication_date' => 'Date de publication',
 			'publication_date' => 'Date de publication',
-			'related_tags' => 'Tags associés',
+			'related_tags' => 'Tags de l’article',
 			'sharing' => 'Partage',
 			'sharing' => 'Partage',
 			'top_line' => 'Ligne du haut',
 			'top_line' => 'Ligne du haut',
 		),
 		),

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

@@ -181,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => 'Attention !',
 		'attention' => 'Attention !',
 		'blank_to_disable' => 'Laissez vide pour désactiver',
 		'blank_to_disable' => 'Laissez vide pour désactiver',
-		'by_author' => 'Par <em>%s</em>',
+		'by_author' => 'Par :',
 		'by_default' => 'Par défaut',
 		'by_default' => 'Par défaut',
 		'damn' => 'Arf !',
 		'damn' => 'Arf !',
 		'default_category' => 'Sans catégorie',
 		'default_category' => 'Sans catégorie',

+ 3 - 2
app/i18n/fr/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Rapports de bugs',
 		'bugs_reports' => 'Rapports de bugs',
 		'credits' => 'Crédits',
 		'credits' => 'Crédits',
 		'credits_content' => 'Des éléments de design sont issus du <a href="http://twitter.github.io/bootstrap/">projet Bootstrap</a> bien que FreshRSS n’utilise pas ce framework. Les <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icônes</a> sont issues du <a href="https://www.gnome.org/">projet GNOME</a>. La police <em>Open Sans</em> utilisée a été créée par <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS repose sur <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
 		'credits_content' => 'Des éléments de design sont issus du <a href="http://twitter.github.io/bootstrap/">projet Bootstrap</a> bien que FreshRSS n’utilise pas ce framework. Les <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icônes</a> sont issues du <a href="https://www.gnome.org/">projet GNOME</a>. La police <em>Open Sans</em> utilisée a été créée par <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS repose sur <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
-		'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://projet.idleman.fr/leed/">Leed</a>. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.',
+		'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.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">sur Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">sur Github</a>',
 		'license' => 'Licence',
 		'license' => 'Licence',
 		'project_website' => 'Site du projet',
 		'project_website' => 'Site du projet',
@@ -53,10 +53,11 @@ return array(
 		'starred' => 'Afficher les favoris',
 		'starred' => 'Afficher les favoris',
 		'stats' => 'Statistiques',
 		'stats' => 'Statistiques',
 		'subscription' => 'Gestion des abonnements',
 		'subscription' => 'Gestion des abonnements',
+		'tags' => 'Mes étiquettes',
 		'unread' => 'Afficher les non-lus',
 		'unread' => 'Afficher les non-lus',
 	),
 	),
 	'share' => 'Partager',
 	'share' => 'Partager',
 	'tag' => array(
 	'tag' => array(
-		'related' => 'Tags associés',
+		'related' => 'Tags de l’article',
 	),
 	),
 );
 );

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

@@ -27,6 +27,7 @@ return array(
 			'password' => 'Mot de passe HTTP',
 			'password' => 'Mot de passe HTTP',
 			'username' => 'Identifiant HTTP',
 			'username' => 'Identifiant HTTP',
 		),
 		),
+		'clear_cache' => 'Toujours vider le cache',
 		'css_help' => 'Permet de récupérer les flux tronqués (attention, demande plus de temps !)',
 		'css_help' => 'Permet de récupérer les flux tronqués (attention, demande plus de temps !)',
 		'css_path' => 'Sélecteur CSS des articles sur le site d’origine',
 		'css_path' => 'Sélecteur CSS des articles sur le site d’origine',
 		'description' => 'Description',
 		'description' => 'Description',

+ 1 - 1
app/i18n/he/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => 'שורה תחתונה',
 			'bottom_line' => 'שורה תחתונה',
 			'entry' => 'סמלילי מאמרים',
 			'entry' => 'סמלילי מאמרים',
 			'publication_date' => 'תאריך הפרסום',
 			'publication_date' => 'תאריך הפרסום',
-			'related_tags' => 'תגיות קשורות',
+			'related_tags' => 'תגיות קשורות',	//TODO
 			'sharing' => 'שיתוף',
 			'sharing' => 'שיתוף',
 			'top_line' => 'שורה עליונה',
 			'top_line' => 'שורה עליונה',
 		),
 		),

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

@@ -181,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => 'זהירות!',
 		'attention' => 'זהירות!',
 		'blank_to_disable' => 'יש להשאיר ריק על מנת לנטרל',
 		'blank_to_disable' => 'יש להשאיר ריק על מנת לנטרל',
-		'by_author' => 'מאת <em>%s</em>',
+		'by_author' => 'מאת :',
 		'by_default' => 'ברירת מחדל',
 		'by_default' => 'ברירת מחדל',
 		'damn' => 'הו לא!',
 		'damn' => 'הו לא!',
 		'default_category' => 'ללא קטגוריה',
 		'default_category' => 'ללא קטגוריה',

+ 3 - 2
app/i18n/he/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'דיווח באגים',
 		'bugs_reports' => 'דיווח באגים',
 		'credits' => 'קרדיטים',
 		'credits' => 'קרדיטים',
 		'credits_content' => 'מאפייני עיצוב מסויימים הגיעו מ <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> אף על פי ש FreshRSS אינו משתמש בתשתית הזו. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">סמלילים</a> הגיעו מ <a href="https://www.gnome.org/"> פרוייקט GNOME </a>. <em>Open Sans</em> הגופן police נוצר על ידי <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Favicons נאספים בעזרת <a href="https://getfavicon.appspot.com/">getFavicon API</a>. FreshRSS מבוסס על <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, תשתית PHP.',
 		'credits_content' => 'מאפייני עיצוב מסויימים הגיעו מ <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> אף על פי ש FreshRSS אינו משתמש בתשתית הזו. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">סמלילים</a> הגיעו מ <a href="https://www.gnome.org/"> פרוייקט GNOME </a>. <em>Open Sans</em> הגופן police נוצר על ידי <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Favicons נאספים בעזרת <a href="https://getfavicon.appspot.com/">getFavicon API</a>. FreshRSS מבוסס על <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, תשתית PHP.',
-		'freshrss_description' => 'FreshRSS הוא קורא RSS לאחסון עצמי בדומה ל <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> או <a href="http://projet.idleman.fr/leed/">Leed</a>. אינו צורך משאבים רבים, וקל לתפעול אך בו בזמן חזק וניתן להתאמה.',
+		'freshrss_description' => 'FreshRSS הוא קורא RSS לאחסון עצמי בדומה ל <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> או <a href="http://leed.idleman.fr/">Leed</a>. אינו צורך משאבים רבים, וקל לתפעול אך בו בזמן חזק וניתן להתאמה.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">בגיטהאב</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">בגיטהאב</a>',
 		'license' => 'רישיון',
 		'license' => 'רישיון',
 		'project_website' => 'אתר',
 		'project_website' => 'אתר',
@@ -53,10 +53,11 @@ return array(
 		'starred' => 'הצגת מועדפים בלבד',
 		'starred' => 'הצגת מועדפים בלבד',
 		'stats' => 'סטטיסטיקות',
 		'stats' => 'סטטיסטיקות',
 		'subscription' => 'ניהול הרשמות',
 		'subscription' => 'ניהול הרשמות',
+		'tags' => 'My labels',	//TODO
 		'unread' => 'הצגת מאמרים שלא נקראו בלבד',
 		'unread' => 'הצגת מאמרים שלא נקראו בלבד',
 	),
 	),
 	'share' => 'שיתוף',
 	'share' => 'שיתוף',
 	'tag' => array(
 	'tag' => array(
-		'related' => 'תגיות קשורות',
+		'related' => 'תגיות קשורות',	//TODO
 	),
 	),
 );
 );

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

@@ -27,6 +27,7 @@ return array(
 			'password' => 'HTTP סיסמה',
 			'password' => 'HTTP סיסמה',
 			'username' => 'HTTP שם משתמש',
 			'username' => 'HTTP שם משתמש',
 		),
 		),
+		'clear_cache' => 'Always clear cache',	//TODO
 		'css_help' => 'קבלת הזנות RSS קטומות  (זהירות, לוקח זמן רב יותר!)',
 		'css_help' => 'קבלת הזנות RSS קטומות  (זהירות, לוקח זמן רב יותר!)',
 		'css_path' => 'נתיב הCSS של המאמר באתר המקורי',
 		'css_path' => 'נתיב הCSS של המאמר באתר המקורי',
 		'description' => 'תיאור',
 		'description' => 'תיאור',

+ 1 - 1
app/i18n/it/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => 'Barra in fondo',
 			'bottom_line' => 'Barra in fondo',
 			'entry' => 'Icone degli articoli',
 			'entry' => 'Icone degli articoli',
 			'publication_date' => 'Data di pubblicazione',
 			'publication_date' => 'Data di pubblicazione',
-			'related_tags' => 'Tags correlati',
+			'related_tags' => 'Tags correlati',	//TODO
 			'sharing' => 'Condivisione',
 			'sharing' => 'Condivisione',
 			'top_line' => 'Barra in alto',
 			'top_line' => 'Barra in alto',
 		),
 		),

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

@@ -181,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => 'Attenzione!',
 		'attention' => 'Attenzione!',
 		'blank_to_disable' => 'Lascia vuoto per disabilitare',
 		'blank_to_disable' => 'Lascia vuoto per disabilitare',
-		'by_author' => 'di <em>%s</em>',
+		'by_author' => 'di:',
 		'by_default' => 'predefinito',
 		'by_default' => 'predefinito',
 		'damn' => 'Ops!',
 		'damn' => 'Ops!',
 		'default_category' => 'Senza categoria',
 		'default_category' => 'Senza categoria',

+ 3 - 2
app/i18n/it/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Bugs',
 		'bugs_reports' => 'Bugs',
 		'credits' => 'Crediti',
 		'credits' => 'Crediti',
 		'credits_content' => 'Alcuni elementi di design provengono da <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> sebbene FreshRSS non usi questo framework. Le <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icone</a> provengono dal progetto <a href="https://www.gnome.org/">GNOME</a>. Il carattere <em>Open Sans</em> è stato creato da <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS è basato su <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
 		'credits_content' => 'Alcuni elementi di design provengono da <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> sebbene FreshRSS non usi questo framework. Le <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icone</a> provengono dal progetto <a href="https://www.gnome.org/">GNOME</a>. Il carattere <em>Open Sans</em> è stato creato da <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS è basato su <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
-		'freshrss_description' => 'FreshRSS è un aggregatore di feeds RSS da installare sul proprio host come <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> o <a href="http://projet.idleman.fr/leed/">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="http://leed.idleman.fr/">Leed</a>. Leggero e facile da mantenere pur essendo molto configurabile e potente.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">su Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">su Github</a>',
 		'license' => 'Licenza',
 		'license' => 'Licenza',
 		'project_website' => 'Sito del progetto',
 		'project_website' => 'Sito del progetto',
@@ -53,10 +53,11 @@ return array(
 		'starred' => 'Mostra solo preferiti',
 		'starred' => 'Mostra solo preferiti',
 		'stats' => 'Statistiche',
 		'stats' => 'Statistiche',
 		'subscription' => 'Gestione sottoscrizioni',
 		'subscription' => 'Gestione sottoscrizioni',
+		'tags' => 'My labels',	//TODO
 		'unread' => 'Mostra solo non letti',
 		'unread' => 'Mostra solo non letti',
 	),
 	),
 	'share' => 'Condividi',
 	'share' => 'Condividi',
 	'tag' => array(
 	'tag' => array(
-		'related' => 'Tags correlati',
+		'related' => 'Tags correlati',	//TODO
 	),
 	),
 );
 );

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

@@ -27,6 +27,7 @@ return array(
 			'password' => 'HTTP password',
 			'password' => 'HTTP password',
 			'username' => 'HTTP username',
 			'username' => 'HTTP username',
 		),
 		),
+		'clear_cache' => 'Always clear cache',	//TODO
 		'css_help' => 'In caso di RSS feeds troncati (attenzione, richiede molto tempo!)',
 		'css_help' => 'In caso di RSS feeds troncati (attenzione, richiede molto tempo!)',
 		'css_path' => 'Percorso del foglio di stile CSS del sito di origine',
 		'css_path' => 'Percorso del foglio di stile CSS del sito di origine',
 		'description' => 'Descrizione',
 		'description' => 'Descrizione',

+ 1 - 1
app/i18n/kr/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => '하단',
 			'bottom_line' => '하단',
 			'entry' => '문서 아이콘',
 			'entry' => '문서 아이콘',
 			'publication_date' => '발행일',
 			'publication_date' => '발행일',
-			'related_tags' => '관련 태그',
+			'related_tags' => '관련 태그',	//TODO
 			'sharing' => '공유',
 			'sharing' => '공유',
 			'top_line' => '상단',
 			'top_line' => '상단',
 		),
 		),

+ 1 - 1
app/i18n/kr/gen.php

@@ -181,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => '경고!',
 		'attention' => '경고!',
 		'blank_to_disable' => '빈 칸으로 두면 비활성화',
 		'blank_to_disable' => '빈 칸으로 두면 비활성화',
-		'by_author' => 'By <em>%s</em>',
+		'by_author' => 'By:',
 		'by_default' => '기본값',
 		'by_default' => '기본값',
 		'damn' => '이런!',
 		'damn' => '이런!',
 		'default_category' => '분류 없음',
 		'default_category' => '분류 없음',

+ 3 - 2
app/i18n/kr/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => '버그 제보하기',
 		'bugs_reports' => '버그 제보하기',
 		'credits' => '크레딧',
 		'credits' => '크레딧',
 		'credits_content' => 'FreshRSS는 <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> 프레임워크를 사용하진 않지만, 일부 디자인 요소를 가져왔습니다. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">아이콘들</a>은 <a href="https://www.gnome.org/">GNOME 프로젝트</a>에서 가져왔습니다. <em>Open Sans</em> 글꼴은 <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>가 제작하였습니다. FreshRSS는 PHP 프레임워크인 <a href="https://github.com/marienfressinaud/MINZ">Minz</a>에 기반하고 있습니다.',
 		'credits_content' => 'FreshRSS는 <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> 프레임워크를 사용하진 않지만, 일부 디자인 요소를 가져왔습니다. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">아이콘들</a>은 <a href="https://www.gnome.org/">GNOME 프로젝트</a>에서 가져왔습니다. <em>Open Sans</em> 글꼴은 <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>가 제작하였습니다. FreshRSS는 PHP 프레임워크인 <a href="https://github.com/marienfressinaud/MINZ">Minz</a>에 기반하고 있습니다.',
-		'freshrss_description' => 'FreshRSS는 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 또는 <a href="http://projet.idleman.fr/leed/">Leed</a>와 같은 셀프 호스팅 기반의 RSS 피드 수집기입니다. FreshRSS는 강력하고 다양한 설정을 할 수 있으면서 도 가볍고 사용하기 쉽습니다.',
+		'freshrss_description' => 'FreshRSS는 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 또는 <a href="http://leed.idleman.fr/">Leed</a>와 같은 셀프 호스팅 기반의 RSS 피드 수집기입니다. FreshRSS는 강력하고 다양한 설정을 할 수 있으면서 도 가볍고 사용하기 쉽습니다.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github 저장소에 제보</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github 저장소에 제보</a>',
 		'license' => '라이센스',
 		'license' => '라이센스',
 		'project_website' => '프로젝트 웹사이트',
 		'project_website' => '프로젝트 웹사이트',
@@ -53,10 +53,11 @@ return array(
 		'starred' => '즐겨찾기만 표시',
 		'starred' => '즐겨찾기만 표시',
 		'stats' => '통계',
 		'stats' => '통계',
 		'subscription' => '구독 관리',
 		'subscription' => '구독 관리',
+		'tags' => 'My labels',	//TODO
 		'unread' => '읽지 않은 글만 표시',
 		'unread' => '읽지 않은 글만 표시',
 	),
 	),
 	'share' => '공유',
 	'share' => '공유',
 	'tag' => array(
 	'tag' => array(
-		'related' => '관련 태그',
+		'related' => '관련 태그',	//TODO
 	),
 	),
 );
 );

+ 1 - 0
app/i18n/kr/sub.php

@@ -27,6 +27,7 @@ return array(
 			'password' => 'HTTP 암호',
 			'password' => 'HTTP 암호',
 			'username' => 'HTTP 사용자 이름',
 			'username' => 'HTTP 사용자 이름',
 		),
 		),
+		'clear_cache' => 'Always clear cache',	//TODO
 		'css_help' => '글의 일부가 포함된 RSS 피드를 가져옵니다 (주의, 시간이 좀 더 걸립니다!)',
 		'css_help' => '글의 일부가 포함된 RSS 피드를 가져옵니다 (주의, 시간이 좀 더 걸립니다!)',
 		'css_path' => '웹사이트 상의 글 본문에 해당하는 CSS 경로',
 		'css_path' => '웹사이트 상의 글 본문에 해당하는 CSS 경로',
 		'description' => '설명',
 		'description' => '설명',

+ 1 - 1
app/i18n/nl/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => 'Onderaan',
 			'bottom_line' => 'Onderaan',
 			'entry' => 'Artikel pictogrammen',
 			'entry' => 'Artikel pictogrammen',
 			'publication_date' => 'Publicatie datum',
 			'publication_date' => 'Publicatie datum',
-			'related_tags' => 'Gerelateerde labels',
+			'related_tags' => 'Gerelateerde labels',	//TODO
 			'sharing' => 'Delen',
 			'sharing' => 'Delen',
 			'top_line' => 'Bovenaan',
 			'top_line' => 'Bovenaan',
 		),
 		),

+ 1 - 1
app/i18n/nl/gen.php

@@ -181,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => 'Attentie!',
 		'attention' => 'Attentie!',
 		'blank_to_disable' => 'Laat leeg om uit te zetten',
 		'blank_to_disable' => 'Laat leeg om uit te zetten',
-		'by_author' => 'Door <em>%s</em>',
+		'by_author' => 'Door:',
 		'by_default' => 'Door standaard',
 		'by_default' => 'Door standaard',
 		'damn' => 'Potverdorie!',
 		'damn' => 'Potverdorie!',
 		'default_category' => 'Niet ingedeeld',
 		'default_category' => 'Niet ingedeeld',

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

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Rapporteer fouten',
 		'bugs_reports' => 'Rapporteer fouten',
 		'credits' => 'Waarderingen',
 		'credits' => 'Waarderingen',
 		'credits_content' => 'Sommige ontwerp elementen komen van <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> alhoewel FreshRSS dit raamwerk niet gebruikt. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Pictogrammen</a> komen van het <a href="https://www.gnome.org/">GNOME project</a>. <em>De Open Sans</em> font police is gemaakt door <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is gebaseerd op <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, een PHP raamwerk. Nederlandse vertaling door Wanabo, <a href="http://www.nieuwskop.be" title="NieuwsKop">NieuwsKop.be</a>. Link naar de Nederlandse vertaling, <a href="https://github.com/Wanabo/FreshRSS-Dutch-translation/tree/master">FreshRSS-Dutch-translation</a>.',
 		'credits_content' => 'Sommige ontwerp elementen komen van <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> alhoewel FreshRSS dit raamwerk niet gebruikt. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Pictogrammen</a> komen van het <a href="https://www.gnome.org/">GNOME project</a>. <em>De Open Sans</em> font police is gemaakt door <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is gebaseerd op <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, een PHP raamwerk. Nederlandse vertaling door Wanabo, <a href="http://www.nieuwskop.be" title="NieuwsKop">NieuwsKop.be</a>. Link naar de Nederlandse vertaling, <a href="https://github.com/Wanabo/FreshRSS-Dutch-translation/tree/master">FreshRSS-Dutch-translation</a>.',
-		'freshrss_description' => 'FreshRSS is een RSS feed aggregator om zelf te hosten zoals <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> of <a href="http://projet.idleman.fr/leed/">Leed</a>. Het gebruikt weinig systeembronnen en is makkelijk te administreren terwijl het een krachtig en makkelijk te configureren programma is.',
+		'freshrss_description' => 'FreshRSS is een RSS feed aggregator om zelf te hosten zoals <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> of <a href="http://leed.idleman.fr/">Leed</a>. Het gebruikt weinig systeembronnen en is makkelijk te administreren terwijl het een krachtig en makkelijk te configureren programma is.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">op Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">op Github</a>',
 		'license' => 'License',
 		'license' => 'License',
 		'project_website' => 'Project website',
 		'project_website' => 'Project website',
@@ -57,6 +57,6 @@ return array(
 	),
 	),
 	'share' => 'Delen',
 	'share' => 'Delen',
 	'tag' => array(
 	'tag' => array(
-		'related' => 'Verwante labels',
+		'related' => 'Verwante labels',	//TODO
 	),
 	),
 );
 );

+ 1 - 0
app/i18n/nl/sub.php

@@ -27,6 +27,7 @@ return array(
 			'password' => 'HTTP wachtwoord',
 			'password' => 'HTTP wachtwoord',
 			'username' => 'HTTP gebruikers naam',
 			'username' => 'HTTP gebruikers naam',
 		),
 		),
+		'clear_cache' => 'Always clear cache',	//TODO
 		'css_help' => 'Haalt verstoorde RSS feeds op (attentie, heeft meer tijd nodig!)',
 		'css_help' => 'Haalt verstoorde RSS feeds op (attentie, heeft meer tijd nodig!)',
 		'css_path' => 'Artikelen CSS pad op originele website',
 		'css_path' => 'Artikelen CSS pad op originele website',
 		'description' => 'Omschrijving',
 		'description' => 'Omschrijving',

+ 1 - 1
app/i18n/pt-br/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => 'Linha inferior',
 			'bottom_line' => 'Linha inferior',
 			'entry' => 'Ícones de artigos',
 			'entry' => 'Ícones de artigos',
 			'publication_date' => 'Data da publicação',
 			'publication_date' => 'Data da publicação',
-			'related_tags' => 'Tags relacionadas',
+			'related_tags' => 'Tags relacionadas',	//TODO
 			'sharing' => 'Compartilhar',
 			'sharing' => 'Compartilhar',
 			'top_line' => 'Linha superior',
 			'top_line' => 'Linha superior',
 		),
 		),

+ 1 - 1
app/i18n/pt-br/gen.php

@@ -180,7 +180,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => 'Atencão!',
 		'attention' => 'Atencão!',
 		'blank_to_disable' => 'Deixe em branco para desativar',
 		'blank_to_disable' => 'Deixe em branco para desativar',
-		'by_author' => 'Por <em>%s</em>',
+		'by_author' => 'Por:',
 		'by_default' => 'Por padrão',
 		'by_default' => 'Por padrão',
 		'damn' => 'Buumm!',
 		'damn' => 'Buumm!',
 		'default_category' => 'Sem categoria',
 		'default_category' => 'Sem categoria',

+ 2 - 2
app/i18n/pt-br/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Reportar Bugs',
 		'bugs_reports' => 'Reportar Bugs',
 		'credits' => 'Créditos',
 		'credits' => 'Créditos',
 		'credits_content' => 'Alguns elementos de design vieram do <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> Embora FreshRRS não utiliza este framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Ícones</a> vieram do <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police foi criada por <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS é baseado no <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, um framework PHP.',
 		'credits_content' => 'Alguns elementos de design vieram do <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> Embora FreshRRS não utiliza este framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Ícones</a> vieram do <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police foi criada por <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS é baseado no <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, um framework PHP.',
-		'freshrss_description' => 'FreshRSS é um RSS feeds aggregator para um host próprio como o <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://projet.idleman.fr/leed/">Leed</a>. É leve e fácil de utilizar enquanto é uma ferramenta poderosa e configurável. ',
+		'freshrss_description' => 'FreshRSS é um RSS feeds aggregator para um host próprio como o <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://leed.idleman.fr/">Leed</a>. É leve e fácil de utilizar enquanto é uma ferramenta poderosa e configurável. ',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">no Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">no Github</a>',
 		'license' => 'licença',
 		'license' => 'licença',
 		'project_website' => 'Site do projeto',
 		'project_website' => 'Site do projeto',
@@ -57,6 +57,6 @@ return array(
 	),
 	),
 	'share' => 'Compartilhar',
 	'share' => 'Compartilhar',
 	'tag' => array(
 	'tag' => array(
-		'related' => 'Tags relacionadas',
+		'related' => 'Tags relacionadas',	//TODO
 	),
 	),
 );
 );

+ 1 - 0
app/i18n/pt-br/sub.php

@@ -27,6 +27,7 @@ return array(
 			'password' => 'Senha HTTP',
 			'password' => 'Senha HTTP',
 			'username' => 'Usuário HTTP',
 			'username' => 'Usuário HTTP',
 		),
 		),
+		'clear_cache' => 'Always clear cache',	//TODO
 		'css_help' => 'Retorna RSS feeds truncados (atenção, requer mais tempo!)',
 		'css_help' => 'Retorna RSS feeds truncados (atenção, requer mais tempo!)',
 		'css_path' => 'Caminho do CSS do artigo no site original',
 		'css_path' => 'Caminho do CSS do artigo no site original',
 		'description' => 'Descrição',
 		'description' => 'Descrição',

+ 1 - 1
app/i18n/ru/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => 'Bottom line',
 			'bottom_line' => 'Bottom line',
 			'entry' => 'Article icons',
 			'entry' => 'Article icons',
 			'publication_date' => 'Date of publication',
 			'publication_date' => 'Date of publication',
-			'related_tags' => 'Related tags',
+			'related_tags' => 'Related tags',	//TODO
 			'sharing' => 'Sharing',
 			'sharing' => 'Sharing',
 			'top_line' => 'Top line',
 			'top_line' => 'Top line',
 		),
 		),

+ 1 - 1
app/i18n/ru/gen.php

@@ -181,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => 'Warning!',
 		'attention' => 'Warning!',
 		'blank_to_disable' => 'Leave blank to disable',
 		'blank_to_disable' => 'Leave blank to disable',
-		'by_author' => 'By <em>%s</em>',
+		'by_author' => 'By:',
 		'by_default' => 'By default',
 		'by_default' => 'By default',
 		'damn' => 'Damn!',
 		'damn' => 'Damn!',
 		'default_category' => 'Uncategorized',
 		'default_category' => 'Uncategorized',

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

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Bugs reports',
 		'bugs_reports' => 'Bugs reports',
 		'credits' => 'Credits',
 		'credits' => 'Credits',
 		'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn’t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police has been created by <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
 		'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn’t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police has been created by <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
-		'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="http://projet.idleman.fr/leed/">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="http://leed.idleman.fr/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
 		'license' => 'License',
 		'license' => 'License',
 		'project_website' => 'Project website',
 		'project_website' => 'Project website',
@@ -57,6 +57,6 @@ return array(
 	),
 	),
 	'share' => 'Share',
 	'share' => 'Share',
 	'tag' => array(
 	'tag' => array(
-		'related' => 'Related tags',
+		'related' => 'Article tags',	//TODO
 	),
 	),
 );
 );

+ 1 - 0
app/i18n/ru/sub.php

@@ -27,6 +27,7 @@ return array(
 			'password' => 'HTTP password',// TODO
 			'password' => 'HTTP password',// TODO
 			'username' => 'HTTP username',// TODO
 			'username' => 'HTTP username',// TODO
 		),
 		),
+		'clear_cache' => 'Always clear cache',	//TODO
 		'css_help' => 'Retrieves truncated RSS feeds (caution, requires more time!)',// TODO
 		'css_help' => 'Retrieves truncated RSS feeds (caution, requires more time!)',// TODO
 		'css_path' => 'Articles CSS path on original website',// TODO
 		'css_path' => 'Articles CSS path on original website',// TODO
 		'description' => 'Description',// TODO
 		'description' => 'Description',// TODO

+ 1 - 1
app/i18n/tr/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => 'Alt çizgi',
 			'bottom_line' => 'Alt çizgi',
 			'entry' => 'Makale ikonları',
 			'entry' => 'Makale ikonları',
 			'publication_date' => 'Yayınlama Tarihi',
 			'publication_date' => 'Yayınlama Tarihi',
-			'related_tags' => 'İlgili etiketler',
+			'related_tags' => 'İlgili etiketler',	//TODO
 			'sharing' => 'Paylaşım',
 			'sharing' => 'Paylaşım',
 			'top_line' => 'Üst çizgi',
 			'top_line' => 'Üst çizgi',
 		),
 		),

+ 1 - 1
app/i18n/tr/gen.php

@@ -181,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => 'Tehlike!',
 		'attention' => 'Tehlike!',
 		'blank_to_disable' => 'Devredışı bırakmak için boş bırakın',
 		'blank_to_disable' => 'Devredışı bırakmak için boş bırakın',
-		'by_author' => '<em>%s</em> tarafından',
+		'by_author' => 'Tarafından:',
 		'by_default' => 'Öntanımlı',
 		'by_default' => 'Öntanımlı',
 		'damn' => 'Hay aksi!',
 		'damn' => 'Hay aksi!',
 		'default_category' => 'Kategorisiz',
 		'default_category' => 'Kategorisiz',

+ 3 - 2
app/i18n/tr/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Hata raporu',
 		'bugs_reports' => 'Hata raporu',
 		'credits' => 'Tanıtım',
 		'credits' => 'Tanıtım',
 		'credits_content' => 'Bu frameworkü kullanmamasına rağmen FreshRSS bazı tasarım ögelerini <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> dan almıştır. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">İkonlar</a> <a href="https://www.gnome.org/">GNOME projesinden</a> alınmıştır. <em>Open Sans</em> yazı tipi <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> tarafından oluşturulmuştur. FreshRSS bir PHP framework olan <a href="https://github.com/marienfressinaud/MINZ">Minz</a> i temel alır.',
 		'credits_content' => 'Bu frameworkü kullanmamasına rağmen FreshRSS bazı tasarım ögelerini <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> dan almıştır. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">İkonlar</a> <a href="https://www.gnome.org/">GNOME projesinden</a> alınmıştır. <em>Open Sans</em> yazı tipi <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> tarafından oluşturulmuştur. FreshRSS bir PHP framework olan <a href="https://github.com/marienfressinaud/MINZ">Minz</a> i temel alır.',
-		'freshrss_description' => 'FreshRSS <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> veya <a href="http://projet.idleman.fr/leed/">Leed</a> gibi kendi hostunuzda çalışan bir RSS akış toplayıcısıdır. Güçlü ve yapılandırılabilir araçlarıyla basit ve kullanımı kolay bir uygulamadır.',
+		'freshrss_description' => 'FreshRSS <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> veya <a href="http://leed.idleman.fr/">Leed</a> gibi kendi hostunuzda çalışan bir RSS akış toplayıcısıdır. Güçlü ve yapılandırılabilir araçlarıyla basit ve kullanımı kolay bir uygulamadır.',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github sayfası</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github sayfası</a>',
 		'license' => 'Lisans',
 		'license' => 'Lisans',
 		'project_website' => 'Proje sayfası',
 		'project_website' => 'Proje sayfası',
@@ -53,10 +53,11 @@ return array(
 		'starred' => 'Favorileri göster',
 		'starred' => 'Favorileri göster',
 		'stats' => 'İstatistikler',
 		'stats' => 'İstatistikler',
 		'subscription' => 'Abonelik yönetimi',
 		'subscription' => 'Abonelik yönetimi',
+		'tags' => 'My labels',	//TODO
 		'unread' => 'Okunmamışları göster',
 		'unread' => 'Okunmamışları göster',
 	),
 	),
 	'share' => 'Share',
 	'share' => 'Share',
 	'tag' => array(
 	'tag' => array(
-		'related' => 'İlgili etiketler',
+		'related' => 'İlgili etiketler',	//TODO
 	),
 	),
 );
 );

+ 1 - 0
app/i18n/tr/sub.php

@@ -27,6 +27,7 @@ return array(
 			'password' => 'HTTP şifre',
 			'password' => 'HTTP şifre',
 			'username' => 'HTTP kullanıcı adı',
 			'username' => 'HTTP kullanıcı adı',
 		),
 		),
+		'clear_cache' => 'Always clear cache',	//TODO
 		'css_help' => 'Dikkat, daha çok zaman gerekir!',
 		'css_help' => 'Dikkat, daha çok zaman gerekir!',
 		'css_path' => 'Makaleleri kendi CSS görünümü ile göster',
 		'css_path' => 'Makaleleri kendi CSS görünümü ile göster',
 		'description' => 'Tanım',
 		'description' => 'Tanım',

+ 1 - 1
app/i18n/zh-cn/conf.php

@@ -19,7 +19,7 @@ return array(
 			'bottom_line' => '底栏',
 			'bottom_line' => '底栏',
 			'entry' => '文章图标',
 			'entry' => '文章图标',
 			'publication_date' => '更新日期',
 			'publication_date' => '更新日期',
-			'related_tags' => '相关标签',
+			'related_tags' => '相关标签',	//TODO
 			'sharing' => '分享',
 			'sharing' => '分享',
 			'top_line' => '顶栏',
 			'top_line' => '顶栏',
 		),
 		),

+ 1 - 1
app/i18n/zh-cn/gen.php

@@ -181,7 +181,7 @@ return array(
 	'short' => array(
 	'short' => array(
 		'attention' => '警告!',
 		'attention' => '警告!',
 		'blank_to_disable' => '留空以禁用',
 		'blank_to_disable' => '留空以禁用',
-		'by_author' => '作者 <em>%s</em>',
+		'by_author' => '作者',
 		'by_default' => '默认',
 		'by_default' => '默认',
 		'damn' => '错误!',
 		'damn' => '错误!',
 		'default_category' => '未分类',
 		'default_category' => '未分类',

+ 3 - 2
app/i18n/zh-cn/index.php

@@ -7,7 +7,7 @@ return array(
 		'bugs_reports' => 'Bug 报告',
 		'bugs_reports' => 'Bug 报告',
 		'credits' => '致谢',
 		'credits' => '致谢',
 		'credits_content' => '某些设计元素来自于 <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> ,尽管 FreshRSS 并没有使用此框架。<a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">图标</a> 来自于 <a href="https://www.gnome.org/">GNOME 项目</a>。<em>Open Sans</em> 字体出自 <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> 之手。FreshRSS 基于 PHP 框架 <a href="https://github.com/marienfressinaud/MINZ">Minz</a>。',
 		'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> 字体出自 <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> 之手。FreshRSS 基于 PHP 框架 <a href="https://github.com/marienfressinaud/MINZ">Minz</a>。',
-		'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 或 <a href="http://projet.idleman.fr/leed/">Leed</a>。 它不仅轻快又易用,而且强大又易于配置。',
+		'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 或 <a href="http://leed.idleman.fr/">Leed</a>。 它不仅轻快又易用,而且强大又易于配置。',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github Issues</a>',
 		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github Issues</a>',
 		'license' => '授权',
 		'license' => '授权',
 		'project_website' => '项目网站',
 		'project_website' => '项目网站',
@@ -53,10 +53,11 @@ return array(
 		'starred' => '显示收藏',
 		'starred' => '显示收藏',
 		'stats' => '统计',
 		'stats' => '统计',
 		'subscription' => '订阅管理',
 		'subscription' => '订阅管理',
+		'tags' => 'My labels',	//TODO
 		'unread' => '显示未读',
 		'unread' => '显示未读',
 	),
 	),
 	'share' => '分享',
 	'share' => '分享',
 	'tag' => array(
 	'tag' => array(
-		'related' => '相关标签',
+		'related' => '相关标签',	//TODO
 	),
 	),
 );
 );

+ 1 - 0
app/i18n/zh-cn/sub.php

@@ -27,6 +27,7 @@ return array(
 			'password' => 'HTTP 密码',
 			'password' => 'HTTP 密码',
 			'username' => 'HTTP 用户名',
 			'username' => 'HTTP 用户名',
 		),
 		),
+		'clear_cache' => 'Always clear cache',	//TODO
 		'css_help' => '用于获取全文(注意,这将耗费更多时间!)',
 		'css_help' => '用于获取全文(注意,这将耗费更多时间!)',
 		'css_path' => '原文的 CSS 选择器',
 		'css_path' => '原文的 CSS 选择器',
 		'description' => '描述',
 		'description' => '描述',

+ 3 - 3
app/install.php

@@ -343,13 +343,13 @@ function checkDbUser(&$dbOptions) {
 	try {
 	try {
 		$c = new PDO($str, $dbOptions['user'], $dbOptions['password'], $driver_options);
 		$c = new PDO($str, $dbOptions['user'], $dbOptions['password'], $driver_options);
 		if (defined('SQL_CREATE_TABLES')) {
 		if (defined('SQL_CREATE_TABLES')) {
-			$sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP . SQL_INSERT_FEEDS,
+			$sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP . SQL_CREATE_TABLE_TAGS . SQL_INSERT_FEEDS,
 				$dbOptions['prefix_user'], _t('gen.short.default_category'));
 				$dbOptions['prefix_user'], _t('gen.short.default_category'));
 			$stm = $c->prepare($sql);
 			$stm = $c->prepare($sql);
 			$ok = $stm && $stm->execute();
 			$ok = $stm && $stm->execute();
 		} else {
 		} else {
-			global $SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_INSERT_FEEDS;
-			$instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_INSERT_FEEDS);
+			global $SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS, $SQL_INSERT_FEEDS;
+			$instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS, $SQL_INSERT_FEEDS);
 			$ok = !empty($instructions);
 			$ok = !empty($instructions);
 			foreach ($instructions as $instruction) {
 			foreach ($instructions as $instruction) {
 				$sql = sprintf($instruction, $dbOptions['prefix_user'], _t('gen.short.default_category'));
 				$sql = sprintf($instruction, $dbOptions['prefix_user'], _t('gen.short.default_category'));

+ 35 - 0
app/layout/aside_feed.phtml

@@ -34,6 +34,30 @@
 			</div>
 			</div>
 		</li>
 		</li>
 
 
+		<?php
+			$t_active = FreshRSS_Context::isCurrentGet('T');
+		?>
+		<li class="tree-folder category tags<?php echo $t_active ? ' active' : ''; ?>">
+			<div class="tree-folder-title">
+				<a class="dropdown-toggle" href="#"><?php echo _i($t_active ? 'up' : 'down'); ?></a>
+				<a class="title" data-unread="<?php echo format_number($this->nbUnreadTags); ?>" href="<?php echo _url('index', 'index', 'get', 'T'); ?>"><?php echo _t('index.menu.tags'); ?></a>
+			</div>
+			<ul class="tree-folder-items<?php echo $t_active ? ' active' : ''; ?>">
+				<?php
+					foreach ($this->tags as $tag):
+				?>
+				<li id="t_<?php echo $tag->id(); ?>" class="item feed<?php echo FreshRSS_Context::isCurrentGet('t_' . $tag->id()) ? ' active' : ''; ?>" data-unread="<?php echo $tag->nbUnread(); ?>">
+					<div class="dropdown no-mobile">
+						<div class="dropdown-target"></div>
+						<a class="dropdown-toggle"><?php echo _i('configure'); ?></a>
+						<?php /* tag_config_template */ ?>
+					</div>
+					<?php echo FreshRSS_Themes::alt('label'); ?> <a class="item-title" data-unread="<?php echo format_number($tag->nbUnread()); ?>" href="<?php echo _url('index', 'index', 'get', 't_' . $tag->id()); ?>"><?php echo $tag->name(); ?></a>
+				</li>
+				<?php endforeach; ?>
+			</ul>
+		</li>
+
 		<?php
 		<?php
 			foreach ($this->categories as $cat) {
 			foreach ($this->categories as $cat) {
 				$feeds = $cat->feeds();
 				$feeds = $cat->feeds();
@@ -72,6 +96,17 @@
 	</form>
 	</form>
 </div>
 </div>
 
 
+<script id="tag_config_template" type="text/html">
+	<ul class="dropdown-menu">
+		<li class="dropdown-close"><a href="#close">❌</a></li>
+		<li class="item">
+			<button class="as-link confirm" disabled="disabled"
+				form="mark-read-aside" formaction="<?php echo _url('tag', 'delete', 'id_tag', '------'); ?>"
+				type="submit"><?php echo _t('gen.action.remove'); ?></button>
+		</li>
+	</ul>
+</script>
+
 <script id="feed_config_template" type="text/html">
 <script id="feed_config_template" type="text/html">
 	<ul class="dropdown-menu">
 	<ul class="dropdown-menu">
 		<li class="dropdown-close"><a href="#close">❌</a></li>
 		<li class="dropdown-close"><a href="#close">❌</a></li>

+ 13 - 21
app/layout/layout.phtml

@@ -11,10 +11,6 @@
 		<?php echo self::headScript(); ?>
 		<?php echo self::headScript(); ?>
 		<link rel="shortcut icon" id="favicon" type="image/x-icon" sizes="16x16 64x64" href="<?php echo Minz_Url::display('/favicon.ico'); ?>" />
 		<link rel="shortcut icon" id="favicon" type="image/x-icon" sizes="16x16 64x64" href="<?php echo Minz_Url::display('/favicon.ico'); ?>" />
 		<link rel="icon msapplication-TileImage apple-touch-icon" type="image/png" sizes="256x256" href="<?php echo Minz_Url::display('/themes/icons/favicon-256.png'); ?>" />
 		<link rel="icon msapplication-TileImage apple-touch-icon" type="image/png" sizes="256x256" href="<?php echo Minz_Url::display('/themes/icons/favicon-256.png'); ?>" />
-		<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('starred', true); ?>" />
-		<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('non-starred', true); ?>" />
-		<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('read', true); ?>" />
-		<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('unread', true); ?>" />
 		<link rel="apple-touch-icon" href="<?php echo Minz_Url::display('/themes/icons/apple-touch-icon.png'); ?>" />
 		<link rel="apple-touch-icon" href="<?php echo Minz_Url::display('/themes/icons/apple-touch-icon.png'); ?>" />
 		<meta name="apple-mobile-web-app-capable" content="yes" />
 		<meta name="apple-mobile-web-app-capable" content="yes" />
 		<meta name="apple-mobile-web-app-status-bar-style" content="black" />
 		<meta name="apple-mobile-web-app-status-bar-style" content="black" />
@@ -22,24 +18,11 @@
 		<meta name="msapplication-TileColor" content="#FFF" />
 		<meta name="msapplication-TileColor" content="#FFF" />
 <?php if (!FreshRSS_Context::$system_conf->allow_referrer) { ?>
 <?php if (!FreshRSS_Context::$system_conf->allow_referrer) { ?>
 		<meta name="referrer" content="never" />
 		<meta name="referrer" content="never" />
-<?php
-	}
-	flush();
-	if (isset($this->callbackBeforeContent)) {
-		call_user_func($this->callbackBeforeContent, $this);
-	}
-?>
+<?php } ?>
 		<?php echo self::headTitle(); ?>
 		<?php echo self::headTitle(); ?>
 <?php
 <?php
 	$url_base = Minz_Request::currentRequest();
 	$url_base = Minz_Request::currentRequest();
-	if (FreshRSS_Context::$next_id !== '') {
-		$url_next = $url_base;
-		$url_next['params']['next'] = FreshRSS_Context::$next_id;
-		$url_next['params']['ajax'] = 1;
-?>
-		<link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display($url_next); ?>" />
-<?php
-	} if (isset($this->rss_title)) {
+	if (isset($this->rss_title)) {
 		$url_rss = $url_base;
 		$url_rss = $url_base;
 		$url_rss['a'] = 'rss';
 		$url_rss['a'] = 'rss';
 		if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) {
 		if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) {
@@ -54,10 +37,19 @@
 <?php } ?>
 <?php } ?>
 	</head>
 	</head>
 	<body class="<?php echo Minz_Request::actionName(); ?>">
 	<body class="<?php echo Minz_Request::actionName(); ?>">
-<?php $this->partial('header'); ?>
+<?php
+	flush();
+	$this->partial('header');
+?>
 
 
 <div id="global">
 <div id="global">
-	<?php $this->render(); ?>
+	<?php
+		flush();
+		if (isset($this->callbackBeforeFeeds)) {
+			call_user_func($this->callbackBeforeFeeds, $this);
+		}
+		$this->render();
+	?>
 </div>
 </div>
 
 
 <?php
 <?php

+ 3 - 3
app/views/configure/display.phtml

@@ -39,7 +39,7 @@
 								<?php } ?>
 								<?php } ?>
 							</div>
 							</div>
 							<div class="properties">
 							<div class="properties">
-								<div><?php echo sprintf('%s — %s', $theme['name'], _t('gen.short.by_author', $theme['author'])); ?></div>
+								<div><?php echo sprintf('%s — %s %s', $theme['name'], _t('gen.short.by_author'), $theme['author']); ?></div>
 								<div><?php echo $theme['description'] ?></div>
 								<div><?php echo $theme['description'] ?></div>
 								<div class="page-number"><?php echo sprintf('%d/%d', $i, $slides) ?></div>
 								<div class="page-number"><?php echo sprintf('%d/%d', $i, $slides) ?></div>
 							</div>
 							</div>
@@ -79,8 +79,8 @@
 						<th> </th>
 						<th> </th>
 						<th title="<?php echo _t('gen.action.mark_read'); ?>"><?php echo _i('read'); ?></th>
 						<th title="<?php echo _t('gen.action.mark_read'); ?>"><?php echo _i('read'); ?></th>
 						<th title="<?php echo _t('gen.action.mark_favorite'); ?>"><?php echo _i('bookmark'); ?></th>
 						<th title="<?php echo _t('gen.action.mark_favorite'); ?>"><?php echo _i('bookmark'); ?></th>
-						<th><?php echo _t('conf.display.icon.sharing'); ?></th>
 						<th><?php echo _t('conf.display.icon.related_tags'); ?></th>
 						<th><?php echo _t('conf.display.icon.related_tags'); ?></th>
+						<th><?php echo _t('conf.display.icon.sharing'); ?></th>
 						<th><?php echo _t('conf.display.icon.publication_date'); ?></th>
 						<th><?php echo _t('conf.display.icon.publication_date'); ?></th>
 						<th><?php echo _i('link'); ?></th>
 						<th><?php echo _i('link'); ?></th>
 					</tr>
 					</tr>
@@ -98,8 +98,8 @@
 						<th><?php echo _t('conf.display.icon.bottom_line'); ?></th>
 						<th><?php echo _t('conf.display.icon.bottom_line'); ?></th>
 						<td><input type="checkbox" name="bottomline_read" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_read ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_read; ?>"/></td>
 						<td><input type="checkbox" name="bottomline_read" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_read ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_read; ?>"/></td>
 						<td><input type="checkbox" name="bottomline_favorite" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_favorite ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_favorite; ?>"/></td>
 						<td><input type="checkbox" name="bottomline_favorite" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_favorite ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_favorite; ?>"/></td>
-						<td><input type="checkbox" name="bottomline_sharing" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_sharing ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_sharing; ?>"/></td>
 						<td><input type="checkbox" name="bottomline_tags" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_tags ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_tags; ?>"/></td>
 						<td><input type="checkbox" name="bottomline_tags" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_tags ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_tags; ?>"/></td>
+						<td><input type="checkbox" name="bottomline_sharing" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_sharing ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_sharing; ?>"/></td>
 						<td><input type="checkbox" name="bottomline_date" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_date ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_date; ?>"/></td>
 						<td><input type="checkbox" name="bottomline_date" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_date ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_date; ?>"/></td>
 						<td><input type="checkbox" name="bottomline_link" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_link ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_link; ?>"/></td>
 						<td><input type="checkbox" name="bottomline_link" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_link ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_link; ?>"/></td>
 					</tr>
 					</tr>

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff