Selaa lähdekoodia

Merge pull request #1902 from FreshRSS/dev

FreshRSS 1.11.0
Alexandre Alapetite 7 vuotta sitten
vanhempi
commit
c0122003fe
100 muutettua tiedostoa jossa 948 lisäystä ja 372 poistoa
  1. 51 1
      CHANGELOG.md
  2. 8 3
      Docker/Dockerfile
  3. 1 1
      Docker/FreshRSS.Apache.conf
  4. 72 18
      Docker/README.md
  5. 38 0
      Docker/docker-compose.yml
  6. 12 0
      Docker/entrypoint.sh
  7. 38 10
      README.fr.md
  8. 34 14
      README.md
  9. 1 1
      app/Controllers/entryController.php
  10. 12 7
      app/Controllers/feedController.php
  11. 13 8
      app/Controllers/importExportController.php
  12. 1 1
      app/Controllers/indexController.php
  13. 19 5
      app/Controllers/subscriptionController.php
  14. 27 20
      app/Controllers/userController.php
  15. 55 0
      app/Models/BooleanSearch.php
  16. 22 4
      app/Models/CategoryDAO.php
  17. 1 1
      app/Models/DatabaseDAO.php
  18. 2 1
      app/Models/Entry.php
  19. 102 86
      app/Models/EntryDAO.php
  20. 21 57
      app/Models/EntryDAOSQLite.php
  21. 7 1
      app/Models/Factory.php
  22. 56 9
      app/Models/Feed.php
  23. 69 11
      app/Models/FeedDAO.php
  24. 17 0
      app/Models/FeedDAOSQLite.php
  25. 1 1
      app/Models/UserQuery.php
  26. 1 0
      app/SQL/install.sql.mysql.php
  27. 2 1
      app/SQL/install.sql.pgsql.php
  28. 1 0
      app/SQL/install.sql.sqlite.php
  29. 1 0
      app/actualize_script.php
  30. 2 0
      app/i18n/cz/conf.php
  31. 3 3
      app/i18n/cz/feedback.php
  32. 1 0
      app/i18n/cz/gen.php
  33. 2 0
      app/i18n/cz/sub.php
  34. 3 3
      app/i18n/de/admin.php
  35. 11 9
      app/i18n/de/conf.php
  36. 5 5
      app/i18n/de/feedback.php
  37. 2 1
      app/i18n/de/gen.php
  38. 15 13
      app/i18n/de/sub.php
  39. 4 2
      app/i18n/en/conf.php
  40. 3 3
      app/i18n/en/feedback.php
  41. 1 0
      app/i18n/en/gen.php
  42. 2 0
      app/i18n/en/sub.php
  43. 2 0
      app/i18n/es/conf.php
  44. 3 3
      app/i18n/es/feedback.php
  45. 1 0
      app/i18n/es/gen.php
  46. 2 0
      app/i18n/es/sub.php
  47. 2 0
      app/i18n/fr/conf.php
  48. 1 1
      app/i18n/fr/feedback.php
  49. 1 0
      app/i18n/fr/gen.php
  50. 2 0
      app/i18n/fr/sub.php
  51. 2 0
      app/i18n/he/conf.php
  52. 3 3
      app/i18n/he/feedback.php
  53. 1 0
      app/i18n/he/gen.php
  54. 2 0
      app/i18n/he/sub.php
  55. 2 0
      app/i18n/it/conf.php
  56. 1 1
      app/i18n/it/feedback.php
  57. 1 0
      app/i18n/it/gen.php
  58. 2 0
      app/i18n/it/sub.php
  59. 2 0
      app/i18n/kr/conf.php
  60. 3 3
      app/i18n/kr/feedback.php
  61. 1 0
      app/i18n/kr/gen.php
  62. 2 0
      app/i18n/kr/sub.php
  63. 3 3
      app/i18n/nl/admin.php
  64. 14 12
      app/i18n/nl/conf.php
  65. 5 5
      app/i18n/nl/feedback.php
  66. 2 1
      app/i18n/nl/gen.php
  67. 8 6
      app/i18n/nl/sub.php
  68. 2 0
      app/i18n/pt-br/conf.php
  69. 3 3
      app/i18n/pt-br/feedback.php
  70. 1 0
      app/i18n/pt-br/gen.php
  71. 2 0
      app/i18n/pt-br/sub.php
  72. 2 0
      app/i18n/ru/conf.php
  73. 3 3
      app/i18n/ru/feedback.php
  74. 1 0
      app/i18n/ru/gen.php
  75. 2 0
      app/i18n/ru/sub.php
  76. 2 0
      app/i18n/tr/conf.php
  77. 3 3
      app/i18n/tr/feedback.php
  78. 1 0
      app/i18n/tr/gen.php
  79. 1 1
      app/i18n/tr/install.php
  80. 2 0
      app/i18n/tr/sub.php
  81. 2 0
      app/i18n/zh-cn/conf.php
  82. 3 3
      app/i18n/zh-cn/feedback.php
  83. 1 0
      app/i18n/zh-cn/gen.php
  84. 2 0
      app/i18n/zh-cn/sub.php
  85. 1 1
      app/install.php
  86. 3 3
      app/layout/aside_subscription.phtml
  87. 6 0
      app/shares.php
  88. 1 1
      app/views/auth/formLogin.phtml
  89. 1 1
      app/views/auth/register.phtml
  90. 3 3
      app/views/configure/queries.phtml
  91. 1 1
      app/views/feed/add.phtml
  92. 1 1
      app/views/helpers/export/articles.phtml
  93. 50 1
      app/views/helpers/feed/update.phtml
  94. 3 0
      app/views/helpers/index/normal/entry_bottom.phtml
  95. 1 1
      app/views/subscription/index.phtml
  96. 1 1
      app/views/user/manage.phtml
  97. 2 2
      app/views/user/profile.phtml
  98. 1 0
      cli/README.md
  99. 1 1
      cli/create-user.php
  100. 32 4
      cli/i18n/I18nData.php

+ 51 - 1
CHANGELOG.md

@@ -1,5 +1,55 @@
 # FreshRSS changelog
 
+## 2018-06-03 FreshRSS 1.11.0
+
+* API
+	* Add support for Fever compatible API, enabling more clients [#1406](https://github.com/FreshRSS/FreshRSS/pull/1406)
+		* iOS: [Fiery Feeds](https://itunes.apple.com/app/fiery-feeds-rss-reader/id1158763303), [Unread](https://itunes.apple.com/app/unread-rss-reader/id1252376153)
+		* MacOS: [Readkit](https://itunes.apple.com/app/readkit/id588726889)
+* Features
+	* Several per-feed options (implemented in JSON) [#1838](https://github.com/FreshRSS/FreshRSS/pull/1838)
+		* Mark updated articles as read [#891](https://github.com/FreshRSS/FreshRSS/issues/891)
+		* Mark as read upon reception [#1702](https://github.com/FreshRSS/FreshRSS/issues/1702)
+		* Only for admin user [#1905](https://github.com/FreshRSS/FreshRSS/pull/1905)
+			* Feed cURL timeout
+			* Ignore SSL (unsafe) [#1811](https://github.com/FreshRSS/FreshRSS/issues/1811)
+	* Light Boolean search implementation [#879](https://github.com/FreshRSS/FreshRSS/issues/879)
+		* All parts are implicitly `AND` (which must not be written), except if `OR` is stated.
+		* No use of parentheses. Support for quotes to disable the Boolean search, like `"This or that"`.
+		* Example: `Hello intitle:World OR date:P1D example OR author:Else intitle:"This or that"`
+	* Share with Pocket [#1884](https://github.com/FreshRSS/FreshRSS/issues/1884)
+* Deployment
+	* Includes an optional cron daemon in Docker to refresh feeds automatically [#1869](https://github.com/FreshRSS/FreshRSS/issues/1869)
+	* Docker Compose example [#1882](https://github.com/FreshRSS/FreshRSS/pull/1882)
+* Bug fixing
+	* Fix Docker bug affecting Apache `CustomLog` (unwanted local copy of access logs), `ErrorLog`, `Listen` (IPv6 bug) [#1873](https://github.com/FreshRSS/FreshRSS/pull/1873)
+	* Fix muted feeds that were not actually muted [#1844](https://github.com/FreshRSS/FreshRSS/issues/1844)
+	* Fix null exception in shares, showing only the first article [#1824](https://github.com/FreshRSS/FreshRSS/issues/1824)
+	* Fix error during import [#1890](https://github.com/FreshRSS/FreshRSS/issues/1890)
+		* Fix additional automatic sequence bug with PostgreSQL [#1907](https://github.com/FreshRSS/FreshRSS/pull/1907)
+	* Fix errors in case of empty/wrong username when updating user settings [#1857](https://github.com/FreshRSS/FreshRSS/pull/1857)
+	* Fixes in subscription menu [#1858](https://github.com/FreshRSS/FreshRSS/pull/1858)
+	* Fix allowing Unix sockets for MySQL and PostgreSQL [#1888](https://github.com/FreshRSS/FreshRSS/issues/1888)
+	* Fix `create-user` CLI option `no_default_feeds` [#1900](https://github.com/FreshRSS/FreshRSS/pull/1900)
+* SimplePie
+	* Work-around for feeds with invalid non-unique GUIDs [#1887](https://github.com/FreshRSS/FreshRSS/pull/1887)
+	* Fix for Atom feeds using a namespace for type [#1892](https://github.com/FreshRSS/FreshRSS/issues/1892)
+	* Remove some warnings during parsing attemps of some bad feeds [#1909](https://github.com/FreshRSS/FreshRSS/pull/1909)
+* Security
+	* Strip HTTP credentials from HTTP Referer in SimplePie [#1891](https://github.com/FreshRSS/FreshRSS/pull/1891)
+	* Use `autocomplete="new-password"` to prevent form autocomplete in user management pages (fix bug with e.g. Firefox) [#1877](https://github.com/FreshRSS/FreshRSS/pull/1877)
+* UI
+	* Add tooltips on user queries [#1823](https://github.com/FreshRSS/FreshRSS/pull/1823)
+* I18n
+	* Improve i18n tools [#1829](https://github.com/FreshRSS/FreshRSS/pull/1829)
+	* Updated German [#1856](https://github.com/FreshRSS/FreshRSS/pull/1856)
+	* Updated Dutch [#1903](https://github.com/FreshRSS/FreshRSS/pull/1903)
+* Misc.
+	* Use cURL for fetching full articles content [#1870](https://github.com/FreshRSS/FreshRSS/issues/1870)
+	* Add error log information when SQLite has not enough temp space [#1816](https://github.com/FreshRSS/FreshRSS/issues/1816)
+	* Allow extension dir to be a symlink [#1911](https://github.com/FreshRSS/FreshRSS/pull/1911)
+
+
 ## 2018-03-09 FreshRSS 1.10.2 (Docker only)
 
 * Bug fixing
@@ -31,7 +81,7 @@
 ## 2018-02-24 FreshRSS 1.10.0
 
 * API
-	* Add compatibility with FeedMe 3.5.3+ on Android [#1774](https://github.com/FreshRSS/FreshRSS/pull/1774)
+	* Add compatibility with [FeedMe](https://play.google.com/store/apps/details?id=com.seazon.feedme) 3.5.3+ on Android [#1774](https://github.com/FreshRSS/FreshRSS/pull/1774)
 * Features
 	* Ability to pause feeds, and to hide them from categories [#1750](https://github.com/FreshRSS/FreshRSS/pull/1750)
 	* Ability for the admin to reset a user’s password [#960](https://github.com/FreshRSS/FreshRSS/issues/960)

+ 8 - 3
Docker/Dockerfile

@@ -15,8 +15,13 @@ WORKDIR ${FRESHRSS_ROOT}
 COPY . ${FRESHRSS_ROOT}
 COPY ./Docker/*.Apache.conf /etc/apache2/conf.d/
 
+RUN sed -r -i "/^[ ]*(CustomLog|ErrorLog|Listen) /s/^/#/" /etc/apache2/httpd.conf && \
+	echo "17,37 * * * * php ${FRESHRSS_ROOT}/app/actualize_script.php 2>&1 | tee /tmp/FreshRSS.log" >> \
+		/var/spool/cron/crontabs/root
+
+ENV CRON_MIN ''
+ENTRYPOINT ["./Docker/entrypoint.sh"]
+
 EXPOSE 80
-CMD php -f ./cli/prepare.php > /dev/null && \
-	chown -R :www-data ${FRESHRSS_ROOT} && \
-	chmod -R g+r ${FRESHRSS_ROOT} && chmod -R g+w ${FRESHRSS_ROOT}/data/ && \
+CMD ([ -z "$CRON_MIN" ] || crond -d 6) && \
 	exec httpd -D FOREGROUND

+ 1 - 1
Docker/FreshRSS.Apache.conf

@@ -17,8 +17,8 @@
 ServerName freshrss.localhost
 Listen 0.0.0.0:80
 DocumentRoot /var/www/FreshRSS/p/
+CustomLog /dev/stdout combined
 ErrorLog /dev/stderr
-TransferLog /dev/stdout
 AllowEncodedSlashes On
 
 <Directory /var/www/FreshRSS/p>

+ 72 - 18
Docker/README.md

@@ -26,7 +26,7 @@ sudo docker build --tag freshrss/freshrss -f Docker/Dockerfile .
 
 ## Run FreshRSS
 
-Example using SQLite, and exposing FreshRSS on port 8080. You may have to adapt the network parameters to fit your needs.
+Example using SQLite, built-in cron, and exposing FreshRSS on port 8080. You may have to adapt the parameters to fit your needs.
 
 ```sh
 # You can optionally run from the directory containing the FreshRSS source code:
@@ -35,10 +35,11 @@ cd ./FreshRSS/
 # The data will be saved on the host in `./data/`
 mkdir -p ./data/
 
-sudo docker run -dit --restart unless-stopped --log-opt max-size=10m \
-	-v $(pwd)/data:/var/www/FreshRSS/data \
-	-p 8080:80 \
-	--name freshrss freshrss/freshrss
+sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+  -v $(pwd)/data:/var/www/FreshRSS/data \
+  -e 'CRON_MIN=5,35' \
+  -p 8080:80 \
+  --name freshrss freshrss/freshrss
 ```
 
 ### Examples with external databases
@@ -50,10 +51,11 @@ See https://hub.docker.com/_/mysql/
 
 ```sh
 sudo docker run -d -v /path/to/mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=rootpass -e MYSQL_DATABASE=freshrss -e MYSQL_USER=freshrss -e MYSQL_PASSWORD=pass --name mysql mysql
-sudo docker run -dit --restart unless-stopped --log-opt max-size=10m \
-	-v $(pwd)/data:/var/www/FreshRSS/data \
-	--link mysql -p 8080:80 \
-	--name freshrss freshrss/freshrss
+sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+  -v $(pwd)/data:/var/www/FreshRSS/data \
+  -e 'CRON_MIN=17,47' \
+  --link mysql -p 8080:80 \
+  --name freshrss freshrss/freshrss
 ```
 
 #### PostgreSQL
@@ -61,10 +63,11 @@ See https://hub.docker.com/_/postgres/
 
 ```sh
 sudo docker run -d -v /path/to/pgsql-data:/var/lib/postgresql/data -e POSTGRES_DB=freshrss -e POSTGRES_USER=freshrss -e POSTGRES_PASSWORD=pass --name postgres postgres
-sudo docker run -dit --restart unless-stopped --log-opt max-size=10m \
-	-v $(pwd)/data:/var/www/FreshRSS/data \
-	--link postgres -p 8080:80 \
-	--name freshrss freshrss/freshrss
+sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+  -v $(pwd)/data:/var/www/FreshRSS/data \
+  -e 'CRON_MIN=23,53' \
+  --link postgres -p 8080:80 \
+  --name freshrss freshrss/freshrss
 ```
 
 ## Update
@@ -72,7 +75,7 @@ sudo docker run -dit --restart unless-stopped --log-opt max-size=10m \
 ```sh
 # Rebuild an image (see build section above) or get a new online version:
 sudo docker pull freshrss/freshrss
-# And then 
+# And then
 sudo docker stop freshrss
 sudo docker rename freshrss freshrss_old
 # See the run section above for the full command
@@ -89,16 +92,50 @@ sudo docker exec --user apache -it freshrss php ./cli/list-users.php
 
 See the [CLI documentation](../cli/) for all the other commands.
 
-### Cron job to refresh feeds
+## Cron job to automatically refresh feeds
+We recommend a refresh rate of about twice per hour (see *WebSub* / *PubSubHubbub* for real-time updates).
+There is no less than 3 options. Pick a single one.
+
+### Option 1) Cron inside the FreshRSS Docker image
+Easiest, built-in solution, also used in the examples above
+(but your Docker instance will have a second process in the background, without monitoring).
+Just pass the environment variable `CRON_MIN` to your `docker run` command,
+containing a valid cron minute definition such as `'13,43'` (recommended) or `'*/20'`.
+Not passing the `CRON_MIN` environment variable – or setting it to empty string – will disable the cron daemon.
+
+```sh
+sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+  -v $(pwd)/data:/var/www/FreshRSS/data \
+  -e 'CRON_MIN=13,43' \
+  -p 8080:80 \
+  --name freshrss freshrss/freshrss
+```
+
+### Option 2) Cron on the host machine
+Traditional solution.
 Set a cron job up on your host machine, calling the `actualize_script.php` inside the FreshRSS Docker instance.
+Remember not pass the `CRON_MIN` environment variable to your Docker run, to avoid running the built-in cron daemon of option 1.
 
-#### Example on Debian / Ubuntu
-Create `/etc/cron.d/FreshRSS` with:
+Example on Debian / Ubuntu: Create `/etc/cron.d/FreshRSS` with:
 
 ```
 7,37 * * * * root docker exec --user apache -it freshrss php ./app/actualize_script.php > /tmp/FreshRSS.log 2>&1
 ```
 
+### Option 3) Cron as another instance of the same FreshRSS Docker image
+For advanced users. Offers good logging and monitoring with auto-restart on failure.
+Watch out to use the same run parameters than in your main FreshRSS instance, for database, networking, and file system.
+See cron option 1 for customising the cron schedule.
+
+```sh
+sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+  -v $(pwd)/data:/var/www/FreshRSS/data \
+  -e 'CRON_MIN=17,37' \
+  --name freshrss_cron freshrss/freshrss \
+  crond -f -d 6
+```
+
+
 ## Debugging
 
 ```sh
@@ -115,5 +152,22 @@ ls /var/www/FreshRSS/
 
 ## Deployment in production
 
-Use a reverse proxy on your host server, such as [Træfik](https://traefik.io/) or [nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/),
+Use a reverse proxy on your host server, such as [Træfik](https://traefik.io/)
+or [nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/),
 with HTTPS, for instance using [Let’s Encrypt](https://letsencrypt.org/).
+
+### Example with [docker-compose](https://docs.docker.com/compose/)
+
+A [docker-compose.yml](docker-compose.yml) file is given as an example, using PostgreSQL. In order to use it, you have to adapt:
+- In the `postgresql` service:
+	* the `volumes` section. Be careful to keep the path `/var/lib/postgresql/data` for the container. If the path is wrong, you will not get any error but your db will be gone at the next run;
+	* the `POSTGRES_PASSWORD` in the `environment` section;
+- In the `freshrss` service:
+	* the `volumes` section;
+	* options under the `labels` section are specific to [Træfik](https://traefik.io/), a reverse proxy. If you are not using it, feel free to delete this section. If you are using it, adapt accordingly to your config, especially the `traefik.frontend.rule` option.
+	* the `environment` section to adapt the strategy to update feeds.
+
+You can then launch the stack (postgres + freshrss) with:
+```sh
+docker-compose up -d
+```

+ 38 - 0
Docker/docker-compose.yml

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

+ 12 - 0
Docker/entrypoint.sh

@@ -0,0 +1,12 @@
+#!/bin/sh
+
+php -f ./cli/prepare.php > /dev/null
+
+chown -R :www-data .
+chmod -R g+r . && chmod -R g+w ./data/
+
+if [ -n "$CRON_MIN" ]; then
+	sed -r -i "/FreshRSS/s/^[^ ]+ /$CRON_MIN /" /var/spool/cron/crontabs/root
+fi
+
+exec "$@"

+ 38 - 10
README.fr.md

@@ -1,3 +1,6 @@
+[![Build Status][travis-badge]][travis-link]
+
+* Lire ce document sur [github.com/FreshRSS/FreshRSS/](https://github.com/FreshRSS/FreshRSS/blob/master/README.md) pour avoir les images et liens corrects.
 * [English version](README.md)
 
 # FreshRSS
@@ -54,6 +57,8 @@ Nous sommes une communauté amicale.
 6. Des paramètres de configuration avancée peuvent être vues dans [config.default.php](config.default.php) et modifiées dans `data/config.php`.
 7. Avec Apache, activer [`AllowEncodedSlashes`](https://httpd.apache.org/docs/trunk/mod/core.html#allowencodedslashes) pour une meilleure compatibilité avec les clients mobiles.
 
+Plus d’informations sur l’installation et la configuration serveur peuvent être trouvées dans [notre documentation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.md).
+
 ## Installation automatisée
 * [Docker](./Docker/)
 * [![Cloudron](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=org.freshrss.cloudronapp)
@@ -107,6 +112,8 @@ sudo git pull
 sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
 ```
 
+Voir la [documentation de la ligne de commande](cli/README.md) pour plus de détails.
+
 ## Contrôle d’accès
 Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix :
 * En utilisant l’identification par formulaire (requiert JavaScript, et PHP 5.5+ recommandé)
@@ -150,11 +157,41 @@ mysqldump --skip-comments --disable-keys --user=<db_user> --password --host <db_
 ```
 
 
-# Extensions 
+# Extensions
 FreshRSS permet l’ajout d’extensions en plus des fonctionnalités natives.
 Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensions).
 
 
+# APIs et applications natives
+
+FreshRSS supporte l’accès depuis des applications native pour Linux, Android, iOS, et OS X, grâce à deux APIs distinctes.
+
+## Via l’API compatible Google Reader
+
+Voir notre [documentation sur l’accès mobile](https://freshrss.github.io/FreshRSS/fr/users/06_Mobile_access.html).
+
+Tout client supportant une API de type Google Reader ; Sélection :
+
+* Android
+	* [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) avec [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Propriétaire)
+	* [FeedMe 3.5.3+](https://play.google.com/store/apps/details?id=com.seazon.feedme) (Propriétaire)
+	* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Libre, [F-Droid](https://f-droid.org/fr/packages/org.freshrss.easyrss/))
+* GNU/Linux
+	* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre)
+
+## Via l’API compatible Fever
+
+Voir notre [documentation sur l’API Fever](https://freshrss.github.io/FreshRSS/fr/users/06_Fever_API.html) page.
+
+Tout client supportant une API de type Fever ; Sélection :
+
+* iOS
+	* [Fiery Feeds](https://itunes.apple.com/app/fiery-feeds-rss-reader/id1158763303) (Propriétaire)
+	* [Unread](https://itunes.apple.com/app/unread-rss-reader/id1252376153) (Propriétaire)
+* MacOS
+	* [Readkit](https://itunes.apple.com/app/readkit/id588726889) (Propriétaire)
+
+
 # Bibliothèques incluses
 * [SimplePie](https://simplepie.org/)
 * [MINZ](https://github.com/marienfressinaud/MINZ)
@@ -174,12 +211,3 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio
 * [password_compat](https://github.com/ircmaxell/password_compat)
 
 
-# [Clients compatibles](https://freshrss.github.io/FreshRSS/fr/users/06_Mobile_access.html)
-Tout client supportant une API de type Google Reader. Sélection :
-
-* Android
-	* [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) avec [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Propriétaire)
-	* [FeedMe 3.5.3+](https://play.google.com/store/apps/details?id=com.seazon.feedme) (Propriétaire)
-	* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Libre, [F-Droid](https://f-droid.org/fr/packages/org.freshrss.easyrss/))
-* GNU/Linux
-	* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre)

+ 34 - 14
README.md

@@ -57,7 +57,7 @@ We are a friendly community.
 6. Advanced configuration settings can be seen in [config.default.php](config.default.php) and modified in `data/config.php`.
 7. When using Apache, enable [`AllowEncodedSlashes`](https://httpd.apache.org/docs/trunk/mod/core.html#allowencodedslashes) for better compatibility with mobile clients.
 
-More information about installation and server configuration can be found in [our documentation](https://freshrss.github.io/FreshRSS/en/admins/02_Installation.html). 
+More information about installation and server configuration can be found in [our documentation](https://freshrss.github.io/FreshRSS/en/admins/02_Installation.html).
 
 ## Automated install
 * [Docker](./Docker/)
@@ -111,6 +111,7 @@ cd /usr/share/FreshRSS
 sudo git pull
 sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
 ```
+
 See more commands and git commands in the [Command-Line Interface documentation](cli/README.md).
 
 ## Access control
@@ -156,9 +157,39 @@ mysqldump --skip-comments --disable-keys --user=<db_user> --password --host <db_
 ```
 
 
-# Extensions 
+# Extensions
 FreshRSS supports further customizations by adding extensions on top of its core functionality.
-See the [repository dedicated to those extensions](https://github.com/FreshRSS/Extensions). 
+See the [repository dedicated to those extensions](https://github.com/FreshRSS/Extensions).
+
+
+# APIs & native apps
+
+FreshRSS supports access from native apps for Linux, Android, iOS, and OS X, via two distinct APIs.
+
+## Google Reader-like API
+
+There is more information available about our Google Reader compatible API on the page [mobile access](https://freshrss.github.io/FreshRSS/en/users/06_Mobile_access.html).
+
+Supported clients are:
+
+* Android
+	* [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) with [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Closed source)
+	* [FeedMe 3.5.3+](https://play.google.com/store/apps/details?id=com.seazon.feedme) (Closed source)
+	* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Open source, [F-Droid](https://f-droid.org/packages/org.freshrss.easyrss/))
+* GNU/Linux
+	* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source)
+
+## Fever API
+
+See our [Fever API documentation](https://freshrss.github.io/FreshRSS/en/users/06_Fever_API.html) page.
+
+Supported clients are:
+
+* iOS
+	* [Fiery Feeds](https://itunes.apple.com/app/fiery-feeds-rss-reader/id1158763303) (Closed source)
+	* [Unread](https://itunes.apple.com/app/unread-rss-reader/id1252376153) (Closed source)
+* MacOS
+	* [Readkit](https://itunes.apple.com/app/readkit/id588726889) (Closed source)
 
 
 # Included libraries
@@ -179,16 +210,5 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E
 * [Services_JSON](https://pear.php.net/pepr/pepr-proposal-show.php?id=198)
 * [password_compat](https://github.com/ircmaxell/password_compat)
 
-
-# [Compatible clients](https://freshrss.github.io/FreshRSS/en/users/06_Mobile_access.html)
-Any client supporting a Google Reader-like API. Selection:
-
-* Android
-	* [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) with [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Closed source)
-	* [FeedMe 3.5.3+](https://play.google.com/store/apps/details?id=com.seazon.feedme) (Closed source)
-	* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Open source, [F-Droid](https://f-droid.org/packages/org.freshrss.easyrss/))
-* GNU/Linux
-	* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source)
-
 [travis-badge]:https://travis-ci.org/FreshRSS/FreshRSS.svg?branch=master
 [travis-link]:https://travis-ci.org/FreshRSS/FreshRSS

+ 1 - 1
app/Controllers/entryController.php

@@ -40,7 +40,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 		$get = Minz_Request::param('get');
 		$next_get = Minz_Request::param('nextGet', $get);
 		$id_max = Minz_Request::param('idMax', 0);
-		FreshRSS_Context::$search = new FreshRSS_Search(Minz_Request::param('search', ''));
+		FreshRSS_Context::$search = new FreshRSS_BooleanSearch(Minz_Request::param('search', ''));
 
 		FreshRSS_Context::$state = Minz_Request::param('state', 0);
 		if (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_FAVORITE)) {

+ 12 - 7
app/Controllers/feedController.php

@@ -84,6 +84,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			'description' => $feed->description(),
 			'lastUpdate' => time(),
 			'httpAuth' => $feed->httpAuth(),
+			'attributes' => array(),
 		);
 
 		$id = $feedDAO->addFeed($values);
@@ -271,7 +272,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 
 		$updated_feeds = 0;
 		$nb_new_articles = 0;
-		$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
 		foreach ($feeds as $feed) {
 			$url = $feed->url();	//For detection of HTTP 301
 
@@ -284,10 +284,10 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			}
 
 			$mtime = 0;
-			$ttl = $feed->ttl();
-			if ($ttl < FreshRSS_Feed::TTL_DEFAULT) {
+			if ($feed->mute()) {
 				continue;	//Feed refresh is disabled
 			}
+			$ttl = $feed->ttl();
 			if ((!$simplePiePush) && (!$feed_id) &&
 				($feed->lastUpdate() + 10 >= time() - ($ttl == FreshRSS_Feed::TTL_DEFAULT ? FreshRSS_Context::$user_conf->ttl_default : $ttl))) {
 				//Too early to refresh from source, but check whether the feed was updated by another user
@@ -353,8 +353,10 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 						} else {	//This entry already exists but has been updated
 							//Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() .
 								//', old hash ' . $existingHash . ', new hash ' . $entry->hash());
-							//TODO: Make an updated/is_read policy by feed, in addition to the global one.
-							$needFeedCacheRefresh = FreshRSS_Context::$user_conf->mark_updated_article_unread;
+							$mark_updated_article_unread = $feed->attributes('mark_updated_article_unread') !== null ? (
+									$feed->attributes('mark_updated_article_unread')
+								) : FreshRSS_Context::$user_conf->mark_updated_article_unread;
+							$needFeedCacheRefresh = $mark_updated_article_unread;
 							$entry->_isRead(FreshRSS_Context::$user_conf->mark_updated_article_unread ? false : null);	//Change is_read according to policy.
 							if (!$entryDAO->inTransaction()) {
 								$entryDAO->beginTransaction();
@@ -365,15 +367,18 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 						// This entry should not be added considering configuration and date.
 						$oldGuids[] = $entry->guid();
 					} else {
+						$read_upon_reception = $feed->attributes('read_upon_reception') !== null ? (
+								$feed->attributes('read_upon_reception')
+							) : FreshRSS_Context::$user_conf->mark_when['reception'];
 						if ($isNewFeed) {
 							$id = min(time(), $entry_date) . uSecString();
-							$entry->_isRead($is_read);
+							$entry->_isRead($read_upon_reception);
 						} elseif ($entry_date < $date_min) {
 							$id = min(time(), $entry_date) . uSecString();
 							$entry->_isRead(true);	//Old article that was not in database. Probably an error, so mark as read
 						} else {
 							$id = uTimeString();
-							$entry->_isRead($is_read);
+							$entry->_isRead($read_upon_reception);
 						}
 						$entry->_id($id);
 

+ 13 - 8
app/Controllers/importExportController.php

@@ -390,6 +390,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$article_to_feed = array();
 
 		$nb_feeds = count($this->feedDAO->listFeeds());
+		$newFeedGuids = array();
 		$limits = FreshRSS_Context::$system_conf->limits;
 
 		// First, we check feeds of articles are in DB (and add them if needed).
@@ -417,21 +418,25 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 			if ($feed != null) {
 				$article_to_feed[$item['id']] = $feed->id();
+				if (!isset($newFeedGuids['f_' . $feed->id()])) {
+					$newFeedGuids['f_' . $feed->id()] = array();
+				}
+				$newFeedGuids['f_' . $feed->id()][] = safe_ascii($item['id']);
 			}
 		}
 
-		$newGuids = array();
-		foreach ($article_object['items'] as $item) {
-			$newGuids[] = safe_ascii($item['id']);
+		// For each feed, check existing GUIDs already in database.
+		$existingHashForGuids = array();
+		foreach ($newFeedGuids as $feedId => $newGuids) {
+			$existingHashForGuids[$feedId] = $this->entryDAO->listHashForFeedGuids(substr($feedId, 2), $newGuids);
 		}
-		// For this feed, check existing GUIDs already in database.
-		$existingHashForGuids = $this->entryDAO->listHashForFeedGuids($feed->id(), $newGuids);
-		$newGuids = array();
+		unset($newFeedGuids);
 
 		// Then, articles are imported.
+		$newGuids = array();
 		$this->entryDAO->beginTransaction();
 		foreach ($article_object['items'] as $item) {
-			if (!isset($article_to_feed[$item['id']])) {
+			if (empty($article_to_feed[$item['id']])) {
 				// Related feed does not exist for this entry, do nothing.
 				continue;
 			}
@@ -468,7 +473,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 			$values = $entry->toArray();
 			$ok = false;
-			if (isset($existingHashForGuids[$entry->guid()])) {
+			if (isset($existingHashForGuids['f_' . $feed_id][$entry->guid()])) {
 				$ok = $this->entryDAO->updateEntry($values);
 			} else {
 				$ok = $this->entryDAO->addEntry($values);

+ 1 - 1
app/Controllers/indexController.php

@@ -182,7 +182,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 			FreshRSS_Context::$state |= FreshRSS_Entry::STATE_READ;
 		}
 
-		FreshRSS_Context::$search = new FreshRSS_Search(Minz_Request::param('search', ''));
+		FreshRSS_Context::$search = new FreshRSS_BooleanSearch(Minz_Request::param('search', ''));
 		FreshRSS_Context::$order = Minz_Request::param(
 			'order', FreshRSS_Context::$user_conf->sort_order
 		);

+ 19 - 5
app/Controllers/subscriptionController.php

@@ -15,7 +15,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 		}
 
 		$catDAO = new FreshRSS_CategoryDAO();
-		$feedDAO = new FreshRSS_FeedDAO();
+		$feedDAO = FreshRSS_Factory::createFeedDao();
 
 		$catDAO->checkDefault();
 		$feedDAO->updateTTL();
@@ -74,9 +74,10 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 			return;
 		}
 
-		$this->view->feed = $this->view->feeds[$id];
+		$feed = $this->view->feeds[$id];
+		$this->view->feed = $feed;
 
-		Minz_View::prependTitle(_t('sub.title.feed_management') . ' · ' . $this->view->feed->name() . ' · ');
+		Minz_View::prependTitle(_t('sub.title.feed_management') . ' · ' . $feed->name() . ' · ');
 
 		if (Minz_Request::isPost()) {
 			$user = trim(Minz_Request::param('http_user_feed' . $id, ''));
@@ -95,6 +96,18 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 				$ttl = FreshRSS_Context::$user_conf->ttl_default;
 			}
 
+			$feed->_attributes('mark_updated_article_unread', Minz_Request::paramTernary('mark_updated_article_unread'));
+			$feed->_attributes('read_upon_reception', Minz_Request::paramTernary('read_upon_reception'));
+
+			if (FreshRSS_Auth::hasAccess('admin')) {
+				$feed->_attributes('ssl_verify', Minz_Request::paramTernary('ssl_verify'));
+				$timeout = intval(Minz_Request::param('timeout', 0));
+				$feed->_attributes('timeout', $timeout > 0 ? $timeout : null);
+			} else {
+				$feed->_attributes('ssl_verify', null);
+				$feed->_attributes('timeout', null);
+			}
+
 			$values = array(
 				'name' => Minz_Request::param('name', ''),
 				'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
@@ -106,14 +119,15 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 				'httpAuth' => $httpAuth,
 				'keep_history' => intval(Minz_Request::param('keep_history', FreshRSS_Feed::KEEP_HISTORY_DEFAULT)),
 				'ttl' => $ttl * ($mute ? -1 : 1),
+				'attributes' => $feed->attributes()
 			);
 
 			invalidateHttpCache();
 
 			$url_redirect = array('c' => 'subscription', 'params' => array('id' => $id));
 			if ($feedDAO->updateFeed($id, $values) !== false) {
-				$this->view->feed->_category($cat);
-				$this->view->feed->faviconPrepare();
+				$feed->_category($cat);
+				$feed->faviconPrepare();
 
 				Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect);
 			} else {

+ 27 - 20
app/Controllers/userController.php

@@ -44,8 +44,20 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1;
 	}
 
+	public static function deleteFeverKey($username) {
+		$userConfig = get_user_configuration($username);
+		if ($userConfig !== null && ctype_xdigit($userConfig->feverKey)) {
+			return @unlink(DATA_PATH . '/fever/.key-' . sha1(FreshRSS_Context::$system_conf->salt) . '-' . $userConfig->feverKey . '.txt');
+		}
+		return false;
+	}
+
 	public static function updateUser($user, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) {
 		$userConfig = get_user_configuration($user);
+		if ($userConfig === null) {
+			return false;
+		}
+
 		if ($passwordPlain != '') {
 			$passwordHash = self::hashPassword($passwordPlain);
 			$userConfig->passwordHash = $passwordHash;
@@ -54,6 +66,16 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		if ($apiPasswordPlain != '') {
 			$apiPasswordHash = self::hashPassword($apiPasswordPlain);
 			$userConfig->apiPasswordHash = $apiPasswordHash;
+
+			@mkdir(DATA_PATH . '/fever/', 0770, true);
+			self::deleteFeverKey($user);
+			$userConfig->feverKey = strtolower(md5($user . ':' . $apiPasswordPlain));
+			$ok = file_put_contents(DATA_PATH . '/fever/.key-' . sha1(FreshRSS_Context::$system_conf->salt) . '-' . $userConfig->feverKey . '.txt', $user) !== false;
+
+			if (!$ok) {
+				Minz_Log::warning('Could not save API credentials for fever API', ADMIN_LOG);
+				return $ok;
+			}
 		}
 
 		if (is_array($userConfigUpdated)) {
@@ -78,8 +100,8 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 
 			$username = Minz_Request::param('username');
 			$ok = self::updateUser($username, $passwordPlain, $apiPasswordPlain, array(
-					'token' => Minz_Request::param('token', null),
-				));
+				'token' => Minz_Request::param('token', null),
+			));
 
 			if ($ok) {
 				Minz_Request::good(_t('feedback.user.updated', $username),
@@ -168,30 +190,16 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			$configPath = join_path($homeDir, 'config.php');
 			$ok &= !file_exists($configPath);
 		}
-		if ($ok) {
-			$passwordHash = '';
-			if ($passwordPlain != '') {
-				$passwordHash = self::hashPassword($passwordPlain);
-				$ok &= ($passwordHash != '');
-			}
-
-			$apiPasswordHash = '';
-			if ($apiPasswordPlain != '') {
-				$apiPasswordHash = self::hashPassword($apiPasswordPlain);
-				$ok &= ($apiPasswordHash != '');
-			}
-		}
 		if ($ok) {
 			if (!is_dir($homeDir)) {
 				mkdir($homeDir);
 			}
-			$userConfig['passwordHash'] = $passwordHash;
-			$userConfig['apiPasswordHash'] = $apiPasswordHash;
 			$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
 		}
 		if ($ok) {
 			$userDAO = new FreshRSS_UserDAO();
 			$ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds);
+			$ok &= self::updateUser($new_user_name, $passwordPlain, $apiPasswordPlain);
 		}
 		return $ok;
 	}
@@ -246,10 +254,9 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			$ok &= (strcasecmp($username, $default_user) !== 0);	//It is forbidden to delete the default user
 		}
 		$user_data = join_path(DATA_PATH, 'users', $username);
+		$ok &= is_dir($user_data);
 		if ($ok) {
-			$ok &= is_dir($user_data);
-		}
-		if ($ok) {
+			self::deleteFeverKey($username);
 			$userDAO = new FreshRSS_UserDAO();
 			$ok &= $userDAO->deleteUser($username);
 			$ok &= recursive_unlink($user_data);

+ 55 - 0
app/Models/BooleanSearch.php

@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * Contains Boolean search from the search form.
+ */
+class FreshRSS_BooleanSearch {
+
+	private $raw_input = '';
+	private $searches = array();
+
+	public function __construct($input) {
+		$input = trim($input);
+		if ($input == '') {
+			return;
+		}
+		$this->raw_input = $input;
+
+		$input = preg_replace('/:&quot;(.*?)&quot;/', ':"\1"', $input);
+		$splits = preg_split('/\b(OR)\b/i', $input, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+		$segment = '';
+		$ns = count($splits);
+		for ($i = 0; $i < $ns; $i++) {
+			$segment = $segment . $splits[$i];
+			if (trim($segment) == '' || strcasecmp($segment, 'OR') === 0) {
+				$segment = '';
+			} else {
+				$quotes = substr_count($segment, '"') + substr_count($segment, '&quot;');
+				if ($quotes % 2 === 0) {
+					$segment = trim($segment);
+					if ($segment != '') {
+						$this->searches[] = new FreshRSS_Search($segment);
+					}
+					$segment = '';
+				}
+			}
+		}
+		$segment = trim($segment);
+		if ($segment != '') {
+			$this->searches[] = new FreshRSS_Search($segment);
+		}
+	}
+
+	public function searches() {
+		return $this->searches;
+	}
+
+	public function __toString() {
+		return $this->getRawInput();
+	}
+
+	public function getRawInput() {
+		return $this->raw_input;
+	}
+}

+ 22 - 4
app/Models/CategoryDAO.php

@@ -134,7 +134,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		if (isset($cat[0])) {
 			return $cat[0];
 		} else {
-			return false;
+			if (FreshRSS_Context::$isCli) {
+				fwrite(STDERR, 'FreshRSS database error: Default category not found!' . "\n");
+			}
+			Minz_Log::error('FreshRSS database error: Default category not found!');
+			return null;
 		}
 	}
 	public function checkDefault() {
@@ -144,13 +148,27 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 			$cat = new FreshRSS_Category(_t('gen.short.default_category'));
 			$cat->_id(self::DEFAULTCATEGORYID);
 
+			$sql = 'INSERT INTO `' . $this->prefix . 'category`(id, name) VALUES(?, ?)';
+			if (parent::$sharedDbType === 'pgsql') {
+				//Force call to nextval()
+				$sql .= " RETURNING nextval('" . $this->prefix . "category_id_seq');";
+			}
+			$stm = $this->bd->prepare($sql);
+
 			$values = array(
-				'id' => $cat->id(),
-				'name' => $cat->name(),
+				$cat->id(),
+				$cat->name(),
 			);
 
-			$this->addCategory($values);
+			if ($stm && $stm->execute($values)) {
+				return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"');
+			} else {
+				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				Minz_Log::error('SQL error check default category: ' . json_encode($info));
+				return false;
+			}
 		}
+		return true;
 	}
 
 	public function count() {

+ 1 - 1
app/Models/DatabaseDAO.php

@@ -50,7 +50,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 	public function feedIsCorrect() {
 		return $this->checkTable('feed', array(
 			'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate',
-			'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl',
+			'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', 'attributes',
 			'cache_nbEntries', 'cache_nbUnreads'
 		));
 	}

+ 2 - 1
app/Models/Entry.php

@@ -193,7 +193,8 @@ class FreshRSS_Entry extends Minz_Model {
 				try {
 					// l'article n'est pas en BDD, on va le chercher sur le site
 					$this->content = get_content_by_parsing(
-						htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries
+						htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries,
+						$this->feed->attributes()
 					);
 				} catch (Exception $e) {
 					// rien à faire, on garde l'ancien contenu(requête a échoué)

+ 102 - 86
app/Models/EntryDAO.php

@@ -437,7 +437,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 * @param integer $priorityMin
 	 * @return integer affected rows
 	 */
-	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filter = null, $state = 0) {
+	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filters = null, $state = 0) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
@@ -454,7 +454,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		$values = array($idMax);
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -480,7 +480,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 * @param integer $idMax fail safe article ID
 	 * @return integer affected rows
 	 */
-	public function markReadCat($id, $idMax = 0, $filter = null, $state = 0) {
+	public function markReadCat($id, $idMax = 0, $filters = null, $state = 0) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
@@ -492,7 +492,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			 . 'WHERE f.category=? AND e.is_read=0 AND e.id <= ?';
 		$values = array($id, $idMax);
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -518,7 +518,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 * @param integer $idMax fail safe article ID
 	 * @return integer affected rows
 	 */
-	public function markReadFeed($id_feed, $idMax = 0, $filter = null, $state = 0) {
+	public function markReadFeed($id_feed, $idMax = 0, $filters = null, $state = 0) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
@@ -531,7 +531,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			 . 'WHERE id_feed=? AND is_read=0 AND id <= ?';
 		$values = array($id_feed, $idMax);
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -625,7 +625,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return 'CONCAT(' . $s1 . ',' . $s2 . ')';	//MySQL
 	}
 
-	protected function sqlListEntriesWhere($alias = '', $filter = null, $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $firstId = '', $date_min = 0) {
+	protected function sqlListEntriesWhere($alias = '', $filters = null, $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $firstId = '', $date_min = 0) {
 		$search = ' ';
 		$values = array();
 		if ($state & FreshRSS_Entry::STATE_NOT_READ) {
@@ -650,10 +650,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			default:
 				throw new FreshRSS_EntriesGetter_Exception('Bad order in Entry->listByType: [' . $order . ']!');
 		}
-		/*if ($firstId === '' && parent::$sharedDbType === 'mysql') {
-			//MySQL optimization. TODO: check if this is needed again, after the filtering for old articles has been removed in 0.9-dev
-			$firstId = $order === 'DESC' ? '9000000000'. '000000' : '0';
-		}*/
 		if ($firstId !== '') {
 			$search .= 'AND ' . $alias . 'id ' . ($order === 'DESC' ? '<=' : '>=') . ' ? ';
 			$values[] = $firstId;
@@ -662,91 +658,111 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$search .= 'AND ' . $alias . 'id >= ? ';
 			$values[] = $date_min . '000000';
 		}
-		if ($filter) {
-			if ($filter->getMinDate()) {
-				$search .= 'AND ' . $alias . 'id >= ? ';
-				$values[] = "{$filter->getMinDate()}000000";
-			}
-			if ($filter->getMaxDate()) {
-				$search .= 'AND ' . $alias . 'id <= ? ';
-				$values[] = "{$filter->getMaxDate()}000000";
-			}
-			if ($filter->getMinPubdate()) {
-				$search .= 'AND ' . $alias . 'date >= ? ';
-				$values[] = $filter->getMinPubdate();
-			}
-			if ($filter->getMaxPubdate()) {
-				$search .= 'AND ' . $alias . 'date <= ? ';
-				$values[] = $filter->getMaxPubdate();
-			}
+		if ($filters && count($filters->searches()) > 0) {
+			$isOpen = false;
+			foreach ($filters->searches() as $filter) {
+				if ($filter == null) {
+					continue;
+				}
+				$sub_search = '';
+				if ($filter->getMinDate()) {
+					$sub_search .= 'AND ' . $alias . 'id >= ? ';
+					$values[] = "{$filter->getMinDate()}000000";
+				}
+				if ($filter->getMaxDate()) {
+					$sub_search .= 'AND ' . $alias . 'id <= ? ';
+					$values[] = "{$filter->getMaxDate()}000000";
+				}
+				if ($filter->getMinPubdate()) {
+					$sub_search .= 'AND ' . $alias . 'date >= ? ';
+					$values[] = $filter->getMinPubdate();
+				}
+				if ($filter->getMaxPubdate()) {
+					$sub_search .= 'AND ' . $alias . 'date <= ? ';
+					$values[] = $filter->getMaxPubdate();
+				}
 
-			if ($filter->getAuthor()) {
-				foreach ($filter->getAuthor() as $author) {
-					$search .= 'AND ' . $alias . 'author LIKE ? ';
-					$values[] = "%{$author}%";
+				if ($filter->getAuthor()) {
+					foreach ($filter->getAuthor() as $author) {
+						$sub_search .= 'AND ' . $alias . 'author LIKE ? ';
+						$values[] = "%{$author}%";
+					}
 				}
-			}
-			if ($filter->getIntitle()) {
-				foreach ($filter->getIntitle() as $title) {
-					$search .= 'AND ' . $alias . 'title LIKE ? ';
-					$values[] = "%{$title}%";
+				if ($filter->getIntitle()) {
+					foreach ($filter->getIntitle() as $title) {
+						$sub_search .= 'AND ' . $alias . 'title LIKE ? ';
+						$values[] = "%{$title}%";
+					}
 				}
-			}
-			if ($filter->getTags()) {
-				foreach ($filter->getTags() as $tag) {
-					$search .= 'AND ' . $alias . 'tags LIKE ? ';
-					$values[] = "%{$tag}%";
+				if ($filter->getTags()) {
+					foreach ($filter->getTags() as $tag) {
+						$sub_search .= 'AND ' . $alias . 'tags LIKE ? ';
+						$values[] = "%{$tag}%";
+					}
 				}
-			}
-			if ($filter->getInurl()) {
-				foreach ($filter->getInurl() as $url) {
-					$search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? ';
-					$values[] = "%{$url}%";
+				if ($filter->getInurl()) {
+					foreach ($filter->getInurl() as $url) {
+						$sub_search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? ';
+						$values[] = "%{$url}%";
+					}
 				}
-			}
 
-			if ($filter->getNotAuthor()) {
-				foreach ($filter->getNotAuthor() as $author) {
-					$search .= 'AND (NOT ' . $alias . 'author LIKE ?) ';
-					$values[] = "%{$author}%";
+				if ($filter->getNotAuthor()) {
+					foreach ($filter->getNotAuthor() as $author) {
+						$sub_search .= 'AND (NOT ' . $alias . 'author LIKE ?) ';
+						$values[] = "%{$author}%";
+					}
 				}
-			}
-			if ($filter->getNotIntitle()) {
-				foreach ($filter->getNotIntitle() as $title) {
-					$search .= 'AND (NOT ' . $alias . 'title LIKE ?) ';
-					$values[] = "%{$title}%";
+				if ($filter->getNotIntitle()) {
+					foreach ($filter->getNotIntitle() as $title) {
+						$sub_search .= 'AND (NOT ' . $alias . 'title LIKE ?) ';
+						$values[] = "%{$title}%";
+					}
 				}
-			}
-			if ($filter->getNotTags()) {
-				foreach ($filter->getNotTags() as $tag) {
-					$search .= 'AND (NOT ' . $alias . 'tags LIKE ?) ';
-					$values[] = "%{$tag}%";
+				if ($filter->getNotTags()) {
+					foreach ($filter->getNotTags() as $tag) {
+						$sub_search .= 'AND (NOT ' . $alias . 'tags LIKE ?) ';
+						$values[] = "%{$tag}%";
+					}
 				}
-			}
-			if ($filter->getNotInurl()) {
-				foreach ($filter->getNotInurl() as $url) {
-					$search .= 'AND (NOT CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ?) ';
-					$values[] = "%{$url}%";
+				if ($filter->getNotInurl()) {
+					foreach ($filter->getNotInurl() as $url) {
+						$sub_search .= 'AND (NOT CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ?) ';
+						$values[] = "%{$url}%";
+					}
 				}
-			}
 
-			if ($filter->getSearch()) {
-				foreach ($filter->getSearch() as $search_value) {
-					$search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? ';
-					$values[] = "%{$search_value}%";
+				if ($filter->getSearch()) {
+					foreach ($filter->getSearch() as $search_value) {
+						$sub_search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? ';
+						$values[] = "%{$search_value}%";
+					}
 				}
-			}
-			if ($filter->getNotSearch()) {
-				foreach ($filter->getNotSearch() as $search_value) {
-					$search .= 'AND (NOT ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ?) ';
-					$values[] = "%{$search_value}%";
+				if ($filter->getNotSearch()) {
+					foreach ($filter->getNotSearch() as $search_value) {
+						$sub_search .= 'AND (NOT ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ?) ';
+						$values[] = "%{$search_value}%";
+					}
 				}
+
+				if ($sub_search != '') {
+					if ($isOpen) {
+						$search .= 'OR ';
+					} else {
+						$search .= 'AND (';
+						$isOpen = true;
+					}
+					$search .= '(' . substr($sub_search, 4) . ') ';
+				}
+			}
+			if ($isOpen) {
+				$search .= ') ';
 			}
 		}
 		return array($values, $search);
 	}
 
-	private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
+	private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
 		if (!$state) {
 			$state = FreshRSS_Entry::STATE_ALL;
 		}
@@ -777,7 +793,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
 		}
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state, $order, $firstId, $date_min);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state, $order, $firstId, $date_min);
 
 		return array(array_merge($values, $searchValues),
 			'SELECT e.id FROM `' . $this->prefix . 'entry` e '
@@ -785,11 +801,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			. 'WHERE ' . $where
 			. $search
 			. 'ORDER BY e.id ' . $order
-			. ($limit > 0 ? ' LIMIT ' . $limit : ''));	//TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
+			. ($limit > 0 ? ' LIMIT ' . intval($limit) : ''));	//TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
 	}
 
-	public function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
-		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min);
+	public function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
+		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
 
 		$sql = 'SELECT e0.id, e0.guid, e0.title, e0.author, '
 			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
@@ -805,8 +821,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $stm;
 	}
 
-	public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
-		$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filter, $date_min);
+	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);
 		return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
@@ -827,8 +843,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
-	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {	//For API
-		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min);
+	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {	//For API
+		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
 
 		$stm = $this->bd->prepare($sql);
 		$stm->execute($values);

+ 21 - 57
app/Models/EntryDAOSQLite.php

@@ -7,7 +7,6 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 	}
 
 	protected function autoUpdateDb($errorInfo) {
-		Minz_Log::error('FreshRSS_EntryDAO::autoUpdateDb error: ' . print_r($errorInfo, true));
 		if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entrytmp'")) {
 			$showCreate = $tableInfo->fetchColumn();
 			if (stripos($showCreate, 'entrytmp') === false) {
@@ -27,63 +26,28 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 
 	public function commitNewEntries() {
 		$sql = '
-			CREATE TEMP TABLE `tmp` AS
-				SELECT
-					id,
-					guid,
-					title,
-					author,
-					content,
-					link,
-					date,
-					`lastSeen`,
-					hash, is_read,
-					is_favorite,
-					id_feed,
-					tags
-				FROM `' . $this->prefix . 'entrytmp`
-				ORDER BY date;
-				INSERT OR IGNORE INTO `' . $this->prefix . 'entry`
-					(
-						id,
-						guid,
-						title,
-						author,
-						content,
-						link,
-						date,
-						`lastSeen`,
-						hash,
-						is_read,
-						is_favorite,
-						id_feed,
-						tags
-					)
-				SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS
-					id,
-					guid,
-					title,
-					author,
-					content,
-					link,
-					date,
-					`lastSeen`,
-					hash,
-					is_read,
-					is_favorite,
-					id_feed,
-					tags
-				FROM `tmp`
-				ORDER BY date;
-			DELETE FROM `' . $this->prefix . 'entrytmp`
-			WHERE id <= (SELECT MAX(id)
-			FROM `tmp`);
-			DROP TABLE `tmp`;';
+DROP TABLE IF EXISTS `tmp`;
+CREATE TEMP TABLE `tmp` AS
+	SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
+	FROM `' . $this->prefix . 'entrytmp`
+	ORDER BY date;
+INSERT OR IGNORE INTO `' . $this->prefix . 'entry`
+	(id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
+	SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS id,
+	guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
+	FROM `tmp`
+	ORDER BY date;
+DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= (SELECT MAX(id) FROM `tmp`);
+DROP TABLE IF EXISTS `tmp`;
+';
 		$hadTransaction = $this->bd->inTransaction();
 		if (!$hadTransaction) {
 			$this->bd->beginTransaction();
 		}
 		$result = $this->bd->exec($sql) !== false;
+		if (!$result) {
+			Minz_Log::error('SQL error commitNewEntries: ' . json_encode($this->bd->errorInfo()));
+		}
 		if (!$hadTransaction) {
 			$this->bd->commit();
 		}
@@ -195,7 +159,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 	 * @param integer $priorityMin
 	 * @return integer affected rows
 	 */
-	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filter = null, $state = 0) {
+	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filters = null, $state = 0) {
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
 			Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
@@ -209,7 +173,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 		}
 		$values = array($idMax);
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -235,7 +199,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 	 * @param integer $idMax fail safe article ID
 	 * @return integer affected rows
 	 */
-	public function markReadCat($id, $idMax = 0, $filter = null, $state = 0) {
+	public function markReadCat($id, $idMax = 0, $filters = null, $state = 0) {
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
 			Minz_Log::debug('Calling markReadCat(0) is deprecated!');
@@ -247,7 +211,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 			 . 'id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.category=?)';
 		$values = array($idMax, $id);
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {

+ 7 - 1
app/Models/Factory.php

@@ -3,7 +3,13 @@
 class FreshRSS_Factory {
 
 	public static function createFeedDao($username = null) {
-		return new FreshRSS_FeedDAO($username);
+		$conf = Minz_Configuration::get('system');
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_FeedDAOSQLite($username);
+			default:
+				return new FreshRSS_FeedDAO($username);
+		}
 	}
 
 	public static function createEntryDao($username = null) {

+ 56 - 9
app/Models/Feed.php

@@ -26,6 +26,7 @@ class FreshRSS_Feed extends Minz_Model {
 	private $error = false;
 	private $keep_history = self::KEEP_HISTORY_DEFAULT;
 	private $ttl = self::TTL_DEFAULT;
+	private $attributes = array();
 	private $mute = false;
 	private $hash = null;
 	private $lockPath = '';
@@ -114,6 +115,13 @@ class FreshRSS_Feed extends Minz_Model {
 	public function ttl() {
 		return $this->ttl;
 	}
+	public function attributes($key = '') {
+		if ($key == '') {
+			return $this->attributes;
+		} else {
+			return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
+		}
+	}
 	public function mute() {
 		return $this->mute;
 	}
@@ -234,6 +242,22 @@ class FreshRSS_Feed extends Minz_Model {
 		$this->ttl = abs($value);
 		$this->mute = $value < self::TTL_DEFAULT;
 	}
+
+	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 _nbNotRead($value) {
 		$this->nbNotRead = intval($value);
 	}
@@ -253,7 +277,7 @@ class FreshRSS_Feed extends Minz_Model {
 				if ($this->httpAuth != '') {
 					$url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url);
 				}
-				$feed = customSimplePie();
+				$feed = customSimplePie($this->attributes());
 				if (substr($url, -11) === '#force_feed') {
 					$feed->force_feed(true);
 					$url = substr($url, 0, -11);
@@ -311,6 +335,8 @@ class FreshRSS_Feed extends Minz_Model {
 
 	public function loadEntries($feed) {
 		$entries = array();
+		$guids = array();
+		$hasUniqueGuids = true;
 
 		foreach ($feed->get_items() as $item) {
 			$title = html_only_entity_decode(strip_tags($item->get_title()));
@@ -351,9 +377,13 @@ class FreshRSS_Feed extends Minz_Model {
 				}
 			}
 
+			$guid = $item->get_id(false, false);
+			$hasUniqueGuids &= empty($guids['_' . $guid]);
+			$guids['_' . $guid] = true;
+
 			$entry = new FreshRSS_Entry(
 				$this->id(),
-				$item->get_id(false, false),
+				$guid,
 				$title === null ? '' : $title,
 				$author === null ? '' : html_only_entity_decode(strip_tags($author->name == null ? $author->email : $author->name)),
 				$content === null ? '' : $content,
@@ -368,14 +398,31 @@ class FreshRSS_Feed extends Minz_Model {
 			unset($item);
 		}
 
+		$hasBadGuids = $this->attributes('hasBadGuids');
+		if ($hasBadGuids != !$hasUniqueGuids) {
+			$hasBadGuids = !$hasUniqueGuids;
+			if ($hasBadGuids) {
+				Minz_Log::warning('Feed has invalid GUIDs: ' . $this->url);
+			} else {
+				Minz_Log::warning('Feed has valid GUIDs again: ' . $this->url);
+			}
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			$feedDAO->updateFeedAttribute($this, 'hasBadGuids', $hasBadGuids);
+		}
+		if (!$hasUniqueGuids) {
+			foreach ($entries as $entry) {
+				$entry->_guid('');
+			}
+		}
+
 		$this->entries = $entries;
 	}
 
-	function cacheModifiedTime() {
+	public function cacheModifiedTime() {
 		return @filemtime(CACHE_PATH . '/' . md5($this->url) . '.spc');
 	}
 
-	function lock() {
+	public function lock() {
 		$this->lockPath = TMP_PATH . '/' . $this->hash() . '.freshrss.lock';
 		if (file_exists($this->lockPath) && ((time() - @filemtime($this->lockPath)) > 3600)) {
 			@unlink($this->lockPath);
@@ -388,13 +435,13 @@ class FreshRSS_Feed extends Minz_Model {
 		return true;
 	}
 
-	function unlock() {
+	public function unlock() {
 		@unlink($this->lockPath);
 	}
 
 	//<PubSubHubbub>
 
-	function pubSubHubbubEnabled() {
+	public function pubSubHubbubEnabled() {
 		$url = $this->selfUrl ? $this->selfUrl : $this->url;
 		$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
 		if ($hubFile = @file_get_contents($hubFilename)) {
@@ -407,7 +454,7 @@ class FreshRSS_Feed extends Minz_Model {
 		return false;
 	}
 
-	function pubSubHubbubError($error = true) {
+	public function pubSubHubbubError($error = true) {
 		$url = $this->selfUrl ? $this->selfUrl : $this->url;
 		$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
 		$hubFile = @file_get_contents($hubFilename);
@@ -420,7 +467,7 @@ class FreshRSS_Feed extends Minz_Model {
 		return false;
 	}
 
-	function pubSubHubbubPrepare() {
+	public function pubSubHubbubPrepare() {
 		$key = '';
 		if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl && @is_dir(PSHB_PATH)) {
 			$path = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl);
@@ -467,7 +514,7 @@ class FreshRSS_Feed extends Minz_Model {
 	}
 
 	//Parameter true to subscribe, false to unsubscribe.
-	function pubSubHubbubSubscribe($state) {
+	public function pubSubHubbubSubscribe($state) {
 		$url = $this->selfUrl ? $this->selfUrl : $this->url;
 		if (FreshRSS_Context::$system_conf->base_url && $url) {
 			$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';

+ 69 - 11
app/Models/FeedDAO.php

@@ -1,6 +1,33 @@
 <?php
 
 class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
+
+	protected function addColumn($name) {
+		Minz_Log::warning('FreshRSS_FeedDAO::addColumn: ' . $name);
+		try {
+			if ($name === 'attributes') {	//v1.11.0
+				$stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN attributes TEXT');
+				return $stm && $stm->execute();
+			}
+		} catch (Exception $e) {
+			Minz_Log::error('FreshRSS_FeedDAO::addColumn error: ' . $e->getMessage());
+		}
+		return false;
+	}
+
+	protected function autoUpdateDb($errorInfo) {
+		if (isset($errorInfo[0])) {
+			if ($errorInfo[0] === '42S22' || $errorInfo[0] === '42703') {	//ER_BAD_FIELD_ERROR (Mysql), undefined_column (PostgreSQL)
+				foreach (array('attributes') as $column) {
+					if (stripos($errorInfo[2], $column) !== false) {
+						return $this->addColumn($column);
+					}
+				}
+			}
+		}
+		return false;
+	}
+
 	public function addFeed($valuesTmp) {
 		$sql = '
 			INSERT INTO `' . $this->prefix . 'feed`
@@ -15,10 +42,11 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 					`httpAuth`,
 					error,
 					keep_history,
-					ttl
+					ttl,
+					attributes
 				)
 				VALUES
-				(?, ?, ?, ?, ?, ?, 10, ?, 0, ?, ?)';
+				(?, ?, ?, ?, ?, ?, 10, ?, 0, ?, ?, ?)';
 		$stm = $this->bd->prepare($sql);
 
 		$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
@@ -34,12 +62,16 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			base64_encode($valuesTmp['httpAuth']),
 			FreshRSS_Feed::KEEP_HISTORY_DEFAULT,
 			FreshRSS_Feed::TTL_DEFAULT,
+			isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
 		);
 
 		if ($stm && $stm->execute($values)) {
 			return $this->bd->lastInsertId('"' . $this->prefix . 'feed_id_seq"');
 		} else {
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			if ($this->autoUpdateDb($info)) {
+				return $this->addFeed($valuesTmp);
+			}
 			Minz_Log::error('SQL error addFeed: ' . $info[2]);
 			return false;
 		}
@@ -60,7 +92,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 				'website' => $feed->website(),
 				'description' => $feed->description(),
 				'lastUpdate' => 0,
-				'httpAuth' => $feed->httpAuth()
+				'httpAuth' => $feed->httpAuth(),
+				'attributes' => $feed->attributes(),
 			);
 
 			$id = $this->addFeed($values);
@@ -87,8 +120,10 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		foreach ($valuesTmp as $key => $v) {
 			$set .= '`' . $key . '`=?, ';
 
-			if ($key == 'httpAuth') {
+			if ($key === 'httpAuth') {
 				$valuesTmp[$key] = base64_encode($v);
+			} elseif ($key === 'attributes') {
+				$valuesTmp[$key] = json_encode($v);
 			}
 		}
 		$set = substr($set, 0, -2);
@@ -105,11 +140,25 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			return $stm->rowCount();
 		} else {
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::error('SQL error updateFeed: ' . $info[2]);
+			if ($this->autoUpdateDb($info)) {
+				return $this->updateFeed($id, $valuesTmp);
+			}
+			Minz_Log::error('SQL error updateFeed: ' . $info[2] . ' for feed ' . $id);
 			return false;
 		}
 	}
 
+	public function updateFeedAttribute($feed, $key, $value) {
+		if ($feed instanceof FreshRSS_Feed) {
+			$feed->_attributes($key, $value);
+			return $this->updateFeed(
+					$feed->id(),
+					array('attributes' => $feed->attributes())
+				);
+		}
+		return false;
+	}
+
 	public function updateLastUpdate($id, $inError = false, $mtime = 0) {	//See also updateCachedValue()
 		$sql = 'UPDATE `' . $this->prefix . 'feed` '
 		     . 'SET `lastUpdate`=?, error=? '
@@ -250,17 +299,25 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	/**
 	 * Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL.
 	 */
-	public function listFeedsOrderUpdate($defaultCacheDuration = 3600) {
+	public function listFeedsOrderUpdate($defaultCacheDuration = 3600, $limit = 0) {
 		$this->updateTTL();
-		$sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl '
+		$sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl, attributes '
 		     . 'FROM `' . $this->prefix . 'feed` '
 		     . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT
 		     . ' AND `lastUpdate` < (' . (time() + 60) . '-(CASE WHEN ttl=' . FreshRSS_Feed::TTL_DEFAULT . ' THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ')
-		     . 'ORDER BY `lastUpdate`';
+		     . 'ORDER BY `lastUpdate` '
+		     . ($limit < 1 ? '' : 'LIMIT ' . intval($limit));
 		$stm = $this->bd->prepare($sql);
-		$stm->execute();
-
-		return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
+		if ($stm && $stm->execute()) {
+			return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
+		} else {
+			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			if ($this->autoUpdateDb($info)) {
+				return $this->listFeedsOrderUpdate($defaultCacheDuration);
+			}
+			Minz_Log::error('SQL error listFeedsOrderUpdate: ' . $info[2]);
+			return array();
+		}
 	}
 
 	public function listByCategory($cat) {
@@ -385,6 +442,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$myFeed->_error(isset($dao['error']) ? $dao['error'] : 0);
 			$myFeed->_keepHistory(isset($dao['keep_history']) ? $dao['keep_history'] : FreshRSS_Feed::KEEP_HISTORY_DEFAULT);
 			$myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : FreshRSS_Feed::TTL_DEFAULT);
+			$myFeed->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : '');
 			$myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0);
 			$myFeed->_nbEntries(isset($dao['cache_nbEntries']) ? $dao['cache_nbEntries'] : 0);
 			if (isset($dao['id'])) {

+ 17 - 0
app/Models/FeedDAOSQLite.php

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

+ 1 - 1
app/Models/UserQuery.php

@@ -41,7 +41,7 @@ class FreshRSS_UserQuery {
 			$query['search'] = '';
 		}
 		// linked to deeply with the search object, need to use dependency injection
-		$this->search = new FreshRSS_Search($query['search']);
+		$this->search = new FreshRSS_BooleanSearch($query['search']);
 		if (isset($query['state'])) {
 			$this->state = $query['state'];
 		}

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

@@ -24,6 +24,7 @@ CREATE TABLE IF NOT EXISTS `%1$sfeed` (
 	`error` boolean DEFAULT 0,
 	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,	-- v0.7
 	`ttl` INT NOT NULL DEFAULT 0,	-- v0.7.3
+	`attributes` TEXT,	-- v1.11.0
 	`cache_nbEntries` int DEFAULT 0,	-- v0.7
 	`cache_nbUnreads` int DEFAULT 0,	-- v0.7
 	PRIMARY KEY (`id`),

+ 2 - 1
app/SQL/install.sql.pgsql.php

@@ -22,6 +22,7 @@ $SQL_CREATE_TABLES = array(
 	"error" smallint DEFAULT 0,
 	"keep_history" INT NOT NULL DEFAULT -2,
 	"ttl" INT NOT NULL DEFAULT 0,
+	"attributes" TEXT,	-- v1.11.0
 	"cache_nbEntries" INT DEFAULT 0,
 	"cache_nbUnreads" INT DEFAULT 0,
 	FOREIGN KEY ("category") REFERENCES "%1$scategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE
@@ -51,7 +52,7 @@ $SQL_CREATE_TABLES = array(
 'CREATE INDEX %1$sis_read_index ON "%1$sentry" ("is_read");',
 'CREATE INDEX %1$sentry_lastSeen_index ON "%1$sentry" ("lastSeen");',
 
-'INSERT INTO "%1$scategory" (name) SELECT \'%2$s\' WHERE NOT EXISTS (SELECT id FROM "%1$scategory" WHERE id = 1);',
+'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;

+ 1 - 0
app/SQL/install.sql.sqlite.php

@@ -21,6 +21,7 @@ $SQL_CREATE_TABLES = array(
 	`error` boolean DEFAULT 0,
 	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,
 	`ttl` INT NOT NULL DEFAULT 0,
+	`attributes` TEXT,	-- v1.11.0
 	`cache_nbEntries` int DEFAULT 0,
 	`cache_nbUnreads` int DEFAULT 0,
 	FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,

+ 1 - 0
app/actualize_script.php

@@ -1,3 +1,4 @@
+#!/usr/bin/php
 <?php
 require(__DIR__ . '/../cli/_cli.php');
 

+ 2 - 0
app/i18n/cz/conf.php

@@ -42,6 +42,7 @@ return array(
 	'query' => array(
 		'_' => 'Uživatelské dotazy',
 		'deprecated' => 'Tento dotaz již není platný. Odkazovaná kategorie nebo kanál byly smazány.',
+		'display' => 'Display user query results', // TODO
 		'filter' => 'Filtr aplikován:',
 		'get_all' => 'Zobrazit všechny články',
 		'get_category' => 'Zobrazit "%s" kategorii',
@@ -52,6 +53,7 @@ return array(
 		'number' => 'Dotaz n°%d',
 		'order_asc' => 'Zobrazit nejdříve nejstarší články',
 		'order_desc' => 'Zobrazit nejdříve nejnovější články',
+		'remove' => 'Remove user query', // TODO
 		'search' => 'Hledat "%s"',
 		'state_0' => 'Zobrazit všechny články',
 		'state_1' => 'Zobrazit přečtené články',

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

@@ -31,11 +31,11 @@ return array(
 	'extensions' => array(
 		'already_enabled' => '%s je již zapnut',
 		'disable' => array(
-			'ko' => '%s nelze vypnout. Pro více detailů <a href="%s">zkontrolujte logy FressRSS</a>.',
+			'ko' => '%s nelze vypnout. Pro více detailů <a href="%s">zkontrolujte logy FreshRSS</a>.',
 			'ok' => '%s je nyní vypnut',
 		),
 		'enable' => array(
-			'ko' => '%s nelze zapnout. Pro více detailů <a href="%s">zkontrolujte logy FressRSS</a>.',
+			'ko' => '%s nelze zapnout. Pro více detailů <a href="%s">zkontrolujte logy FreshRSS</a>.',
 			'ok' => '%s je nyní zapnut',
 		),
 		'no_access' => 'Nemáte přístup k %s',
@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => 'Již jste přihlášen k odběru <em>%s</em>',
 			'deleted' => 'Kanál byl smazán',
 			'error' => 'Kanál nelze aktualizovat',
-			'internal_problem' => 'RSS kanál nelze přidat. Pro detaily <a href="%s">zkontrolujte logy FressRSS</a>.',
+			'internal_problem' => 'RSS kanál nelze přidat. Pro detaily <a href="%s">zkontrolujte logy FreshRSS</a>.', // @todo
 			'invalid_url' => 'URL <em>%s</em> není platné',
 			'marked_read' => 'Kanály byly označeny jako přečtené',
 			'n_actualized' => '%d kanálů bylo aktualizováno',

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

@@ -170,6 +170,7 @@ return array(
 		'jdh' => 'Journal du hacker',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => 'Tisk',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

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

@@ -44,8 +44,10 @@ return array(
 			'main_stream' => 'Zobrazit ve “Všechny kanály”',
 			'normal' => 'Show in its category', // TODO
 		),
+		'ssl_verify' => 'Verify SSL security',	//TODO
 		'stats' => 'Statistika',
 		'think_to_add' => 'Můžete přidat kanály.',
+		'timeout' => 'Timeout in seconds',	//TODO
 		'title' => 'Název',
 		'title_add' => 'Přidat RSS kanál',
 		'ttl' => 'Neobnovovat častěji než',

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

@@ -175,15 +175,15 @@ return array(
 	'user' => array(
 		'articles_and_size' => '%s Artikel (%s)',
 		'create' => 'Neuen Benutzer erstellen',
-		'delete_users' => 'Delete user', // TODO
+		'delete_users' => 'Lösche Benutzer',
 		'language' => 'Sprache',
 		'number' => 'Es wurde bis jetzt %d Account erstellt',
 		'numbers' => 'Es wurden bis jetzt %d Accounts erstellt',
 		'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',
 		'password_format' => 'mindestens 7 Zeichen',
-		'selected' => 'Selected user', // TODO
+		'selected' => 'Ausgewählter Benutzer',
 		'title' => 'Benutzer verwalten',
-		'update_users' => 'Update user', // TODO
+		'update_users' => 'Aktualisiere Benutzer',
 		'user_list' => 'Liste der Benutzer',
 		'username' => 'Nutzername',
 		'users' => 'Benutzer',

+ 11 - 9
app/i18n/de/conf.php

@@ -37,11 +37,12 @@ return array(
 			'no_limit' => 'Keine Begrenzung',
 			'thin' => 'Klein',
 		),
-		'show_nav_buttons' => 'Show the navigation buttons',	//TODO
+		'show_nav_buttons' => 'Zeige Navigations-Buttons',
 	),
 	'query' => array(
 		'_' => 'Benutzerabfragen',
 		'deprecated' => 'Diese Abfrage ist nicht länger gültig. Die referenzierte Kategorie oder der Feed ist gelöscht worden.',
+		'display' => 'Zeige Abfrage Ergebnisse',
 		'filter' => 'Angewendeter Filter:',
 		'get_all' => 'Alle Artikel anzeigen',
 		'get_category' => 'Kategorie "%s" anzeigen',
@@ -52,6 +53,7 @@ return array(
 		'number' => 'Abfrage Nr. %d',
 		'order_asc' => 'Älteste Artikel zuerst anzeigen',
 		'order_desc' => 'Neueste Artikel zuerst anzeigen',
+		'remove' => 'Lösche Abfrage',
 		'search' => 'Suche nach "%s"',
 		'state_0' => 'Alle Artikel anzeigen',
 		'state_1' => 'Gelesene Artikel anzeigen',
@@ -94,7 +96,7 @@ return array(
 		'display_categories_unfolded' => 'Kategorien standardmäßig eingeklappt zeigen',
 		'hide_read_feeds' => 'Kategorien & Feeds ohne ungelesene Artikel verstecken (funktioniert nicht mit der Einstellung „Alle Artikel zeigen“)',
 		'img_with_lazyload' => 'Verwende die "träges Laden"-Methode zum Laden von Bildern',
-		'sides_close_article' => 'Clicking outside of article text area closes the article',	//TODO
+		'sides_close_article' => 'Klick außerhalb des Artikel-Textes schließt den Artikel',
 		'jump_next' => 'springe zum nächsten ungelesenen Geschwisterelement (Feed oder Kategorie)',
 		'number_divided_when_reader' => 'Geteilt durch 2 in der Lese-Ansicht.',
 		'read' => array(
@@ -126,7 +128,7 @@ return array(
 	),
 	'sharing' => array(
 		'_' => 'Teilen',
-		'add' => 'Add a sharing method', // TODO
+		'add' => 'Füge eine Teilen-Dienst hinzu',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'email' => 'E-Mail',
@@ -134,7 +136,7 @@ return array(
 		'g+' => 'Google+',
 		'more_information' => 'Weitere Informationen',
 		'print' => 'Drucken',
-		'remove' => 'Remove sharing method', // TODO
+		'remove' => 'Entferne Teilen-Dienst',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Anzuzeigender Teilen-Name',
 		'share_url' => 'Zu verwendende Teilen-URL',
@@ -151,7 +153,7 @@ return array(
 		'collapse_article' => 'Einklappen',
 		'first_article' => 'Zum ersten Artikel springen',
 		'focus_search' => 'Auf das Suchfeld zugreifen',
-		'global_view' => 'Switch to global view', // TODO
+		'global_view' => 'Wechsle zur globalen Ansicht',
 		'help' => 'Dokumentation anzeigen',
 		'javascript' => 'JavaScript muss aktiviert sein, um Tastaturkürzel benutzen zu können',
 		'last_article' => 'Zum letzten Artikel springen',
@@ -161,17 +163,17 @@ return array(
 		'navigation' => 'Navigation',
 		'navigation_help' => 'Mit der "Umschalttaste" finden die Tastenkombination auf Feeds Anwendung.<br/>Mit der "Alt-Taste" finden die Tastenkombination auf Kategorien Anwendung.',
 		'next_article' => 'Zum nächsten Artikel springen',
-		'normal_view' => 'Switch to normal view', // TODO
+		'normal_view' => 'Wechsle zur normalen Ansicht',
 		'other_action' => 'Andere Aktionen',
 		'previous_article' => 'Zum vorherigen Artikel springen',
-		'reading_view' => 'Switch to reading view', // TODO
-		'rss_view' => 'Open RSS view in a new tab', // TODO
+		'reading_view' => 'Wechsle zur Lese-Ansicht',
+		'rss_view' => 'Öffne RSS Ansicht in neuem Tab',
 		'see_on_website' => 'Auf der Original-Webseite ansehen',
 		'shift_for_all_read' => '+ <code>Umschalttaste</code>, um alle Artikel als gelesen zu markieren.',
 		'title' => 'Tastenkombination',
 		'user_filter' => 'Auf Benutzerfilter zugreifen',
 		'user_filter_help' => 'Wenn es nur einen Benutzerfilter gibt, wird dieser verwendet. Ansonsten sind die Filter über ihre Nummer erreichbar.',
-		'views' => 'Views', // TODO
+		'views' => 'Ansichten',
 	),
 	'user' => array(
 		'articles_and_size' => '%s Artikel (%s)',

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

@@ -31,11 +31,11 @@ return array(
 	'extensions' => array(
 		'already_enabled' => '%s ist bereits aktiviert',
 		'disable' => array(
-			'ko' => '%s kann nicht deaktiviert werden. Für Details <a href="%s">prüfen Sie die FressRSS-Protokolle</a>.',
+			'ko' => '%s kann nicht deaktiviert werden. Für Details <a href="%s">prüfen Sie die FreshRSS-Protokolle</a>.',
 			'ok' => '%s ist jetzt deaktiviert',
 		),
 		'enable' => array(
-			'ko' => '%s kann nicht aktiviert werden. Für Details <a href="%s">prüfen Sie die FressRSS-Protokolle</a>.',
+			'ko' => '%s kann nicht aktiviert werden. Für Details <a href="%s">prüfen Sie die FreshRSS-Protokolle</a>.',
 			'ok' => '%s ist jetzt aktiviert',
 		),
 		'no_access' => 'Sie haben keinen Zugang zu %s',
@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => 'Sie haben <em>%s</em> bereits abonniert',
 			'deleted' => 'Der Feed ist gelöscht worden',
 			'error' => 'Der Feed kann nicht aktualisiert werden',
-			'internal_problem' => 'Der RSS-Feed konnte nicht hinzugefügt werden. Für Details <a href="%s">prüfen Sie die FressRSS-Protokolle</a>.',
+			'internal_problem' => 'Der RSS-Feed konnte nicht hinzugefügt werden. Für Details <a href="%s">prüfen Sie die FreshRSS-Protokolle</a>.', // @todo
 			'invalid_url' => 'Die URL <em>%s</em> ist ungültig',
 			'marked_read' => 'Die Feeds sind als gelesen markiert worden',
 			'n_actualized' => 'Die %d Feeds sind aktualisiert worden',
@@ -102,8 +102,8 @@ return array(
 			'error' => 'Der Benutzer %s kann nicht gelöscht werden',
 		),
 		'updated' => array(
-			'_' => 'User %s has been updated', // TODO
-			'error' => 'User %s has not been updated', // TODO
+			'_' => 'Benutzer %s wurde aktualisiert',
+			'error' => 'Benutzer %s wurde nicht aktualisiert',
 		),
 	),
 	'profile' => array(

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

@@ -19,7 +19,7 @@ return array(
 		'see_website' => 'Webseite ansehen',
 		'submit' => 'Abschicken',
 		'truncate' => 'Alle Artikel löschen',
-		'update' => 'Update', // TODO
+		'update' => 'Aktualisieren',
 	),
 	'auth' => array(
 		'email' => 'E-Mail-Adresse',
@@ -169,6 +169,7 @@ return array(
 		'jdh' => 'Journal du hacker',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => 'Drucken',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

+ 15 - 13
app/i18n/de/sub.php

@@ -2,13 +2,13 @@
 
 return array(
 	'api' => array(
-		'documentation' => 'Copy the following URL to use it within an external tool.',// TODO
-		'title' => 'API',// TODO
+		'documentation' => 'Kopieren Sie die folgende URL, um sie in einem externen Tool zu verwenden.',
+		'title' => 'API',
 	),
 	'bookmarklet' => array(
-		'documentation' => 'Drag this button to your bookmarks toolbar or right-click it and choose "Bookmark This Link". Then click "Subscribe" button in any page you want to subscribe to.',// TODO
-		'label' => 'Subscribe',// TODO
-		'title' => 'Bookmarklet',// TODO
+		'documentation' => 'Ziehen Sie diese Schaltfläche auf Ihre Lesezeichen-Symbolleiste oder klicken Sie mit der rechten Maustaste darauf und wählen Sie "Als Lesezeichen hinzufügen". Klicken Sie dann auf einer beliebigen Seite, die Sie abonnieren möchten, auf die Schaltfläche "Abonnieren".',
+		'label' => 'Abonnieren',
+		'title' => 'Bookmarklet',
 	),
 	'category' => array(
 		'_' => 'Kategorie',
@@ -35,17 +35,19 @@ return array(
 		'informations' => 'Information',
 		'keep_history' => 'Minimale Anzahl an Artikeln, die behalten wird',
 		'moved_category_deleted' => 'Wenn Sie eine Kategorie entfernen, werden deren Feeds automatisch in die Kategorie <em>%s</em> eingefügt.',
-		'mute' => 'mute', // TODO
+		'mute' => 'Stumm schalten',
 		'no_selected' => 'Kein Feed ausgewählt.',
 		'number_entries' => '%d Artikel',
 		'priority' => array(
-			'_' => 'Visibility', // TODO
-			'archived' => 'Do not show (archived)', // TODO
+			'_' => 'Sichtbarkeit',
+			'archived' => 'Nicht anzeigen (archiviert)',
 			'main_stream' => 'In Haupt-Feeds zeigen',
-			'normal' => 'Show in its category', // TODO
+			'normal' => 'Zeige in eigener Kategorie',
 		),
+		'ssl_verify' => 'Verify SSL security',	//TODO
 		'stats' => 'Statistiken',
 		'think_to_add' => 'Sie können Feeds hinzufügen.',
+		'timeout' => 'Timeout in seconds',	//TODO
 		'title' => 'Titel',
 		'title_add' => 'Einen RSS-Feed hinzufügen',
 		'ttl' => 'Aktualisiere automatisch nicht öfter als',
@@ -55,8 +57,8 @@ return array(
 		'pubsubhubbub' => 'Sofortbenachrichtigung mit PubSubHubbub',
 	),
 	'firefox' => array(
-		'documentation' => 'Follow the steps described <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">here</a> to add FreshRSS to Firefox feed reader list.',// TODO
-		'title' => 'Firefox feed reader',// TODO
+		'documentation' => 'Folge den <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">hier</a> beschriebenen Schritten um FreshRSS zu Deiner Firefox RSS-Reader Liste hinzuzufügen.',
+		'title' => 'Firefox RSS-Reader',
 	),
 	'import_export' => array(
 		'export' => 'Exportieren',
@@ -73,11 +75,11 @@ return array(
 		'bookmark' => 'Abonnieren (FreshRSS-Lesezeichen)',
 		'import_export' => 'Importieren / Exportieren',
 		'subscription_management' => 'Abonnementverwaltung',
-		'subscription_tools' => 'Subscription tools',// TODO
+		'subscription_tools' => 'Abonnement-Tools',
 	),
 	'title' => array(
 		'_' => 'Abonnementverwaltung',
 		'feed_management' => 'Verwaltung der RSS-Feeds',
-		'subscription_tools' => 'Subscription tools',// TODO
+		'subscription_tools' => 'Abonnement-Tools',
 	),
 );

+ 4 - 2
app/i18n/en/conf.php

@@ -42,6 +42,7 @@ return array(
 	'query' => array(
 		'_' => 'User queries',
 		'deprecated' => 'This query is no longer valid. The referenced category or feed has been deleted.',
+		'display' => 'Display user query results',
 		'filter' => 'Filter applied:',
 		'get_all' => 'Display all articles',
 		'get_category' => 'Display "%s" category',
@@ -52,6 +53,7 @@ return array(
 		'number' => 'Query n°%d',
 		'order_asc' => 'Display oldest articles first',
 		'order_desc' => 'Display newest articles first',
+		'remove' => 'Remove user query',
 		'search' => 'Search for "%s"',
 		'state_0' => 'Display all articles',
 		'state_1' => 'Display read articles',
@@ -169,8 +171,8 @@ return array(
 		'see_on_website' => 'See on original website',
 		'shift_for_all_read' => '+ <code>shift</code> to mark all articles as read',
 		'title' => 'Shortcuts',
-		'user_filter' => 'Access user filters',
-		'user_filter_help' => 'If there is only one user filter, it is used. Otherwise, filters are accessible by their number.',
+		'user_filter' => 'Access user queries',
+		'user_filter_help' => 'If there is only one user query, it is used. Otherwise, queries are accessible by their number.',
 		'views' => 'Views',
 	),
 	'user' => array(

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

@@ -31,11 +31,11 @@ return array(
 	'extensions' => array(
 		'already_enabled' => '%s is already enabled',
 		'disable' => array(
-			'ko' => '%s cannot be disabled. <a href="%s">Check FressRSS logs</a> for details.',
+			'ko' => '%s cannot be disabled. <a href="%s">Check FreshRSS logs</a> for details.',
 			'ok' => '%s is now disabled',
 		),
 		'enable' => array(
-			'ko' => '%s cannot be enabled. <a href="%s">Check FressRSS logs</a> for details.',
+			'ko' => '%s cannot be enabled. <a href="%s">Check FreshRSS logs</a> for details.',
 			'ok' => '%s is now enabled',
 		),
 		'no_access' => 'You have no access on %s',
@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => 'You have already subscribed to <em>%s</em>',
 			'deleted' => 'Feed has been deleted',
 			'error' => 'Feed cannot be updated',
-			'internal_problem' => 'The RSS feed could not be added. <a href="%s">Check FressRSS logs</a> for details.',
+			'internal_problem' => 'The newsfeed could not be added. <a href="%s">Check FreshRSS logs</a> for details. You can try force adding by appending <code>#force_feed</code> to the URL.',
 			'invalid_url' => 'URL <em>%s</em> is invalid',
 			'marked_read' => 'Feeds have been marked as read',
 			'n_actualized' => '%d feeds have been updated',

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

@@ -170,6 +170,7 @@ return array(
 		'Known' => 'Known based sites',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => 'Print',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

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

@@ -44,8 +44,10 @@ return array(
 			'main_stream' => 'Show in main stream',
 			'normal' => 'Show in its category',
 		),
+		'ssl_verify' => 'Verify SSL security',
 		'stats' => 'Statistics',
 		'think_to_add' => 'You may add some feeds.',
+		'timeout' => 'Timeout in seconds',
 		'title' => 'Title',
 		'title_add' => 'Add a RSS feed',
 		'ttl' => 'Do not automatically refresh more often than',

+ 2 - 0
app/i18n/es/conf.php

@@ -42,6 +42,7 @@ return array(
 	'query' => array(
 		'_' => 'Consultas de usuario',
 		'deprecated' => 'Esta consulta ya no es válida. La categoría referenciada o fuente ha sido eliminada.',
+		'display' => 'Display user query results', // TODO
 		'filter' => 'Filtro aplicado:',
 		'get_all' => 'Mostrar todos los artículos',
 		'get_category' => 'Mostrar la categoría "%s"',
@@ -52,6 +53,7 @@ return array(
 		'number' => 'Consulta n° %d',
 		'order_asc' => 'Mostrar primero los artículos más antiguos',
 		'order_desc' => 'Mostrar primero los artículos más recientes',
+		'remove' => 'Remove user query', // TODO
 		'search' => 'Buscar "%s"',
 		'state_0' => 'Mostrar todos los artículos',
 		'state_1' => 'Mostrar artículos leídos',

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

@@ -31,11 +31,11 @@ return array(
 	'extensions' => array(
 		'already_enabled' => '%s ya está activado',
 		'disable' => array(
-			'ko' => '%s no se puede desactivar. <a href="%s">Revisa el registro de FressRSS</a> para más información.',
+			'ko' => '%s no se puede desactivar. <a href="%s">Revisa el registro de FreshRSS</a> para más información.',
 			'ok' => '%s ha quedado desactivado',
 		),
 		'enable' => array(
-			'ko' => '%s no se puede activar. <a href="%s">Revisa el registro de FressRSS</a> para más información.',
+			'ko' => '%s no se puede activar. <a href="%s">Revisa el registro de FreshRSS</a> para más información.',
 			'ok' => '%s ha quedado activado',
 		),
 		'no_access' => 'No tienes acceso a %s',
@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => 'Ya estás suscrito a <em>%s</em>',
 			'deleted' => 'Fuente eliminada',
 			'error' => 'No es posible actualizar la fuente',
-			'internal_problem' => 'No ha sido posible agregar la fuente RSS. <a href="%s">Revisa el registro de FressRSS </a> para más información.',
+			'internal_problem' => 'No ha sido posible agregar la fuente RSS. <a href="%s">Revisa el registro de FreshRSS </a> para más información.', // @todo
 			'invalid_url' => 'La URL <em>%s</em> es inválida',
 			'marked_read' => 'Fuentes marcadas como leídas',
 			'n_actualized' => 'Se han actualiado %d fuentes',

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

@@ -170,6 +170,7 @@ return array(
 		'jdh' => 'Journal du hacker',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => 'Print',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

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

@@ -39,8 +39,10 @@ return array(
 			'main_stream' => 'Mostrar en salida principal',
 			'normal' => 'Show in its category', // TODO
 		),
+		'ssl_verify' => 'Verify SSL security',	//TODO
 		'stats' => 'Estadísticas',
 		'think_to_add' => 'Puedes añadir fuentes.',
+		'timeout' => 'Timeout in seconds',	//TODO
 		'title' => 'Título',
 		'title_add' => 'Añadir fuente RSS',
 		'ttl' => 'No actualizar de forma automática con una frecuencia mayor a',

+ 2 - 0
app/i18n/fr/conf.php

@@ -42,6 +42,7 @@ return array(
 	'query' => array(
 		'_' => 'Filtres utilisateurs',
 		'deprecated' => 'Ce filtre n’est plus valide. La catégorie ou le flux concerné a été supprimé.',
+		'display' => 'Afficher les résultats du filtre',
 		'filter' => 'Filtres appliqués :',
 		'get_all' => 'Afficher tous les articles',
 		'get_category' => 'Afficher la catégorie "%s"',
@@ -52,6 +53,7 @@ return array(
 		'number' => 'Filtre n°%d',
 		'order_asc' => 'Afficher les articles les plus anciens en premier',
 		'order_desc' => 'Afficher les articles les plus récents en premier',
+		'remove' => 'Supprimer le filtre',
 		'search' => 'Recherche de "%s"',
 		'state_0' => 'Afficher tous les articles',
 		'state_1' => 'Afficher les articles lus',

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

@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => 'Vous êtes déjà abonné à <em>%s</em>',
 			'deleted' => 'Le flux a été supprimé.',
 			'error' => 'Une erreur est survenue',
-			'internal_problem' => 'Le flux ne peut pas être ajouté. <a href="%s">Consulter les logs de FreshRSS</a> pour plus de détails.',
+			'internal_problem' => 'Le flux ne peut pas être ajouté. <a href="%s">Consulter les logs de FreshRSS</a> pour plus de détails. Vous pouvez essayer de forcer l’ajout par addition de <code>#force_feed</code> à l’URL.',
 			'invalid_url' => 'L’url <em>%s</em> est invalide.',
 			'marked_read' => 'Les flux ont été marqués comme lus.',
 			'n_actualized' => '%d flux ont été mis à jour.',

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

@@ -170,6 +170,7 @@ return array(
 		'jdh' => 'Journal du hacker',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => 'Imprimer',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

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

@@ -44,8 +44,10 @@ return array(
 			'main_stream' => 'Afficher dans le flux principal',
 			'normal' => 'Afficher dans sa catégorie',
 		),
+		'ssl_verify' => 'Vérification sécurité SSL',
 		'stats' => 'Statistiques',
 		'think_to_add' => 'Vous pouvez ajouter des flux.',
+		'timeout' => 'Délai d’attente en secondes',
 		'title' => 'Titre',
 		'title_add' => 'Ajouter un flux RSS',
 		'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',

+ 2 - 0
app/i18n/he/conf.php

@@ -42,6 +42,7 @@ return array(
 	'query' => array(
 		'_' => 'שאילתות',
 		'deprecated' => 'שאילתה זו אינה בתוקף יותר, הפיד או הקטגוריה לייחוס נמחקו.',
+		'display' => 'Display user query results', // TODO
 		'filter' => 'מסננים בשימוש:',
 		'get_all' => 'הצגת כל המאמרים',
 		'get_category' => 'הצגת קטגוריה "%s"',
@@ -52,6 +53,7 @@ return array(
 		'number' => 'שאילתה מספר °%d',
 		'order_asc' => 'הצגת מאמרים ישנים בראש',
 		'order_desc' => 'הצגת מאמרים חדשים בראש',
+		'remove' => 'Remove user query', // TODO
 		'search' => 'חיפוש "%s"',
 		'state_0' => 'הצגת כל המאמרים',
 		'state_1' => 'הצגת מאמרים שנקראו',

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

@@ -32,11 +32,11 @@ return array(
 	'extensions' => array(
 		'already_enabled' => '%s is already enabled', // @todo
 		'disable' => array(
-			'ko' => '%s cannot be disabled. <a href="%s">Check FressRSS logs</a> for details.', // @todo
+			'ko' => '%s cannot be disabled. <a href="%s">Check FreshRSS logs</a> for details.', // @todo
 			'ok' => '%s is now disabled', // @todo
 		),
 		'enable' => array(
-			'ko' => '%s cannot be enabled. <a href="%s">Check FressRSS logs</a> for details.', // @todo
+			'ko' => '%s cannot be enabled. <a href="%s">Check FreshRSS logs</a> for details.', // @todo
 			'ok' => '%s is now enabled', // @todo
 		),
 		'no_access' => 'You have no access on %s', // @todo
@@ -73,7 +73,7 @@ return array(
 			'already_subscribed' => 'אתה כבר רשום ל <em>%s</em>',
 			'deleted' => 'ההזנה נמחקה',
 			'error' => 'Feed cannot be updated', // @todo
-			'internal_problem' => 'אין אפשרות להוסיף את ההזנה. <a href="%s">בדקו את הלוגים</a> לפרטים.',
+			'internal_problem' => 'אין אפשרות להוסיף את ההזנה. <a href="%s">בדקו את הלוגים</a> לפרטים.', // @todo
 			'invalid_url' => 'URL <em>%s</em> אינו תקין',
 			'marked_read' => 'הזנות סומנו כנקראו',
 			'n_actualized' => '%d הזנות עודכנו',

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

@@ -170,6 +170,7 @@ return array(
 		'Known' => 'Known based sites',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => 'הדפסה',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

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

@@ -44,8 +44,10 @@ return array(
 			'main_stream' => 'הצגה בזרם המרכזי',
 			'normal' => 'Show in its category', // TODO
 		),
+		'ssl_verify' => 'Verify SSL security',	//TODO
 		'stats' => 'סטטיסטיקות',
 		'think_to_add' => 'ניתן להוסיף הזנות חדשות.',
+		'timeout' => 'Timeout in seconds',	//TODO
 		'title' => 'כותרת',
 		'title_add' => 'הוספת הזנה',
 		'ttl' => 'אין לרענן אוטומטית יותר מ',

+ 2 - 0
app/i18n/it/conf.php

@@ -42,6 +42,7 @@ return array(
 	'query' => array(
 		'_' => 'Ricerche personali',
 		'deprecated' => 'Questa query non è più valida. La categoria o il feed di riferimento non stati cancellati.',
+		'display' => 'Display user query results', // TODO
 		'filter' => 'Filtro applicato:',
 		'get_all' => 'Mostra tutti gli articoli',
 		'get_category' => 'Mostra la categoria "%s" ',
@@ -52,6 +53,7 @@ return array(
 		'number' => 'Ricerca n°%d',
 		'order_asc' => 'Mostra prima gli articoli più vecchi',
 		'order_desc' => 'Mostra prima gli articoli più nuovi',
+		'remove' => 'Remove user query', // TODO
 		'search' => 'Cerca per "%s"',
 		'state_0' => 'Mostra tutti gli articoli',
 		'state_1' => 'Mostra gli articoli letti',

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

@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => 'Hai già sottoscritto <em>%s</em>',
 			'deleted' => 'Feed cancellato',
 			'error' => 'Feed non aggiornato',
-			'internal_problem' => 'RSS feed non aggiunto. <a href="%s">Verifica i logs</a> per dettagli.',
+			'internal_problem' => 'RSS feed non aggiunto. <a href="%s">Verifica i logs</a> per dettagli.', // @todo
 			'invalid_url' => 'URL <em>%s</em> non valido',
 			'marked_read' => 'Feeds segnati come letti',
 			'n_actualized' => '%d feeds aggiornati',

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

@@ -170,6 +170,7 @@ return array(
 		'jdh' => 'Journal du hacker',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => 'Stampa',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

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

@@ -44,8 +44,10 @@ return array(
 			'main_stream' => 'Mostra in homepage', // TODO
 			'normal' => 'Show in its category', // TODO
 		),
+		'ssl_verify' => 'Verify SSL security',	//TODO
 		'stats' => 'Statistiche',
 		'think_to_add' => 'Aggiungi feed.',
+		'timeout' => 'Timeout in seconds',	//TODO
 		'title' => 'Titolo',
 		'title_add' => 'Aggiungi RSS feed',
 		'ttl' => 'Non aggiornare automaticamente piu di',

+ 2 - 0
app/i18n/kr/conf.php

@@ -42,6 +42,7 @@ return array(
 	'query' => array(
 		'_' => '사용자 쿼리',
 		'deprecated' => '이 쿼리는 더 이상 유효하지 않습니다. 해당하는 카테고리나 피드가 삭제되었습니다.',
+		'display' => 'Display user query results', // TODO
 		'filter' => '적용된 필터:',
 		'get_all' => '모든 글 표시',
 		'get_category' => '"%s" 카테고리 표시',
@@ -52,6 +53,7 @@ return array(
 		'number' => '쿼리 #%d',
 		'order_asc' => '오래된 글 먼저 표시',
 		'order_desc' => '최근 글 먼저 표시',
+		'remove' => 'Remove user query', // TODO
 		'search' => '"%s"의 검색 결과',
 		'state_0' => '모든 글 표시',
 		'state_1' => '읽은 글 표시',

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

@@ -31,11 +31,11 @@ return array(
 	'extensions' => array(
 		'already_enabled' => '%s 확장 기능은 이미 활성화되어 있습니다',
 		'disable' => array(
-			'ko' => '%s 확장 기능을 비활성화 할 수 없습니다. 자세한 내용은 <a href="%s">FressRSS 로그</a>를 참고하세요.',
+			'ko' => '%s 확장 기능을 비활성화 할 수 없습니다. 자세한 내용은 <a href="%s">FreshRSS 로그</a>를 참고하세요.',
 			'ok' => '%s 확장 기능이 비활성화되었습니다',
 		),
 		'enable' => array(
-			'ko' => '%s 확장 기능을 활성화 할 수 없습니다. 자세한 내용은 <a href="%s">FressRSS 로그</a>를 참고하세요.',
+			'ko' => '%s 확장 기능을 활성화 할 수 없습니다. 자세한 내용은 <a href="%s">FreshRSS 로그</a>를 참고하세요.',
 			'ok' => '%s 확장 기능이 활성화되었습니다',
 		),
 		'no_access' => '%s 확장 기능에 접근 권한이 없습니다',
@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => '이미 <em>%s</em> 피드를 구독 중입니다',
 			'deleted' => '피드가 삭제되었습니다',
 			'error' => '피드를 변경할 수 없습니다',
-			'internal_problem' => 'RSS 피드를 추가할 수 없습니다. 자세한 내용은 <a href="%s">FressRSS 로그</a>를 참고하세요.',
+			'internal_problem' => 'RSS 피드를 추가할 수 없습니다. 자세한 내용은 <a href="%s">FreshRSS 로그</a>를 참고하세요.', // @todo
 			'invalid_url' => 'URL (<em>%s</em>)이 유효하지 않습니다',
 			'marked_read' => '피드가 읽음으로 표시되었습니다',
 			'n_actualized' => '%d 개의 피드에서 새 글을 가져왔습니다',

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

@@ -170,6 +170,7 @@ return array(
 		'jdh' => 'Journal du hacker',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => '인쇄',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

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

@@ -44,8 +44,10 @@ return array(
 			'main_stream' => '메인 스트림에 표시하기',
 			'normal' => '피드가 속한 카테고리에만 표시하기',
 		),
+		'ssl_verify' => 'Verify SSL security',	//TODO
 		'stats' => '통계',
 		'think_to_add' => '피드를 추가할 수 있습니다.',
+		'timeout' => 'Timeout in seconds',	//TODO
 		'title' => '제목',
 		'title_add' => 'RSS 피드 추가',
 		'ttl' => '다음 시간이 지나기 전에 새로고침 금지',

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

@@ -175,7 +175,7 @@ return array(
 	'user' => array(
 		'articles_and_size' => '%s artikelen (%s)',
 		'create' => 'Creëer nieuwe gebruiker',
-		'delete_users' => 'Delete user', // TODO
+		'delete_users' => 'Verwijder gebruiker',
 		'language' => 'Taal',
 		'number' => 'Er is %d accounts gemaakt',
 		'numbers' => 'Er zijn %d accounts gemaakt',
@@ -186,9 +186,9 @@ return array(
 			'help' => '0 betekent dat er geen accountlimiet is',
 			'number' => 'Max aantal accounts',
 		),
-		'selected' => 'Selected user', // TODO
+		'selected' => 'Geselecteerde gebruiker',
 		'title' => 'Beheer gebruikers',
-		'update_users' => 'Update user', // TODO
+		'update_users' => 'Gebruiker bijwerken',
 		'user_list' => 'Lijst van gebruikers ',
 		'username' => 'Gebruikersnaam',
 		'users' => 'Gebruikers',

+ 14 - 12
app/i18n/nl/conf.php

@@ -37,11 +37,12 @@ return array(
 			'no_limit' => 'Geen limiet',
 			'thin' => 'Smal',
 		),
-		'show_nav_buttons' => 'Show the navigation buttons',	//TODO
+		'show_nav_buttons' => 'Toon navigatieknoppen',
 	),
 	'query' => array(
-		'_' => 'Gebruikers queries (informatie aanvragen)',
+		'_' => 'Gebruikersquery\'s (informatie aanvragen)',
 		'deprecated' => 'Deze query (informatie aanvraag) is niet langer geldig. De bedoelde categorie of feed is al verwijderd.',
+		'display' => 'Queryresultaten weergeven',
 		'filter' => 'Filter toegepast:',
 		'get_all' => 'Toon alle artikelen',
 		'get_category' => 'Toon "%s" categorie',
@@ -52,6 +53,7 @@ return array(
 		'number' => 'Query n°%d',
 		'order_asc' => 'Toon oudste artikelen eerst',
 		'order_desc' => 'Toon nieuwste artikelen eerst',
+		'remove' => 'Gebruikersquery verwijderen',
 		'search' => 'Zoek naar "%s"',
 		'state_0' => 'Toon alle artikelen',
 		'state_1' => 'Toon gelezen artikelen',
@@ -69,7 +71,7 @@ return array(
 		'state_13' => 'Toon gelezen artikelen',
 		'state_14' => 'Toon ongelezen artikelen',
 		'state_15' => 'Toon alle artikelen',
-		'title' => 'Gebruikers queries',
+		'title' => 'Gebruikersquery\'s',
 	),
 	'profile' => array(
 		'_' => 'Profiel beheer',
@@ -126,7 +128,7 @@ return array(
 	),
 	'sharing' => array(
 		'_' => 'Delen',
-		'add' => 'Add a sharing method', // TODO
+		'add' => 'Deelmethode toevoegen',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
@@ -134,7 +136,7 @@ return array(
 		'g+' => 'Google+',
 		'more_information' => 'Meer informatie',
 		'print' => 'Afdrukken',
-		'remove' => 'Remove sharing method', // TODO
+		'remove' => 'Deelmethode verwijderen',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Gedeelde naam om weer te geven',
 		'share_url' => 'Deel URL voor gebruik',
@@ -146,12 +148,12 @@ return array(
 		'_' => 'Shortcuts',
 		'article_action' => 'Artikel acties',
 		'auto_share' => 'Delen',
-		'auto_share_help' => 'Als er slechts één deel methode i, dan wordt deze gebruikt. Anders zijn ze toegankelijk met hun nummer.',
+		'auto_share_help' => 'Als er slechts één deelmethode is, dan wordt die gebruikt. Anders zijn ze toegankelijk met hun nummer.',
 		'close_dropdown' => 'Sluit menu',
 		'collapse_article' => 'Inklappen',
 		'first_article' => 'Spring naar eerste artikel',
 		'focus_search' => 'Toegang zoek venster',
-		'global_view' => 'Switch to global view', // TODO
+		'global_view' => 'Schakel naar globaal aanzicht',
 		'help' => 'Toon documentatie',
 		'javascript' => 'JavaScript moet geactiveerd zijn om verwijzingen te gebruiken',
 		'last_article' => 'Spring naar laatste artikel',
@@ -161,17 +163,17 @@ return array(
 		'navigation' => 'Navigatie',
 		'navigation_help' => 'Met de "Shift" toets, kunt u navigatie verwijzingen voor feeds gebruiken.<br/>Met de "Alt" toets, kunt u navigatie verwijzingen voor categoriën gebruiken.',
 		'next_article' => 'Spring naar volgende artikel',
-		'normal_view' => 'Switch to normal view', // TODO
+		'normal_view' => 'Schakel naar gewoon aanzicht',
 		'other_action' => 'Andere acties',
 		'previous_article' => 'Spring naar vorige artikel',
-		'reading_view' => 'Switch to reading view', // TODO
-		'rss_view' => 'Open RSS view in a new tab', // TODO
+		'reading_view' => 'Schakel naar leesaanzicht',
+		'rss_view' => 'Open RSS-aanzicht in een nieuwe tab',
 		'see_on_website' => 'Bekijk op originale website',
 		'shift_for_all_read' => '+ <code>shift</code> om alle artikelen als gelezen te markeren',
 		'title' => 'Verwijzingen',
 		'user_filter' => 'Toegang gebruikers filters',
-		'user_filter_help' => 'Als er slechts één gebruikers filter s, dan wordt deze gebruikt. Anders zijn ze toegankelijk met hun nummer.',
-		'views' => 'Views', // TODO
+		'user_filter_help' => 'Als er slechts één gebruikersfilter is, dan wordt die gebruikt. Anders zijn ze toegankelijk met hun nummer.',
+		'views' => 'Aanzichten',
 	),
 	'user' => array(
 		'articles_and_size' => '%s artikelen (%s)',

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

@@ -31,11 +31,11 @@ return array(
 	'extensions' => array(
 		'already_enabled' => '%s is al ingeschakeld',
 		'disable' => array(
-			'ko' => '%s kan niet worden uitgeschakeld. <a href="%s">Controleer FressRSS log bestanden</a> voor details.',
+			'ko' => '%s kan niet worden uitgeschakeld. <a href="%s">Controleer FreshRSS log bestanden</a> voor details.',
 			'ok' => '%s is nu uitgeschakeld',
 		),
 		'enable' => array(
-			'ko' => '%s kan niet worden ingeschakeld. <a href="%s">Controleer FressRSS log bestanden</a> voor details.',
+			'ko' => '%s kan niet worden ingeschakeld. <a href="%s">Controleer FreshRSS log bestanden</a> voor details.',
 			'ok' => '%s is nn ingeschakeld',
 		),
 		'no_access' => 'U hebt geen toegang voor %s',
@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => 'U bent al geabonneerd op <em>%s</em>',
 			'deleted' => 'Feed is verwijderd',
 			'error' => 'Feed kan niet worden vernieuwd',
-			'internal_problem' => 'De RSS feed kon niet worden toegevoegd. <a href="%s">Controleer FressRSS log bestanden</a> voor details.',
+			'internal_problem' => 'De feed kon niet worden toegevoegd. <a href="%s">Controleer de FreshRSS-logbestanden</a> voor details. Toevoegen forceren kan worden geprobeerd door <code>#force_feed</code> aan de URL toe te voegen.',
 			'invalid_url' => 'URL <em>%s</em> is ongeldig',
 			'marked_read' => 'Feeds zijn gemarkeerd als gelezen',
 			'n_actualized' => '%d feeds zijn vernieuwd',
@@ -102,8 +102,8 @@ return array(
 			'error' => 'Gebruiker %s kan niet worden verwijderd',
 		),
 		'updated' => array(
-			'_' => 'User %s has been updated', // TODO
-			'error' => 'User %s has not been updated', // TODO
+			'_' => 'Gebruiker %s is bijgewerkt',
+			'error' => 'Gebruiker %s kan niet worden bijgewerkt',
 		),
 		'set_registration' => 'Het maximale aantal accounts is vernieuwd.',
 	),

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

@@ -19,7 +19,7 @@ return array(
 		'see_website' => 'Bekijk website',
 		'submit' => 'Opslaan',
 		'truncate' => 'Verwijder alle artikelen',
-		'update' => 'Update', // TODO
+		'update' => 'Updaten',
 	),
 	'auth' => array(
 		'email' => 'Email adres',
@@ -170,6 +170,7 @@ return array(
 		'jdh' => 'Journal du hacker',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => 'Print',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

+ 8 - 6
app/i18n/nl/sub.php

@@ -35,18 +35,20 @@ return array(
 		'informations' => 'Informatie',
 		'keep_history' => 'Minimum aantal artikelen om te houden',
 		'moved_category_deleted' => 'Als u een categorie verwijderd, worden de feeds automatisch geclassificeerd onder <em>%s</em>.',
-		'mute' => 'mute', // TODO
+		'mute' => 'demp',
 		'no_selected' => 'Geen feed geselecteerd.',
 		'number_entries' => '%d artikelen',
 		'priority' => array(
-			'_' => 'Visibility', // TODO
-			'archived' => 'Do not show (archived)', // TODO
+			'_' => 'Zichtbaarheid',
+			'archived' => 'Niet weergeven (gearchiveerd)',
 			'main_stream' => 'Zichtbaar in het overzicht',
-			'normal' => 'Show in its category', // TODO
+			'normal' => 'Toon in categorie',
 		),
 		'pubsubhubbub' => 'Directe notificaties met PubSubHubbub',
+		'ssl_verify' => 'SSL-veiligheid controleren',
 		'stats' => 'Statistieken',
 		'think_to_add' => 'Voeg wat feeds toe.',
+		'timeout' => 'Time-out in seconden',
 		'title' => 'Titel',
 		'title_add' => 'Voeg een RSS feed toe',
 		'ttl' => 'Vernieuw automatisch niet vaker dan',
@@ -73,11 +75,11 @@ return array(
 		'bookmark' => 'Abonneer (FreshRSS bladwijzer)',
 		'import_export' => 'Importeer / exporteer',
 		'subscription_management' => 'Abonnementenbeheer',
-		'subscription_tools' => 'Subscription tools',// TODO
+		'subscription_tools' => 'Hulpmiddelen voor abonnementen',
 	),
 	'title' => array(
 		'_' => 'Abonnementenbeheer',
 		'feed_management' => 'RSS-feedbeheer',
-		'subscription_tools' => 'Subscription tools',// TODO
+		'subscription_tools' => 'Hulpmiddelen voor abonnementen',
 	),
 );

+ 2 - 0
app/i18n/pt-br/conf.php

@@ -42,6 +42,7 @@ return array(
 	'query' => array(
 		'_' => 'Queries do usuário',
 		'deprecated' => 'Esta não é mais válida. A categoria ou feed relacionado foi deletado.',
+		'display' => 'Display user query results', // TODO
 		'filter' => 'Filtro aplicado:',
 		'get_all' => 'Mostrar todos os artigos',
 		'get_category' => 'Visualizar "%s" categoria',
@@ -52,6 +53,7 @@ return array(
 		'number' => 'Query n°%d',
 		'order_asc' => 'Exibir artigos mais antigos primeiro',
 		'order_desc' => 'Exibir artigos mais novos primeiro',
+		'remove' => 'Remove user query', // TODO
 		'search' => 'Busca por "%s"',
 		'state_0' => 'Exibir todos os artigos',
 		'state_1' => 'Exibir artigos lidos',

+ 3 - 3
app/i18n/pt-br/feedback.php

@@ -31,11 +31,11 @@ return array(
 	'extensions' => array(
 		'already_enabled' => '%s já está habilitado',
 		'disable' => array(
-			'ko' => '%s não pode ser desabilitado. <a href="%s">verifique os logs do FressRSS</a> para detalhes.',
+			'ko' => '%s não pode ser desabilitado. <a href="%s">verifique os logs do FreshRSS</a> para detalhes.',
 			'ok' => '%s agora está desabilitado',
 		),
 		'enable' => array(
-			'ko' => '%s não pode ser habilitado. <a href="%s">verifique os logs do FressRSS</a> para detalhes.',
+			'ko' => '%s não pode ser habilitado. <a href="%s">verifique os logs do FreshRSS</a> para detalhes.',
 			'ok' => '%s agora está habilitado',
 		),
 		'no_access' => 'Você não tem acesso ao %s',
@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => 'Você já está inscrito no <em>%s</em>',
 			'deleted' => 'o Feed foi deletado',
 			'error' => 'O feed não pode ser atualizado',
-			'internal_problem' => 'O RSS feed não pôde ser adicionado. <a href="%s">Verifique os FressRSS logs</a> para detalhes.',
+			'internal_problem' => 'O RSS feed não pôde ser adicionado. <a href="%s">Verifique os FreshRSS logs</a> para detalhes.', // @todo
 			'invalid_url' => 'URL <em>%s</em> é inválida',
 			'marked_read' => 'Feeds foram marcados como lidos',
 			'n_actualized' => '%d feeds foram atualizados',

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

@@ -169,6 +169,7 @@ return array(
 		'jdh' => 'Journal du hacker',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => 'Imprimir',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

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

@@ -44,8 +44,10 @@ return array(
 			'main_stream' => 'Mostrar na tela principal',
 			'normal' => 'Show in its category', // TODO
 		),
+		'ssl_verify' => 'Verify SSL security',	//TODO
 		'stats' => 'Estatísticas',
 		'think_to_add' => 'Você deve adicionar alguns feeds.',
+		'timeout' => 'Timeout in seconds',	//TODO
 		'title' => 'Título',
 		'title_add' => 'Adicionar o RSS feed',
 		'ttl' => 'Não atualize automáticamente mais que',

+ 2 - 0
app/i18n/ru/conf.php

@@ -42,6 +42,7 @@ return array(
 	'query' => array(
 		'_' => 'User queries',
 		'deprecated' => 'This query is no longer valid. The referenced category or feed has been deleted.',
+		'display' => 'Display user query results', // TODO
 		'filter' => 'Filter applied:',
 		'get_all' => 'Display all articles',
 		'get_category' => 'Display "%s" category',
@@ -52,6 +53,7 @@ return array(
 		'number' => 'Query n°%d',
 		'order_asc' => 'Display oldest articles first',
 		'order_desc' => 'Display newest articles first',
+		'remove' => 'Remove user query', // TODO
 		'search' => 'Search for "%s"',
 		'state_0' => 'Display all articles',
 		'state_1' => 'Display read articles',

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

@@ -31,11 +31,11 @@ return array(
 	'extensions' => array(
 		'already_enabled' => '%s is already enabled',	//TODO
 		'disable' => array(
-			'ko' => '%s cannot be disabled. <a href="%s">Check FressRSS logs</a> for details.',	//TODO
+			'ko' => '%s cannot be disabled. <a href="%s">Check FreshRSS logs</a> for details.',	//TODO
 			'ok' => '%s is now disabled',	//TODO
 		),
 		'enable' => array(
-			'ko' => '%s cannot be enabled. <a href="%s">Check FressRSS logs</a> for details.',	//TODO
+			'ko' => '%s cannot be enabled. <a href="%s">Check FreshRSS logs</a> for details.',	//TODO
 			'ok' => '%s is now enabled',	//TODO
 		),
 		'no_access' => 'You have no access on %s',	//TODO
@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => 'You have already subscribed to <em>%s</em>',	//TODO
 			'deleted' => 'Feed has been deleted',	//TODO
 			'error' => 'Feed cannot be updated',	//TODO
-			'internal_problem' => 'The RSS feed could not be added. <a href="%s">Check FressRSS logs</a> for details.',	//TODO
+			'internal_problem' => 'The newsfeed could not be added. <a href="%s">Check FreshRSS logs</a> for details. You can try force adding by appending <code>#force_feed</code> to the URL.',	//TODO
 			'invalid_url' => 'URL <em>%s</em> is invalid',	//TODO
 			'marked_read' => 'Feeds have been marked as read',	//TODO
 			'n_actualized' => '%d feeds have been updated',	//TODO

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

@@ -170,6 +170,7 @@ return array(
 		'jdh' => 'Journal du hacker',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => 'Print',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

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

@@ -44,8 +44,10 @@ return array(
 			'main_stream' => 'Show in main stream', // TODO
 			'normal' => 'Show in its category', // TODO
 		),
+		'ssl_verify' => 'Verify SSL security',	//TODO
 		'stats' => 'Statistics',// TODO
 		'think_to_add' => 'You may add some feeds.',// TODO
+		'timeout' => 'Timeout in seconds',	//TODO
 		'title' => 'Title',// TODO
 		'title_add' => 'Add a RSS feed',// TODO
 		'ttl' => 'Do not automatically refresh more often than',// TODO

+ 2 - 0
app/i18n/tr/conf.php

@@ -42,6 +42,7 @@ return array(
 	'query' => array(
 		'_' => 'Kullanıcı sorguları',
 		'deprecated' => 'Bu sorgu artık geçerli değil. İlgili akış veya kategori silinmiş.',
+		'display' => 'Display user query results', // TODO
 		'filter' => 'Filtre uygulandı:',
 		'get_all' => 'Tüm makaleleri göster',
 		'get_category' => '"%s" kategorisini göster',
@@ -52,6 +53,7 @@ return array(
 		'number' => 'Sorgu n°%d',
 		'order_asc' => 'Önce eski makaleleri göster',
 		'order_desc' => 'Önce yeni makaleleri göster',
+		'remove' => 'Remove user query', // TODO
 		'search' => '"%s" için arama',
 		'state_0' => 'Tüm makaleleri göster',
 		'state_1' => 'Okunmuş makaleleri göster',

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

@@ -31,11 +31,11 @@ return array(
 	'extensions' => array(
 		'already_enabled' => '%s zaten aktif',
 		'disable' => array(
-			'ko' => '%s gösterilemiyor. Detaylar için <a href="%s">FressRSS log kayıtlarını</a> kontrol edin.',
+			'ko' => '%s gösterilemiyor. Detaylar için <a href="%s">FreshRSS log kayıtlarını</a> kontrol edin.',
 			'ok' => '%s pasif',
 		),
 		'enable' => array(
-			'ko' => '%s aktifleştirilemiyor. Detaylar için <a href="%s">FressRSS log kayıtlarını</a> kontrol edin.',
+			'ko' => '%s aktifleştirilemiyor. Detaylar için <a href="%s">FreshRSS log kayıtlarını</a> kontrol edin.',
 			'ok' => '%s aktif',
 		),
 		'no_access' => '%s de yetkiniz yok',
@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => '<em>%s</em> için zaten aboneliğiniz bulunmakta',
 			'deleted' => 'Akış silindi',
 			'error' => 'Akış güncellenemiyor',
-			'internal_problem' => 'RSS akışı eklenemiyor. Detaylar için <a href="%s">FressRSS log kayıtlarını</a> kontrol edin.',
+			'internal_problem' => 'RSS akışı eklenemiyor. Detaylar için <a href="%s">FreshRSS log kayıtlarını</a> kontrol edin.', // @todo
 			'invalid_url' => 'URL <em>%s</em> geçersiz',
 			'marked_read' => 'Akışlar okundu olarak işaretlendi',
 			'n_actualized' => '%d akışları güncellendi',

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

@@ -170,6 +170,7 @@ return array(
 		'jdh' => 'Journal du hacker',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => 'Print',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

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

@@ -103,7 +103,7 @@ return array(
 	'fix_errors_before' => 'Lütfen sonraki adıma geçmek için hataları düzeltin.',
 	'javascript_is_better' => 'FreshRSS JavaScript ile daha işlevseldir',
 	'js' => array(
-		'confirm_reinstall' => 'FressRSS i yeniden kurarak önceki yapılandırma ayarlarınızı kaybedeceksiniz. Devam etmek istiyor musunuz ?',
+		'confirm_reinstall' => 'FreshRSS i yeniden kurarak önceki yapılandırma ayarlarınızı kaybedeceksiniz. Devam etmek istiyor musunuz ?',
 	),
 	'language' => array(
 		'_' => 'Dil',

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

@@ -44,8 +44,10 @@ return array(
 			'main_stream' => 'Ana akışda göster',
 			'normal' => 'Show in its category', // TODO
 		),
+		'ssl_verify' => 'Verify SSL security',	//TODO
 		'stats' => 'İstatistikler',
 		'think_to_add' => 'Akış ekleyebilirsiniz.',
+		'timeout' => 'Timeout in seconds',	//TODO
 		'title' => 'Başlık',
 		'title_add' => 'RSS akışı ekle',
 		'ttl' => 'Şu kadar süreden fazla otomatik yenileme yapma',

+ 2 - 0
app/i18n/zh-cn/conf.php

@@ -42,6 +42,7 @@ return array(
 	'query' => array(
 		'_' => '自定义查询',
 		'deprecated' => '此查询不再有效。相关的分类或 RSS 源已被删除。',
+		'display' => 'Display user query results', // TODO
 		'filter' => '生效的过滤器:',
 		'get_all' => '显示所有文章',
 		'get_category' => '显示分类 "%s"',
@@ -52,6 +53,7 @@ return array(
 		'number' => '查询 n°%d',
 		'order_asc' => '由旧到新显示文章',
 		'order_desc' => '由新到旧显示文章',
+		'remove' => 'Remove user query', // TODO
 		'search' => '搜索 "%s"',
 		'state_0' => '显示所有文章',
 		'state_1' => '显示已读文章',

+ 3 - 3
app/i18n/zh-cn/feedback.php

@@ -31,11 +31,11 @@ return array(
 	'extensions' => array(
 		'already_enabled' => '%s 已启用',
 		'disable' => array(
-			'ko' => '%s 禁用失败。<a href="%s">检查 FressRSS 日志</a> 查看详情。',
+			'ko' => '%s 禁用失败。<a href="%s">检查 FreshRSS 日志</a> 查看详情。',
 			'ok' => '%s 现已禁用',
 		),
 		'enable' => array(
-			'ko' => '%s 启用失败。<a href="%s">检查 FressRSS 日志</a> 查看详情。',
+			'ko' => '%s 启用失败。<a href="%s">检查 FreshRSS 日志</a> 查看详情。',
 			'ok' => '%s 现已禁用',
 		),
 		'no_access' => '你无权访问 %s',
@@ -72,7 +72,7 @@ return array(
 			'already_subscribed' => '你已订阅 <em>%s</em>',
 			'deleted' => 'RSS 源已删除',
 			'error' => 'RSS 源更新失败',
-			'internal_problem' => 'RSS 源添加失败。<a href="%s">检查 FressRSS 日志</a> 查看详情。',
+			'internal_problem' => 'RSS 源添加失败。<a href="%s">检查 FreshRSS 日志</a> 查看详情。', // @todo
 			'invalid_url' => 'URL <em>%s</em> 无效',
 			'marked_read' => 'RSS 源已被设为已读',
 			'n_actualized' => '%d 个 RSS 源已更新',

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

@@ -170,6 +170,7 @@ return array(
 		'jdh' => 'Journal du hacker',
 		'mastodon' => 'Mastodon',
 		'movim' => 'Movim',
+		'pocket' => 'Pocket',
 		'print' => '打印',
 		'shaarli' => 'Shaarli',
 		'twitter' => 'Twitter',

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

@@ -44,8 +44,10 @@ return array(
 			'main_stream' => '在首页中显示',
 			'normal' => '在分类中显示',
 		),
+		'ssl_verify' => 'Verify SSL security',	//TODO
 		'stats' => '统计',
 		'think_to_add' => '你可以添加一些 RSS 源。',
+		'timeout' => 'Timeout in seconds',	//TODO
 		'title' => '标题',
 		'title_add' => '添加 RSS 源',
 		'ttl' => '最小自动更新时间',

+ 1 - 1
app/install.php

@@ -537,7 +537,7 @@ function printStep2() {
 		<div class="form-group">
 			<label class="group-name" for="default_user"><?php echo _t('install.default_user'); ?></label>
 			<div class="group-controls">
-				<input type="text" id="default_user" name="default_user" required="required" size="16" pattern="<?php echo FreshRSS_user_Controller::USERNAME_PATTERN; ?>" value="<?php echo isset($_SESSION['default_user']) ? $_SESSION['default_user'] : ''; ?>" placeholder="<?php echo httpAuthUser() == '' ? 'alice' : httpAuthUser(); ?>" tabindex="3" />
+				<input type="text" id="default_user" name="default_user" autocomplete="username" required="required" size="16" pattern="<?php echo FreshRSS_user_Controller::USERNAME_PATTERN; ?>" value="<?php echo isset($_SESSION['default_user']) ? $_SESSION['default_user'] : ''; ?>" placeholder="<?php echo httpAuthUser() == '' ? 'alice' : httpAuthUser(); ?>" tabindex="3" />
 			</div>
 		</div>
 

+ 3 - 3
app/layout/aside_subscription.phtml

@@ -1,15 +1,15 @@
 <ul class="nav nav-list aside">
 	<li class="nav-header"><?php echo _t('sub.menu.subscription_management'); ?></li>
 
-	<li class="item<?php echo Minz_Request::controllerName() == 'subscription' ? ' active' : ''; ?>">
+	<li class="item<?php echo Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() !== 'bookmarklet' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('subscription', 'index'); ?>"><?php echo _t('sub.menu.subscription_management'); ?></a>
 	</li>
 
-	<li class="item<?php echo Minz_Request::controllerName() == 'importExport' ? ' active' : ''; ?>">
+	<li class="item<?php echo Minz_Request::controllerName() === 'importExport' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('importExport', 'index'); ?>"><?php echo _t('sub.menu.import_export'); ?></a>
 	</li>
 
-	<li class="item<?php echo Minz_Request::controllerName() == 'bookmarklet' ? ' active' : ''; ?>">
+	<li class="item<?php echo Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'bookmarklet' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('subscription', 'bookmarklet'); ?>"><?php echo _t('sub.menu.subscription_tools'); ?></a>
 	</li>
 </ul>

+ 6 - 0
app/shares.php

@@ -126,4 +126,10 @@ return array(
 		'method' => 'POST',
 		'field' => 'status',
 	),
+	'pocket' => array(
+		'url' => 'https://getpocket.com/save?url=~LINK~&amp;title=~TITLE~',
+		'transform' => array('rawurlencode'),
+		'form' => 'simple',
+		'method' => 'GET',
+	),
 );

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

@@ -9,7 +9,7 @@
 		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
 		<div>
 			<label for="username"><?php echo _t('gen.auth.username'); ?></label>
-			<input type="text" id="username" name="username" size="16" required="required" pattern="<?php echo FreshRSS_user_Controller::USERNAME_PATTERN; ?>" autofocus="autofocus" />
+			<input type="text" id="username" name="username" autocomplete="username" size="16" required="required" pattern="<?php echo FreshRSS_user_Controller::USERNAME_PATTERN; ?>" autofocus="autofocus" />
 		</div>
 		<div>
 			<label for="passwordPlain"><?php echo _t('gen.auth.password'); ?></label>

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

@@ -11,7 +11,7 @@
 		<div>
 			<label class="group-name" for="new_user_passwordPlain"><?php echo _t('gen.auth.password'), '<br />', _i('help'), ' ', _t('gen.auth.password.format'); ?></label>
 			<div class="stick">
-				<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" required="required" autocomplete="off" pattern=".{7,}" />
+				<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" required="required" autocomplete="new-password" pattern=".{7,}" />
 				<a class="btn toggle-password" data-toggle="new_user_passwordPlain"><?php echo _i('key'); ?></a>
 			</div>
 			<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>

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

@@ -14,7 +14,7 @@
 			</label>
 
 			<div class="group-controls">
-				<input type="hidden" id="queries_<?php echo $key; ?>_search" name="queries[<?php echo $key; ?>][url]" value="<?php echo $query->getUrl(); ?>"/>
+				<input type="hidden" id="queries_<?php echo $key; ?>_url" name="queries[<?php echo $key; ?>][url]" value="<?php echo $query->getUrl(); ?>"/>
 				<input type="hidden" id="queries_<?php echo $key; ?>_search" name="queries[<?php echo $key; ?>][search]" value="<?php echo $query->getSearch(); ?>"/>
 				<input type="hidden" id="queries_<?php echo $key; ?>_state" name="queries[<?php echo $key; ?>][state]" value="<?php echo $query->getState(); ?>"/>
 				<input type="hidden" id="queries_<?php echo $key; ?>_order" name="queries[<?php echo $key; ?>][order]" value="<?php echo $query->getOrder(); ?>"/>
@@ -29,11 +29,11 @@
 					       data-leave-validation="<?php echo $query->getName(); ?>"
 					/>
 
-					<a class="btn" href="<?php echo $query->getUrl(); ?>">
+					<a class="btn" href="<?php echo $query->getUrl(); ?>" title="<?php echo _t('conf.query.display'); ?>">
 						<?php echo _i('link'); ?>
 					</a>
 
-					<a class="btn btn-attention remove" href="#" data-remove="query-group-<?php echo $key; ?>">
+					<a class="btn btn-attention remove" href="#" data-remove="query-group-<?php echo $key; ?>" title="<?php echo _t('conf.query.remove'); ?>">
 						<?php echo _i('close'); ?>
 					</a>
 				</div>

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

@@ -73,7 +73,7 @@
 
 			<label class="group-name" for="http_pass"><?php echo _t('sub.feed.auth.password'); ?></label>
 			<div class="group-controls">
-				<input type="password" name="http_pass" id="http_pass" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="off" />
+				<input type="password" name="http_pass" id="http_pass" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="new-password" />
 			</div>
 
 			<div class="group-controls">

+ 1 - 1
app/views/helpers/export/articles.phtml

@@ -23,7 +23,7 @@ foreach ($this->entriesRaw as $entryRaw) {
 	$entry = FreshRSS_EntryDAO::daoToEntry($entryRaw);
 	if (!isset($this->feed)) {
 		$feed = FreshRSS_CategoryDAO::findFeed($this->categories, $entry->feed());
-		if ($feed == null) {
+		if ($feed === null) {
 			$feed = $entry->feed(true);
 		}
 	} else {

+ 50 - 1
app/views/helpers/feed/update.phtml

@@ -158,7 +158,7 @@
 
 			<label class="group-name" for="http_pass_feed<?php echo $this->feed->id(); ?>"><?php echo _t('sub.feed.auth.password'); ?></label>
 			<div class="group-controls">
-				<input type="password" name="http_pass_feed<?php echo $this->feed->id(); ?>" id="http_pass_feed<?php echo $this->feed->id(); ?>" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="off" />
+				<input type="password" name="http_pass_feed<?php echo $this->feed->id(); ?>" id="http_pass_feed<?php echo $this->feed->id(); ?>" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="new-password" />
 			</div>
 		</div>
 
@@ -178,6 +178,55 @@
 			</div>
 		</div>
 
+		<div class="form-group">
+			<label class="group-name" for="mark_updated_article_unread"><?php echo _t('conf.reading.mark_updated_article_unread'); ?></label>
+			<div class="group-controls">
+				<label class="checkbox" for="mark_updated_article_unread">
+					<select name="mark_updated_article_unread" id="mark_updated_article_unread">
+						<option value=""<?php echo $this->feed->attributes('mark_updated_article_unread') === null ? ' selected="selected"' : ''; ?>><?php echo _t('gen.short.by_default'); ?></option>
+						<option value="0"<?php echo $this->feed->attributes('mark_updated_article_unread') === false ? ' selected="selected"' : ''; ?>><?php echo _t('gen.short.no'); ?></option>
+						<option value="1"<?php echo $this->feed->attributes('mark_updated_article_unread') === true ? ' selected="selected"' : ''; ?>><?php echo _t('gen.short.yes'); ?></option>
+					</select>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="read_upon_reception"><?php echo _t('conf.reading.read.when'); ?></label>
+			<div class="group-controls">
+				<label class="checkbox" for="read_upon_reception">
+					<select name="read_upon_reception" id="read_upon_reception">
+						<option value=""<?php echo $this->feed->attributes('read_upon_reception') === null ? ' selected="selected"' : ''; ?>><?php echo _t('gen.short.by_default'); ?></option>
+						<option value="0"<?php echo $this->feed->attributes('read_upon_reception') === false ? ' selected="selected"' : ''; ?>><?php echo _t('gen.short.no'); ?></option>
+						<option value="1"<?php echo $this->feed->attributes('read_upon_reception') === true ? ' selected="selected"' : ''; ?>><?php echo _t('gen.short.yes'); ?></option>
+					</select>
+					<?php echo _t('conf.reading.read.upon_reception'); ?>
+				</label>
+			</div>
+		</div>
+
+		<?php if (FreshRSS_Auth::hasAccess('admin')) { ?>
+		<div class="form-group">
+			<label class="group-name" for="timeout"><?php echo _t('sub.feed.timeout'); ?></label>
+			<div class="group-controls">
+				<input type="number" name="timeout" id="timeout" min="3" max="120" value="<?php echo $this->feed->attributes('timeout'); ?>" placeholder="<?php echo _t('gen.short.by_default'); ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="ssl_verify"><?php echo _t('sub.feed.ssl_verify'); ?></label>
+			<div class="group-controls">
+				<label class="checkbox" for="ssl_verify">
+					<select name="ssl_verify" id="ssl_verify">
+						<option value=""<?php echo $this->feed->attributes('ssl_verify') === null ? ' selected="selected"' : ''; ?>><?php echo _t('gen.short.by_default'); ?></option>
+						<option value="0"<?php echo $this->feed->attributes('ssl_verify') === false ? ' selected="selected"' : ''; ?>><?php echo _t('gen.short.no'); ?></option>
+						<option value="1"<?php echo $this->feed->attributes('ssl_verify') === true ? ' selected="selected"' : ''; ?>><?php echo _t('gen.short.yes'); ?></option>
+					</select>
+				</label>
+			</div>
+		</div>
+		<?php } ?>
+
 		<div class="form-group form-actions">
 			<div class="group-controls">
 				<button type="submit" class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>

+ 3 - 0
app/views/helpers/index/normal/entry_bottom.phtml

@@ -48,6 +48,9 @@
 					$title = $this->entry->title() . ' · ' . $this->feed->name();
 					foreach (FreshRSS_Context::$user_conf->sharing as $share_options) {
 						$share = FreshRSS_Share::get($share_options['type']);
+						if ($share === null) {
+							continue;
+						}
 						$share_options['link'] = $link;
 						$share_options['title'] = $title;
 						$share->update($share_options);

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

@@ -40,7 +40,7 @@
 						<input type="text" name="http_user" id="http_user_feed" value=" " autocomplete="off" placeholder="<?php echo _t('sub.feed.auth.username'); ?>" />
 					</li>
 					<li class="input">
-						<input type="password" name="http_pass" id="http_pass_feed" autocomplete="off" placeholder="<?php echo _t('sub.feed.auth.password'); ?>" />
+						<input type="password" name="http_pass" id="http_pass_feed" autocomplete="new-password" placeholder="<?php echo _t('sub.feed.auth.password'); ?>" />
 					</li>
 				</ul>
 			</div>

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

@@ -66,7 +66,7 @@
 			<label class="group-name" for="newPasswordPlain"><?php echo _t('admin.user.password_form'); ?></label>
 			<div class="group-controls">
 				<div class="stick">
-					<input type="password" id="newPasswordPlain" name="newPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
+					<input type="password" id="newPasswordPlain" name="newPasswordPlain" autocomplete="new-password" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
 					<a class="btn toggle-password" data-toggle="newPasswordPlain"><?php echo _i('key'); ?></a>
 				</div>
 				<?php echo _i('help'); ?> <?php echo _t('conf.profile.password_format'); ?>

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

@@ -22,7 +22,7 @@
 			<label class="group-name" for="newPasswordPlain"><?php echo _t('conf.profile.password_form'); ?></label>
 			<div class="group-controls">
 				<div class="stick">
-					<input type="password" id="newPasswordPlain" name="newPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
+					<input type="password" id="newPasswordPlain" name="newPasswordPlain" autocomplete="new-password" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
 					<a class="btn toggle-password" data-toggle="newPasswordPlain"><?php echo _i('key'); ?></a>
 				</div>
 				<?php echo _i('help'); ?> <?php echo _t('conf.profile.password_format'); ?>
@@ -35,7 +35,7 @@
 			<label class="group-name" for="apiPasswordPlain"><?php echo _t('conf.profile.password_api'); ?></label>
 			<div class="group-controls">
 				<div class="stick">
-					<input type="password" id="apiPasswordPlain" name="apiPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
+					<input type="password" id="apiPasswordPlain" name="apiPasswordPlain" autocomplete="new-password" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
 					<a class="btn toggle-password" data-toggle="apiPasswordPlain"><?php echo _i('key'); ?></a>
 				</div>
 				<?php echo _i('help'); ?> <kbd><a href="../api/"><?php echo Minz_Url::display('/api/', 'html', true); ?></a></kbd>

+ 1 - 0
cli/README.md

@@ -38,6 +38,7 @@ cd /usr/share/FreshRSS
 ./cli/do-install.php --default_user admin ( --auth_type form --environment production --base_url https://rss.example.net/ --language en --title FreshRSS --allow_anonymous --api_enabled --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123 --db-base freshrss --db-prefix freshrss )
 # --auth_type can be: 'form' (default), 'http_auth' (using the Web server access control), 'none' (dangerous)
 # --db-type can be: 'sqlite' (default), 'mysql' (MySQL or MariaDB), 'pgsql' (PostgreSQL)
+# --base_url should be a public (routable) URL if possible, and is used for push (PubSubHubbub), for some API functions (e.g. favicons), and external URLs in FreshRSS.
 # --environment can be: 'production' (default), 'development' (for additional log messages)
 # --language can be: 'en' (default), 'fr', or one of the [supported languages](../app/i18n/)
 # --db-prefix is an optional prefix in front of the names of the tables. We suggest using 'freshrss_'

+ 1 - 1
cli/create-user.php

@@ -20,7 +20,7 @@ $ok = FreshRSS_user_Controller::createUser($username,
 	empty($options['password']) ? '' : $options['password'],
 	empty($options['api_password']) ? '' : $options['api_password'],
 	$values,
-	!isset($options['no-default-feeds']));
+	!isset($options['no_default_feeds']));
 
 if (!$ok) {
 	fail('FreshRSS could not create user!');

+ 32 - 4
cli/i18n/I18nData.php

@@ -49,7 +49,8 @@ class I18nData {
 	 * @throws Exception
 	 */
 	public function addKey($key, $value) {
-		if (array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
+		if (array_key_exists($this->getFilenamePrefix($key), $this->data[static::REFERENCE_LANGUAGE]) &&
+		    array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
 			throw new Exception('The selected key already exist.');
 		}
 		$this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)][$key] = $value;
@@ -67,7 +68,8 @@ class I18nData {
 		if (!in_array($language, $this->getAvailableLanguages())) {
 			throw new Exception('The selected language does not exist.');
 		}
-		if (!array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
+		if (!array_key_exists($this->getFilenamePrefix($key), $this->data[static::REFERENCE_LANGUAGE]) ||
+		    !array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
 			throw new Exception('The selected key does not exist for the selected language.');
 		}
 		$this->data[$language][$this->getFilenamePrefix($key)][$key] = $value;
@@ -80,7 +82,8 @@ class I18nData {
 	 * @throws Exception
 	 */
 	public function duplicateKey($key) {
-		if (!array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
+		if (!array_key_exists($this->getFilenamePrefix($key), $this->data[static::REFERENCE_LANGUAGE]) ||
+		    !array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
 			throw new Exception('The selected key does not exist.');
 		}
 		$value = $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)][$key];
@@ -102,7 +105,8 @@ class I18nData {
 	 * @throws Exception
 	 */
 	public function removeKey($key) {
-		if (!array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
+		if (!array_key_exists($this->getFilenamePrefix($key), $this->data[static::REFERENCE_LANGUAGE]) ||
+		    !array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
 			throw new Exception('The selected key does not exist.');
 		}
 		foreach ($this->getAvailableLanguages() as $language) {
@@ -112,6 +116,30 @@ class I18nData {
 		}
 	}
 
+	/**
+	 * WARNING! This is valid only for ignore files. It's not the best way to
+	 * handle that but as it's meant to be used only for the cli tool, there
+	 * is no point of spending time on making it better than that.
+	 *
+	 * Ignore a key from a language, or reverse it.
+	 *
+	 * @param string $key
+	 * @param string $language
+	 * @param boolean $reverse
+	 */
+	public function ignore($key, $language, $reverse = false) {
+		$index = array_search($key, $this->data[$language]);
+
+		if ($index && $reverse) {
+			unset($this->data[$language][$index]);
+			return;
+		}
+		if ($index && !$reverse) {
+			return;
+		}
+		$this->data[$language][] = $key;
+	}
+
 	/**
 	 * Check if the data has changed
 	 *

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä