aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2018-06-03 13:35:38 +0200
committerGravatar GitHub <noreply@github.com> 2018-06-03 13:35:38 +0200
commitc0122003fe3031926546012b86a38b5187082613 (patch)
tree5502841327e7775f280fbd12732b4e8b8b7be6ff
parent029f4107123f6c318584bf9a43da7118c318657f (diff)
parentbe778c6bc2d8075e5a923153183b47507a2a71e3 (diff)
Merge pull request #1902 from FreshRSS/dev1.11.0
FreshRSS 1.11.0
-rw-r--r--CHANGELOG.md52
-rw-r--r--Docker/Dockerfile11
-rw-r--r--Docker/FreshRSS.Apache.conf2
-rw-r--r--Docker/README.md90
-rw-r--r--Docker/docker-compose.yml38
-rwxr-xr-xDocker/entrypoint.sh12
-rw-r--r--README.fr.md48
-rw-r--r--README.md48
-rwxr-xr-xapp/Controllers/entryController.php2
-rwxr-xr-xapp/Controllers/feedController.php19
-rw-r--r--app/Controllers/importExportController.php21
-rwxr-xr-xapp/Controllers/indexController.php2
-rw-r--r--app/Controllers/subscriptionController.php24
-rw-r--r--app/Controllers/userController.php47
-rw-r--r--app/Models/BooleanSearch.php55
-rw-r--r--app/Models/CategoryDAO.php26
-rw-r--r--app/Models/DatabaseDAO.php2
-rw-r--r--app/Models/Entry.php3
-rw-r--r--app/Models/EntryDAO.php188
-rw-r--r--app/Models/EntryDAOSQLite.php78
-rw-r--r--app/Models/Factory.php8
-rw-r--r--app/Models/Feed.php65
-rw-r--r--app/Models/FeedDAO.php80
-rw-r--r--app/Models/FeedDAOSQLite.php17
-rw-r--r--app/Models/UserQuery.php2
-rw-r--r--app/SQL/install.sql.mysql.php1
-rw-r--r--app/SQL/install.sql.pgsql.php3
-rw-r--r--app/SQL/install.sql.sqlite.php1
-rwxr-xr-xapp/actualize_script.php1
-rw-r--r--app/i18n/cz/conf.php2
-rw-r--r--app/i18n/cz/feedback.php6
-rw-r--r--app/i18n/cz/gen.php1
-rw-r--r--app/i18n/cz/sub.php2
-rw-r--r--app/i18n/de/admin.php6
-rw-r--r--app/i18n/de/conf.php20
-rw-r--r--app/i18n/de/feedback.php10
-rw-r--r--app/i18n/de/gen.php3
-rw-r--r--app/i18n/de/sub.php28
-rw-r--r--app/i18n/en/conf.php6
-rw-r--r--app/i18n/en/feedback.php6
-rw-r--r--app/i18n/en/gen.php1
-rw-r--r--app/i18n/en/sub.php2
-rwxr-xr-xapp/i18n/es/conf.php2
-rwxr-xr-xapp/i18n/es/feedback.php6
-rwxr-xr-xapp/i18n/es/gen.php1
-rwxr-xr-xapp/i18n/es/sub.php2
-rw-r--r--app/i18n/fr/conf.php2
-rw-r--r--app/i18n/fr/feedback.php2
-rw-r--r--app/i18n/fr/gen.php1
-rw-r--r--app/i18n/fr/sub.php2
-rw-r--r--app/i18n/he/conf.php2
-rw-r--r--app/i18n/he/feedback.php6
-rw-r--r--app/i18n/he/gen.php1
-rw-r--r--app/i18n/he/sub.php2
-rw-r--r--app/i18n/it/conf.php2
-rw-r--r--app/i18n/it/feedback.php2
-rw-r--r--app/i18n/it/gen.php1
-rw-r--r--app/i18n/it/sub.php2
-rw-r--r--app/i18n/kr/conf.php2
-rw-r--r--app/i18n/kr/feedback.php6
-rw-r--r--app/i18n/kr/gen.php1
-rw-r--r--app/i18n/kr/sub.php2
-rw-r--r--app/i18n/nl/admin.php6
-rw-r--r--app/i18n/nl/conf.php26
-rw-r--r--app/i18n/nl/feedback.php10
-rw-r--r--app/i18n/nl/gen.php3
-rw-r--r--app/i18n/nl/sub.php14
-rw-r--r--app/i18n/pt-br/conf.php2
-rw-r--r--app/i18n/pt-br/feedback.php6
-rw-r--r--app/i18n/pt-br/gen.php1
-rw-r--r--app/i18n/pt-br/sub.php2
-rw-r--r--app/i18n/ru/conf.php2
-rw-r--r--app/i18n/ru/feedback.php6
-rw-r--r--app/i18n/ru/gen.php1
-rw-r--r--app/i18n/ru/sub.php2
-rw-r--r--app/i18n/tr/conf.php2
-rw-r--r--app/i18n/tr/feedback.php6
-rw-r--r--app/i18n/tr/gen.php1
-rw-r--r--app/i18n/tr/install.php2
-rw-r--r--app/i18n/tr/sub.php2
-rw-r--r--app/i18n/zh-cn/conf.php2
-rw-r--r--app/i18n/zh-cn/feedback.php6
-rw-r--r--app/i18n/zh-cn/gen.php1
-rw-r--r--app/i18n/zh-cn/sub.php2
-rw-r--r--app/install.php2
-rw-r--r--app/layout/aside_subscription.phtml6
-rw-r--r--app/shares.php6
-rw-r--r--app/views/auth/formLogin.phtml2
-rw-r--r--app/views/auth/register.phtml2
-rw-r--r--app/views/configure/queries.phtml6
-rw-r--r--app/views/feed/add.phtml2
-rw-r--r--app/views/helpers/export/articles.phtml2
-rw-r--r--app/views/helpers/feed/update.phtml51
-rw-r--r--app/views/helpers/index/normal/entry_bottom.phtml3
-rw-r--r--app/views/subscription/index.phtml2
-rw-r--r--app/views/user/manage.phtml2
-rw-r--r--app/views/user/profile.phtml4
-rw-r--r--cli/README.md1
-rwxr-xr-xcli/create-user.php2
-rw-r--r--cli/i18n/I18nData.php36
-rw-r--r--cli/i18n/I18nFile.php4
-rw-r--r--cli/i18n/I18nFileInterface.php10
-rw-r--r--cli/i18n/I18nIgnoreFile.php64
-rw-r--r--cli/manipulate.translation.php124
-rwxr-xr-xcli/prepare.php1
-rw-r--r--config-user.default.php2
-rw-r--r--config.default.php2
-rw-r--r--constants.php2
-rw-r--r--data/fever/.gitignore1
-rw-r--r--data/fever/index.html13
-rw-r--r--docs/en/img/users/configuration.article.icons.pngbin0 -> 11154 bytes
-rw-r--r--docs/en/img/users/configuration.navigation.button.pngbin0 -> 491 bytes
-rw-r--r--docs/en/img/users/configuration.sharing.pngbin0 -> 15297 bytes
-rw-r--r--docs/en/users/03_Main_view.md3
-rw-r--r--docs/en/users/05_Configuration.md89
-rw-r--r--docs/en/users/06_Fever_API.md113
-rw-r--r--docs/en/users/06_Mobile_access.md34
-rw-r--r--docs/en/users/07_Frequently_Asked_Questions.md10
-rw-r--r--docs/fr/users/03_Main_view.md5
-rw-r--r--docs/fr/users/06_Fever_API.md19
-rw-r--r--docs/fr/users/06_Mobile_access.md36
-rw-r--r--docs/fr/users/07_Frequently_Asked_Questions.md8
-rw-r--r--docs/img/FreshRSS-logo-font.svg16
-rw-r--r--docs/img/FreshRSS-logo.svg25
-rw-r--r--lib/Minz/ModelPdo.php8
-rw-r--r--lib/Minz/Request.php13
-rw-r--r--lib/SimplePie/SimplePie/File.php2
-rw-r--r--lib/SimplePie/SimplePie/Misc.php11
-rw-r--r--lib/SimplePie/SimplePie/Parser.php2
-rw-r--r--lib/lib_rss.php57
-rw-r--r--p/api/fever.php640
-rw-r--r--p/api/greader.php6
-rw-r--r--p/api/index.php11
-rw-r--r--p/ext.php5
134 files changed, 2209 insertions, 447 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 501bff9c4..ec30f7470 100644
--- a/CHANGELOG.md
+++ b/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)
diff --git a/Docker/Dockerfile b/Docker/Dockerfile
index 9b7336e3b..189d7175a 100644
--- a/Docker/Dockerfile
+++ b/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
diff --git a/Docker/FreshRSS.Apache.conf b/Docker/FreshRSS.Apache.conf
index 59151d749..adfc804c6 100644
--- a/Docker/FreshRSS.Apache.conf
+++ b/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>
diff --git a/Docker/README.md b/Docker/README.md
index ccf4ab3f0..133e7c3b8 100644
--- a/Docker/README.md
+++ b/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
+```
diff --git a/Docker/docker-compose.yml b/Docker/docker-compose.yml
new file mode 100644
index 000000000..8a8ad270b
--- /dev/null
+++ b/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
+
diff --git a/Docker/entrypoint.sh b/Docker/entrypoint.sh
new file mode 100755
index 000000000..5b643da93
--- /dev/null
+++ b/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 "$@"
diff --git a/README.fr.md b/README.fr.md
index 97ffafb56..78a182d7b 100644
--- a/README.fr.md
+++ b/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)
diff --git a/README.md b/README.md
index cacf61fa9..7d6c51338 100644
--- a/README.md
+++ b/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
diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php
index 28f0cb745..73e181b07 100755
--- a/app/Controllers/entryController.php
+++ b/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)) {
diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php
index af732951f..ca85e7cb8 100755
--- a/app/Controllers/feedController.php
+++ b/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);
diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php
index a76dd9a2b..0fb5ba651 100644
--- a/app/Controllers/importExportController.php
+++ b/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);
diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php
index e3dbd4664..8567b4657 100755
--- a/app/Controllers/indexController.php
+++ b/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
);
diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php
index 37efd3b57..701a588e0 100644
--- a/app/Controllers/subscriptionController.php
+++ b/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 {
diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php
index 2dad6a3f0..47f0ecc62 100644
--- a/app/Controllers/userController.php
+++ b/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),
@@ -169,29 +191,15 @@ class FreshRSS_user_Controller extends Minz_ActionController {
$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);
diff --git a/app/Models/BooleanSearch.php b/app/Models/BooleanSearch.php
new file mode 100644
index 000000000..6e016f7e9
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php
index 68db17db3..ef2c402a0 100644
--- a/app/Models/CategoryDAO.php
+++ b/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() {
diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php
index f5469f2b7..b8e5577e4 100644
--- a/app/Models/DatabaseDAO.php
+++ b/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'
));
}
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index 0ad3781e5..c6b26a7cc 100644
--- a/app/Models/Entry.php
+++ b/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é)
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index 8cdebedc5..a3bca3727 100644
--- a/app/Models/EntryDAO.php
+++ b/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);
diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php
index 0f57dc1ba..cca970e36 100644
--- a/app/Models/EntryDAOSQLite.php
+++ b/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)))) {
diff --git a/app/Models/Factory.php b/app/Models/Factory.php
index dfccc883e..764987c46 100644
--- a/app/Models/Factory.php
+++ b/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) {
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index 196d94931..7eb079f15 100644
--- a/app/Models/Feed.php
+++ b/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';
diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php
index 0c25ab0ba..9d980c139 100644
--- a/app/Models/FeedDAO.php
+++ b/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'])) {
diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php
new file mode 100644
index 000000000..3c203b378
--- /dev/null
+++ b/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;
+ }
+
+}
diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php
index 52747f538..ef94fdaf6 100644
--- a/app/Models/UserQuery.php
+++ b/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'];
}
diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php
index b94a24298..747a0a6b3 100644
--- a/app/SQL/install.sql.mysql.php
+++ b/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`),
diff --git a/app/SQL/install.sql.pgsql.php b/app/SQL/install.sql.pgsql.php
index 23afdb783..b80fbf1e7 100644
--- a/app/SQL/install.sql.pgsql.php
+++ b/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;
diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php
index d8e670bc8..cbfb719e5 100644
--- a/app/SQL/install.sql.sqlite.php
+++ b/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,
diff --git a/app/actualize_script.php b/app/actualize_script.php
index 6f48220a6..ba9660a14 100755
--- a/app/actualize_script.php
+++ b/app/actualize_script.php
@@ -1,3 +1,4 @@
+#!/usr/bin/php
<?php
require(__DIR__ . '/../cli/_cli.php');
diff --git a/app/i18n/cz/conf.php b/app/i18n/cz/conf.php
index a7bcf6c08..e73ab168f 100644
--- a/app/i18n/cz/conf.php
+++ b/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',
diff --git a/app/i18n/cz/feedback.php b/app/i18n/cz/feedback.php
index 22eaf77f7..ff9c87d12 100644
--- a/app/i18n/cz/feedback.php
+++ b/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',
diff --git a/app/i18n/cz/gen.php b/app/i18n/cz/gen.php
index 288be61ec..09a8307ff 100644
--- a/app/i18n/cz/gen.php
+++ b/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',
diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php
index ec77be317..5caf9acbe 100644
--- a/app/i18n/cz/sub.php
+++ b/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ž',
diff --git a/app/i18n/de/admin.php b/app/i18n/de/admin.php
index 45bf2de68..f03e6cdaf 100644
--- a/app/i18n/de/admin.php
+++ b/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',
diff --git a/app/i18n/de/conf.php b/app/i18n/de/conf.php
index 6b0d7c7f9..78f3b4510 100644
--- a/app/i18n/de/conf.php
+++ b/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)',
diff --git a/app/i18n/de/feedback.php b/app/i18n/de/feedback.php
index 0ac6efefc..2c46bbe56 100644
--- a/app/i18n/de/feedback.php
+++ b/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(
diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php
index 8df0b1f07..7dbaac384 100644
--- a/app/i18n/de/gen.php
+++ b/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',
diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php
index 6d41d0e5a..0ba818c69 100644
--- a/app/i18n/de/sub.php
+++ b/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',
),
);
diff --git a/app/i18n/en/conf.php b/app/i18n/en/conf.php
index a1c3fc949..fd91ed8f6 100644
--- a/app/i18n/en/conf.php
+++ b/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(
diff --git a/app/i18n/en/feedback.php b/app/i18n/en/feedback.php
index ad7f87fd9..a7fbda3a0 100644
--- a/app/i18n/en/feedback.php
+++ b/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',
diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php
index 82cd4da72..ceee25105 100644
--- a/app/i18n/en/gen.php
+++ b/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',
diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php
index b9bae7955..5ff41a4b3 100644
--- a/app/i18n/en/sub.php
+++ b/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',
diff --git a/app/i18n/es/conf.php b/app/i18n/es/conf.php
index 464bebc4f..0e198caf8 100755
--- a/app/i18n/es/conf.php
+++ b/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',
diff --git a/app/i18n/es/feedback.php b/app/i18n/es/feedback.php
index 7b23f1a8d..627c86afc 100755
--- a/app/i18n/es/feedback.php
+++ b/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',
diff --git a/app/i18n/es/gen.php b/app/i18n/es/gen.php
index fe495a63f..a5957e12f 100755
--- a/app/i18n/es/gen.php
+++ b/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',
diff --git a/app/i18n/es/sub.php b/app/i18n/es/sub.php
index 091c1e3e3..3abc85578 100755
--- a/app/i18n/es/sub.php
+++ b/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',
diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php
index 402a97b1c..52b2f7e0d 100644
--- a/app/i18n/fr/conf.php
+++ b/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',
diff --git a/app/i18n/fr/feedback.php b/app/i18n/fr/feedback.php
index 1abf1b518..2443ad30a 100644
--- a/app/i18n/fr/feedback.php
+++ b/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.',
diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php
index beb578543..24c4b82fb 100644
--- a/app/i18n/fr/gen.php
+++ b/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',
diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php
index 04be55aa5..c6af2fb90 100644
--- a/app/i18n/fr/sub.php
+++ b/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',
diff --git a/app/i18n/he/conf.php b/app/i18n/he/conf.php
index 3d0534fdc..a682461a6 100644
--- a/app/i18n/he/conf.php
+++ b/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' => 'הצגת מאמרים שנקראו',
diff --git a/app/i18n/he/feedback.php b/app/i18n/he/feedback.php
index f773ff270..4b79b0d45 100644
--- a/app/i18n/he/feedback.php
+++ b/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 הזנות עודכנו',
diff --git a/app/i18n/he/gen.php b/app/i18n/he/gen.php
index c4262ab66..401e13620 100644
--- a/app/i18n/he/gen.php
+++ b/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',
diff --git a/app/i18n/he/sub.php b/app/i18n/he/sub.php
index 849a1d5bd..a263cd728 100644
--- a/app/i18n/he/sub.php
+++ b/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' => 'אין לרענן אוטומטית יותר מ',
diff --git a/app/i18n/it/conf.php b/app/i18n/it/conf.php
index 5ab343c4d..65b979c51 100644
--- a/app/i18n/it/conf.php
+++ b/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',
diff --git a/app/i18n/it/feedback.php b/app/i18n/it/feedback.php
index d702eb448..934666aa5 100644
--- a/app/i18n/it/feedback.php
+++ b/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',
diff --git a/app/i18n/it/gen.php b/app/i18n/it/gen.php
index d4f83735a..b2e007bce 100644
--- a/app/i18n/it/gen.php
+++ b/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',
diff --git a/app/i18n/it/sub.php b/app/i18n/it/sub.php
index 698e64481..22d58a27f 100644
--- a/app/i18n/it/sub.php
+++ b/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',
diff --git a/app/i18n/kr/conf.php b/app/i18n/kr/conf.php
index c9e91a804..558505973 100644
--- a/app/i18n/kr/conf.php
+++ b/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' => '읽은 글 표시',
diff --git a/app/i18n/kr/feedback.php b/app/i18n/kr/feedback.php
index bccf1aac0..f13675778 100644
--- a/app/i18n/kr/feedback.php
+++ b/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 개의 피드에서 새 글을 가져왔습니다',
diff --git a/app/i18n/kr/gen.php b/app/i18n/kr/gen.php
index 4fb6c92ff..b40ab14c4 100644
--- a/app/i18n/kr/gen.php
+++ b/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',
diff --git a/app/i18n/kr/sub.php b/app/i18n/kr/sub.php
index e11d4588f..464b64f70 100644
--- a/app/i18n/kr/sub.php
+++ b/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' => '다음 시간이 지나기 전에 새로고침 금지',
diff --git a/app/i18n/nl/admin.php b/app/i18n/nl/admin.php
index cbdfd6978..a1d975305 100644
--- a/app/i18n/nl/admin.php
+++ b/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',
diff --git a/app/i18n/nl/conf.php b/app/i18n/nl/conf.php
index 847c735d1..041b482b9 100644
--- a/app/i18n/nl/conf.php
+++ b/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)',
diff --git a/app/i18n/nl/feedback.php b/app/i18n/nl/feedback.php
index 3fabc97b8..e73f2f7bd 100644
--- a/app/i18n/nl/feedback.php
+++ b/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.',
),
diff --git a/app/i18n/nl/gen.php b/app/i18n/nl/gen.php
index ea3e720b4..683209cb9 100644
--- a/app/i18n/nl/gen.php
+++ b/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',
diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php
index 6b1ac268b..4ce254ef5 100644
--- a/app/i18n/nl/sub.php
+++ b/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',
),
);
diff --git a/app/i18n/pt-br/conf.php b/app/i18n/pt-br/conf.php
index 864c80e61..61a12160c 100644
--- a/app/i18n/pt-br/conf.php
+++ b/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',
diff --git a/app/i18n/pt-br/feedback.php b/app/i18n/pt-br/feedback.php
index 932bb72b1..2057cf985 100644
--- a/app/i18n/pt-br/feedback.php
+++ b/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',
diff --git a/app/i18n/pt-br/gen.php b/app/i18n/pt-br/gen.php
index fa1532787..932b6066d 100644
--- a/app/i18n/pt-br/gen.php
+++ b/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',
diff --git a/app/i18n/pt-br/sub.php b/app/i18n/pt-br/sub.php
index 09dde718f..1b084f08f 100644
--- a/app/i18n/pt-br/sub.php
+++ b/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',
diff --git a/app/i18n/ru/conf.php b/app/i18n/ru/conf.php
index 1b5cd8085..90a1a6797 100644
--- a/app/i18n/ru/conf.php
+++ b/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',
diff --git a/app/i18n/ru/feedback.php b/app/i18n/ru/feedback.php
index 9416ed878..6d4e5e3fe 100644
--- a/app/i18n/ru/feedback.php
+++ b/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
diff --git a/app/i18n/ru/gen.php b/app/i18n/ru/gen.php
index 6f9020695..ef9217d73 100644
--- a/app/i18n/ru/gen.php
+++ b/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',
diff --git a/app/i18n/ru/sub.php b/app/i18n/ru/sub.php
index 9e360630a..bef49623f 100644
--- a/app/i18n/ru/sub.php
+++ b/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
diff --git a/app/i18n/tr/conf.php b/app/i18n/tr/conf.php
index 0671da79e..cae1e4cac 100644
--- a/app/i18n/tr/conf.php
+++ b/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',
diff --git a/app/i18n/tr/feedback.php b/app/i18n/tr/feedback.php
index 8f40e7d85..df7d1b264 100644
--- a/app/i18n/tr/feedback.php
+++ b/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',
diff --git a/app/i18n/tr/gen.php b/app/i18n/tr/gen.php
index 293502e74..d99081396 100644
--- a/app/i18n/tr/gen.php
+++ b/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',
diff --git a/app/i18n/tr/install.php b/app/i18n/tr/install.php
index d5564297b..4ae0ad7e3 100644
--- a/app/i18n/tr/install.php
+++ b/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',
diff --git a/app/i18n/tr/sub.php b/app/i18n/tr/sub.php
index 871731158..e8cd15d0d 100644
--- a/app/i18n/tr/sub.php
+++ b/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',
diff --git a/app/i18n/zh-cn/conf.php b/app/i18n/zh-cn/conf.php
index c57738c5b..00bea4d79 100644
--- a/app/i18n/zh-cn/conf.php
+++ b/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' => '显示已读文章',
diff --git a/app/i18n/zh-cn/feedback.php b/app/i18n/zh-cn/feedback.php
index a005de0ce..1db879891 100644
--- a/app/i18n/zh-cn/feedback.php
+++ b/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 源已更新',
diff --git a/app/i18n/zh-cn/gen.php b/app/i18n/zh-cn/gen.php
index 241fce13b..9289d8571 100644
--- a/app/i18n/zh-cn/gen.php
+++ b/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',
diff --git a/app/i18n/zh-cn/sub.php b/app/i18n/zh-cn/sub.php
index bf73f82c4..034f8a9d9 100644
--- a/app/i18n/zh-cn/sub.php
+++ b/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' => '最小自动更新时间',
diff --git a/app/install.php b/app/install.php
index 870c93908..c30f8d583 100644
--- a/app/install.php
+++ b/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>
diff --git a/app/layout/aside_subscription.phtml b/app/layout/aside_subscription.phtml
index 6d2a5ac8f..e6a378837 100644
--- a/app/layout/aside_subscription.phtml
+++ b/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>
diff --git a/app/shares.php b/app/shares.php
index 5403fd48c..cd09aa19c 100644
--- a/app/shares.php
+++ b/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',
+ ),
);
diff --git a/app/views/auth/formLogin.phtml b/app/views/auth/formLogin.phtml
index 99be6059c..01d1d4736 100644
--- a/app/views/auth/formLogin.phtml
+++ b/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>
diff --git a/app/views/auth/register.phtml b/app/views/auth/register.phtml
index 23bda25ce..19e11ef76 100644
--- a/app/views/auth/register.phtml
+++ b/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>
diff --git a/app/views/configure/queries.phtml b/app/views/configure/queries.phtml
index 0dffa268d..baaf74954 100644
--- a/app/views/configure/queries.phtml
+++ b/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>
diff --git a/app/views/feed/add.phtml b/app/views/feed/add.phtml
index 5cd59d298..823cf1b2f 100644
--- a/app/views/feed/add.phtml
+++ b/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">
diff --git a/app/views/helpers/export/articles.phtml b/app/views/helpers/export/articles.phtml
index 49c370023..75651483a 100644
--- a/app/views/helpers/export/articles.phtml
+++ b/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 {
diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml
index d379c5df8..7144aab46 100644
--- a/app/views/helpers/feed/update.phtml
+++ b/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>
diff --git a/app/views/helpers/index/normal/entry_bottom.phtml b/app/views/helpers/index/normal/entry_bottom.phtml
index bc23938b0..793c644f9 100644
--- a/app/views/helpers/index/normal/entry_bottom.phtml
+++ b/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);
diff --git a/app/views/subscription/index.phtml b/app/views/subscription/index.phtml
index 26af0bd7c..41dd8a7df 100644
--- a/app/views/subscription/index.phtml
+++ b/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>
diff --git a/app/views/user/manage.phtml b/app/views/user/manage.phtml
index 26838fcc1..9d457f7a5 100644
--- a/app/views/user/manage.phtml
+++ b/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'); ?>
diff --git a/app/views/user/profile.phtml b/app/views/user/profile.phtml
index 7a63c0941..83140376d 100644
--- a/app/views/user/profile.phtml
+++ b/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>
diff --git a/cli/README.md b/cli/README.md
index d531b8c3d..0d1c0a7d4 100644
--- a/cli/README.md
+++ b/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_'
diff --git a/cli/create-user.php b/cli/create-user.php
index 5bc6c1e6c..29675fa53 100755
--- a/cli/create-user.php
+++ b/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!');
diff --git a/cli/i18n/I18nData.php b/cli/i18n/I18nData.php
index b8f958288..2178d330d 100644
--- a/cli/i18n/I18nData.php
+++ b/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) {
@@ -113,6 +117,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
*
* @return bool
diff --git a/cli/i18n/I18nFile.php b/cli/i18n/I18nFile.php
index a07efdf88..bdcf3c079 100644
--- a/cli/i18n/I18nFile.php
+++ b/cli/i18n/I18nFile.php
@@ -1,8 +1,9 @@
<?php
require_once __DIR__ . '/I18nData.php';
+require_once __DIR__ . '/I18nFileInterface.php';
-class i18nFile {
+class I18nFile implements I18nFileInterface{
private $i18nPath;
@@ -11,6 +12,7 @@ class i18nFile {
}
public function load() {
+ $i18n = array();
$dirs = new DirectoryIterator($this->i18nPath);
foreach ($dirs as $dir) {
if ($dir->isDot()) {
diff --git a/cli/i18n/I18nFileInterface.php b/cli/i18n/I18nFileInterface.php
new file mode 100644
index 000000000..c5aaf9fcd
--- /dev/null
+++ b/cli/i18n/I18nFileInterface.php
@@ -0,0 +1,10 @@
+<?php
+
+require_once __DIR__ . '/I18nData.php';
+
+interface I18nFileInterface {
+
+ public function load();
+
+ public function dump(I18nData $i18n);
+}
diff --git a/cli/i18n/I18nIgnoreFile.php b/cli/i18n/I18nIgnoreFile.php
new file mode 100644
index 000000000..714e000ca
--- /dev/null
+++ b/cli/i18n/I18nIgnoreFile.php
@@ -0,0 +1,64 @@
+<?php
+
+require_once __DIR__ . '/I18nData.php';
+require_once __DIR__ . '/I18nFileInterface.php';
+
+class I18nIgnoreFile implements I18nFileInterface {
+
+ private $i18nPath;
+
+ public function __construct() {
+ $this->i18nPath = __DIR__ . '/ignore';
+ }
+
+ public function dump(I18nData $i18n) {
+ foreach ($i18n->getData() as $language => $content) {
+ $filename = $this->i18nPath . DIRECTORY_SEPARATOR . $language . '.php';
+ file_put_contents($filename, $this->format($content));
+ }
+ }
+
+ public function load() {
+ $i18n = array();
+ $files = new DirectoryIterator($this->i18nPath);
+ foreach ($files as $file) {
+ if (!$file->isFile()) {
+ continue;
+ }
+ $i18n[$file->getBasename('.php')] = (include $file->getPathname());
+ }
+
+ return new I18nData($i18n);
+ }
+
+ /**
+ * Format an array of translation
+ *
+ * It takes an array of translation and format it to be dumped in a
+ * translation file. The array is first converted to a string then some
+ * formatting regexes are applied to match the original content.
+ *
+ * @param array $translation
+ * @return string
+ */
+ private function format($translation) {
+ $translation = var_export(($translation), true);
+ $patterns = array(
+ '/array \(/',
+ '/=>\s*array/',
+ '/ {2}/',
+ '/\d+ => /',
+ );
+ $replacements = array(
+ 'array(',
+ '=> array',
+ "\t", // Double quoting is mandatory to have a tab instead of the \t string
+ '',
+ );
+ $translation = preg_replace($patterns, $replacements, $translation);
+
+ // Double quoting is mandatory to have new lines instead of \n strings
+ return sprintf("<?php\n\nreturn %s;\n", $translation);
+ }
+
+}
diff --git a/cli/manipulate.translation.php b/cli/manipulate.translation.php
index 0e06993ef..35405bb89 100644
--- a/cli/manipulate.translation.php
+++ b/cli/manipulate.translation.php
@@ -1,45 +1,60 @@
<?php
-$options = getopt("h");
+$options = getopt("a:hk:l:rv:");
if (array_key_exists('h', $options)) {
help();
}
-if (1 === $argc || 5 < $argc) {
- help();
+if (!array_key_exists('a', $options)) {
+ error('You need to specify the action to perform.');
}
-require_once __DIR__ . '/i18n/I18nFile.php';
-
-$i18nFile = new I18nFile();
+if ('ignore' === $options['a']) {
+ require_once __DIR__ . '/i18n/I18nIgnoreFile.php';
+ $i18nFile = new I18nIgnoreFile();
+} else {
+ require_once __DIR__ . '/i18n/I18nFile.php';
+ $i18nFile = new I18nFile();
+}
$i18nData = $i18nFile->load();
-switch ($argv[1]) {
- case 'add_language' :
- $i18nData->addLanguage($argv[2]);
- break;
- case 'add_key' :
- if (3 === $argc) {
- help();
+switch ($options['a']) {
+ case 'add' :
+ if (array_key_exists('k', $options) && array_key_exists('v', $options) && array_key_exists('l', $options)) {
+ $i18nData->addValue($options['k'], $options['v'], $options['l']);
+ } elseif (array_key_exists('k', $options) && array_key_exists('v', $options)) {
+ $i18nData->addKey($options['k'], $options['v']);
+ } elseif (array_key_exists('l', $options)) {
+ $i18nData->addLanguage($options['l']);
+ } else {
+ error('You need to specify a valid set of options.');
}
- $i18nData->addKey($argv[2], $argv[3]);
break;
- case 'add_value':
- if (4 === $argc) {
- help();
+ case 'delete' :
+ if (array_key_exists('k', $options)) {
+ $i18nData->removeKey($options['k']);
+ } else {
+ error('You need to specify the key to delete.');
}
- $i18nData->addValue($argv[2], $argv[3], $argv[4]);
break;
- case 'duplicate_key' :
- $i18nData->duplicateKey($argv[2]);
- break;
- case 'delete_key' :
- $i18nData->removeKey($argv[2]);
+ case 'duplicate' :
+ if (array_key_exists('k', $options)) {
+ $i18nData->duplicateKey($options['k']);
+ } else {
+ error('You need to specify the key to duplicate');
+ }
break;
case 'format' :
$i18nFile->dump($i18nData);
break;
+ case 'ignore' :
+ if (array_key_exists('l', $options) && array_key_exists('k', $options)) {
+ $i18nData->ignore($options['k'], $options['l'], array_key_exists('r', $options));
+ } else {
+ error('You need to specify a valid set of options.');
+ }
+ break;
default :
help();
}
@@ -49,46 +64,65 @@ if ($i18nData->hasChanged()) {
}
/**
+ * Output error message.
+ */
+function error($message) {
+ $error = <<<ERROR
+WARNING
+ %s\n\n
+ERROR;
+ echo sprintf($error, $message);
+ help();
+}
+
+/**
* Output help message.
*/
function help() {
$help = <<<HELP
NAME
- %s
+ %1\$s
SYNOPSIS
- php %s [OPTION] [OPERATION] [KEY] [VALUE] [LANGUAGE]
+ php %1\$s [OPTIONS]
DESCRIPTION
- Manipulate translation files. Available operations are
- Check if translation files have missing keys or missing translations.
-
+ Manipulate translation files.
+
+ -a=ACTION
+ select the action to perform. Available actions are add, delete,
+ duplicate, format, and ignore. This option is mandatory.
+ -k=KEY select the key to work on.
+ -v=VAL select the value to set.
+ -l=LANG select the language to work on.
-h display this help and exit.
-OPERATION
- add_language
- add a new language by duplicating the referential. This operation
- needs only a KEY.
+EXEMPLE
+Exemple 1: add a language. It adds a new language by duplicating the referential.
+ php %1\$s -a add -l my_lang
+
+Exemple 2: add a new key. It adds the key in the referential.
+ php %1\$s -a add -k my_key -v my_value
- add_key add a new key in the referential. This operation needs a KEY and
- a VALUE.
+Exemple 3: add a new value. It adds a new value for the selected key in the selected language.
+ php %1\$s -a add -k my_key -v my_value -l my_lang
- add_value
- add a value in the referential. This operation needs a KEY, a
- VALUE, and a LANGUAGE.
+Exemple 4: delete a key. It deletes the selected key in every languages.
+ php %1\$s -a delete -k my_key
- duplicate_key
- duplicate a referential key in other languages. This operation
- needs only a KEY.
+Exemple 5: duplicate a key. It duplicates the key from the referential in every languages.
+ php %1\$s -a duplicate -k my_key
- delete_key
- delete a referential key from all languages. This operation needs
- only a KEY.
+Exemple 6: format i18n files.
+ php %1\$s -a format
- format format i18n files.
+Exemple 7: ignore a key. It adds the key in the ignore file to mark it as translated.
+ php %1\$s -a ignore -k my_key -l my_lang
+Exemple 8: revert ignore a key. It removes the key from the ignore file.
+ php %1\$s -a ignore -r -k my_key -l my_lang\n\n
HELP;
$file = str_replace(__DIR__ . '/', '', __FILE__);
- echo sprintf($help, $file, $file);
+ echo sprintf($help, $file);
exit;
}
diff --git a/cli/prepare.php b/cli/prepare.php
index 2db2da555..81fb53f85 100755
--- a/cli/prepare.php
+++ b/cli/prepare.php
@@ -7,6 +7,7 @@ $dirs = array(
'/cache',
'/extensions-data',
'/favicons',
+ '/fever',
'/PubSubHubbub',
'/PubSubHubbub/feeds',
'/PubSubHubbub/keys',
diff --git a/config-user.default.php b/config-user.default.php
index 5e67d8d9b..6aef0dc49 100644
--- a/config-user.default.php
+++ b/config-user.default.php
@@ -9,6 +9,8 @@ return array (
'token' => '',
'passwordHash' => '',
'apiPasswordHash' => '',
+ //feverKey is md5($user . ':' . $apiPasswordPlain)
+ 'feverKey' => '',
'posts_per_page' => 20,
'since_hours_posts_per_rss' => 168,
'min_posts_per_rss' => 2,
diff --git a/config.default.php b/config.default.php
index 4e4c97e67..e6297a160 100644
--- a/config.default.php
+++ b/config.default.php
@@ -1,7 +1,7 @@
<?php
# Do not modify this file, which defines default values,
-# but edit `config.php` instead, after the install process is completed.
+# but edit `./data/config.php` instead, after the install process is completed.
return array(
# Set to `development` to get additional error messages,
diff --git a/constants.php b/constants.php
index 188560eb5..29ee59181 100644
--- a/constants.php
+++ b/constants.php
@@ -2,7 +2,7 @@
//NB: Do not edit; use ./constants.local.php instead.
//<Not customisable>
-define('FRESHRSS_VERSION', '1.10.2');
+define('FRESHRSS_VERSION', '1.11.0');
define('FRESHRSS_WEBSITE', 'https://freshrss.org');
define('FRESHRSS_WIKI', 'https://freshrss.github.io/FreshRSS/');
diff --git a/data/fever/.gitignore b/data/fever/.gitignore
new file mode 100644
index 000000000..2211df63d
--- /dev/null
+++ b/data/fever/.gitignore
@@ -0,0 +1 @@
+*.txt
diff --git a/data/fever/index.html b/data/fever/index.html
new file mode 100644
index 000000000..85faaa37e
--- /dev/null
+++ b/data/fever/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">
+<head>
+<meta charset="UTF-8" />
+<meta http-equiv="Refresh" content="0; url=/" />
+<title>Redirection</title>
+<meta name="robots" content="noindex" />
+</head>
+
+<body>
+<p><a href="/">Redirection</a></p>
+</body>
+</html>
diff --git a/docs/en/img/users/configuration.article.icons.png b/docs/en/img/users/configuration.article.icons.png
new file mode 100644
index 000000000..d1dd768ad
--- /dev/null
+++ b/docs/en/img/users/configuration.article.icons.png
Binary files differ
diff --git a/docs/en/img/users/configuration.navigation.button.png b/docs/en/img/users/configuration.navigation.button.png
new file mode 100644
index 000000000..045956472
--- /dev/null
+++ b/docs/en/img/users/configuration.navigation.button.png
Binary files differ
diff --git a/docs/en/img/users/configuration.sharing.png b/docs/en/img/users/configuration.sharing.png
new file mode 100644
index 000000000..a3cfd0570
--- /dev/null
+++ b/docs/en/img/users/configuration.sharing.png
Binary files differ
diff --git a/docs/en/users/03_Main_view.md b/docs/en/users/03_Main_view.md
index adb62c7ec..57eab192d 100644
--- a/docs/en/users/03_Main_view.md
+++ b/docs/en/users/03_Main_view.md
@@ -181,3 +181,6 @@ Some operators can be used negatively, to exclude articles, with the same syntax
`-author:name`, `-intitle:keyword`, `-inurl:keyword`, `-#tag`, `!keyword`.
It is also possible to combine operators to have a very sharp filter, and it is allowed to have multiple instances of `author:`, `intitle:`, `inurl:`, `#`, and free-text.
+
+Combining several search criteria implies a logical *and*, but the keyword ` OR ` can be used to combine several search criteria with a logical *or* instead:
+`author:Dupont OR author:Dupond`
diff --git a/docs/en/users/05_Configuration.md b/docs/en/users/05_Configuration.md
index d0951e905..154cd98b5 100644
--- a/docs/en/users/05_Configuration.md
+++ b/docs/en/users/05_Configuration.md
@@ -3,9 +3,29 @@
## Language
-At the moment, FreshRSS is available in French and English. After you confirm your choice, the whole interface will be displayed in the chosen language.
-
-There are parts of FreshRSS that are not translated and are not intended to be translated. For now, the logs visible in the application as well as the one generated by the script of automatic update are part of it.
+At the moment, FreshRSS is available in 13 languages. After you confirm your choice, the interface will be displayed in the chosen language.
+Depending on the chosen language, there might be parts of the interface that are still not translated. If you're willing to help translating
+the missing bits or add a new language, please check how you can [contribute to the project](../contributing.md#contribute-to-internationalization-i18n).
+
+There are parts of FreshRSS that are not translated and are not intended to be translated. For now, the logs visible in the application as well as the one generated by automatic update scripts are part of it.
+
+Not all languages are equals regarding completion:
+
+| Language | Completion |
+|----------|-----------:|
+| cz | 87.4% |
+| de | 88.1% |
+| en | 100% |
+| es | 88.7% |
+| fr | 99.3% |
+| he | 69.6% |
+| it | 86.4% |
+| kr | 96.3% |
+| nl | 95.4% |
+| pt-br | 87.4% |
+| ru | 36.4% |
+| tr | 88.1% |
+| zh-cn | 99.0% |
## Theme
@@ -33,7 +53,16 @@ There are some who prefer short lines of text while others prefer to maximize th
## Article icons
-**TODO**
+It worth noting that this section only has effects in normal view.
+
+![Article icons configuration](../img/users/configuration.article.icons.png)
+
+Each article is rendered with a header (top line) and a footer (bottom line).
+In that section, you can choose what will be displayed in those.
+
+If you disable every item in the top line, you'll still be able to see it since
+there is the feed name and the article title. But if you do the same thing for
+the bottom line, it will be empty.
## HTML5 notification timout
@@ -41,6 +70,14 @@ After the automatic updates of the feeds, FreshRSS uses the HTML5 notification A
The duration of this notification can be set. By default, the value is 0.
+## Show the navigation button
+
+By default, FreshRSS displays buttons to ease the article navigation when browsing on mobile. The drawback is that they eat up some precious space.
+
+![navigation button configuration](../img/users/configuration.navigation.button.png)
+
+If you don't use those buttons because you never browse on mobile or because you browse with gestures, you can disable them from the interface.
+
# Reading
**TODO**
@@ -51,15 +88,53 @@ The duration of this notification can be set. By default, the value is 0.
# Sharing
-**TODO**
+To make your life easier, you can share directly an article within FreshRSS.
+
+At the moment, FreshRSS supports 15 sharing methods ranging from self-hosted services (Shaarli, etc.) to proprietary services (Facebook, etc.).
+
+By default, the sharing list is empty.
+![Sharing configuration](../img/users/configuration.sharing.png)
+
+To add a new item in the list, follow those simple steps:
+
+ 1. Select the share method in the drop-down.
+ 1. Press the ```✚``` sign to add it to the list.
+ 1. Configure the method in the list. All method names can be modified in the display. Some methods need the sharing URL to be able to work properly (ex: Shaarli).
+ 1. Submit your changes.
+
+To remove an item from the list, follow those simple steps:
+
+ 1. Press the ```❌``` sign next to the share method you want to remove.
+ 1. Submit your changes.
# Shortcuts
-**TODO**
+To ease the use of the application, FreshRSS comes with a lot of predefined keyboard shortcuts.
+They allow actions to improve the user experience with a keyboard.
+
+Of course, if you're not satisfied with the key mapping, you can change you configuration to fit your needs.
+
+There are 4 types of shortcuts:
+
+ 1. Views: they allow switching views with ease.
+ 1. Navigation: they allow navigation through articles, feeds, and categories.
+ 1. Article actions: they allow interactions with an article, like sharing or opening it on the original web-site.
+ 1. Other actions: they allow other interactions with the application, like opening the user queries menu or accessing the documentation.
+
+It's worth noting that the share article action has two levels. Once you press the shortcut, a menu containing all the share options opens.
+To choose one share option, you need to select it by its number. When there is only one option, it's selected automatically though.
+
+The same process applies to the user queries.
+
+Be aware that there is no validation on the selected shortcuts.
+This means that if you assign a shortcut to more than one action, you'll end up with some unexpected behavior.
# User queries
-**TODO**
+You can configure your [user queries](./03_Main_view.md) in that section. There is not much to say here as it is pretty straightforward.
+You can only change user query titles or drop them.
+
+At the moment, there is no helper to build a user query from here.
# Users
diff --git a/docs/en/users/06_Fever_API.md b/docs/en/users/06_Fever_API.md
new file mode 100644
index 000000000..6d8a103b9
--- /dev/null
+++ b/docs/en/users/06_Fever_API.md
@@ -0,0 +1,113 @@
+# FreshRSS - Fever API implementation
+
+See the [page about our Google Reader compatible API](06_Mobile_access.md) for another possibility
+and general aspects of API access.
+
+## RSS clients
+
+There are many RSS clients existing supporting Fever APIs but they seem to understand the Fever API a bit differently.
+If your favourite client does not work properly with this API, create an issue and we will have a look.
+But we can **only** do that for free clients.
+
+### Usage & Authentication
+
+Before you can start to use this API, you have to enable and setup API access, which is [documented here](https://freshrss.github.io/FreshRSS/en/users/06_Mobile_access.html),
+and then re-set the user’s API password.
+
+Then point your mobile application to the URL of `fever.php` (e.g. `https://freshrss.example.net/api/fever.php`).
+
+## Compatibility
+
+Tested with:
+
+- 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)
+
+- Android
+ -Until now, we don't know about compatible Android clients. Please leave your feedback, if you tested the Fever API with Android apps.
+ - Please note, that *Press* is NOT compatible: it was a popular RSS client with Fever support, but its development stopped a while ago. It uses the Fever API in a wrong way, which we don't support.
+
+## Features
+
+Following features are implemented:
+
+- fetching categories
+- fetching feeds
+- fetching RSS items (new, favorites, unread, by_id, by_feed, by_category, since)
+- fetching favicons
+- setting read marker for item(s)
+- setting starred marker for item(s)
+- setting read marker for feed
+- setting read marker for category
+- supports FreshRSS extensions, which use th `entry_before_display` hook
+
+Following features are not supported:
+- **Hot Links** aka **hot** as there is nothing in FreshRSS yet that is similar or could be used to simulate it
+
+## Testing and error search
+
+If this API does not work as expected in your RSS reader, you can test it manually with a tool like [Postman](https://www.getpostman.com/).
+
+Configure a POST request to the URL https://freshrss.example.net/api/fever.php?api which should give you the result:
+```json
+{
+ "api_version": 3,
+ "auth": 0
+}
+```
+Great, the base setup seems to work!
+
+Now lets try an authenticated call. Fever uses an `api_key`, which is the MD5 hash of `"$username:$apiPassword"`.
+Assuming the user is `kevin` and the password `freshrss`, here is a command-line example to compute the resulting `api_key`
+
+```sh
+api_key=`echo -n "kevin:freshrss" | md5sum | cut -d' ' -f1`
+```
+
+Add a body to your POST request encoded as `form-data` and one key named `api_key` with the value `your-password-hash`:
+
+```sh
+curl -s -F "api_key=$api_key" 'https://freshrss.example.net/api/fever.php?api'
+```
+
+This shoud give:
+```json
+{
+ "api_version": 3,
+ "auth": 1,
+ "last_refreshed_on_time": "1520013061"
+}
+```
+Perfect, you are authenticated and can now start testing the more advanced features. Therefor change the URL and append the possible API actions to your request parameters. Check the [original Fever documentation](https://feedafever.com/api) for more infos.
+
+Some basic calls are:
+
+- https://freshrss.example.net/api/fever.php?api&items
+- https://freshrss.example.net/api/fever.php?api&feeds
+- https://freshrss.example.net/api/fever.php?api&groups
+- https://freshrss.example.net/api/fever.php?api&unread_item_ids
+- https://freshrss.example.net/api/fever.php?api&saved_item_ids
+- https://freshrss.example.net/api/fever.php?api&items&since_id=some_id
+- https://freshrss.example.net/api/fever.php?api&items&max_id=some_id
+- https://freshrss.example.net/api/fever.php?api&mark=item&as=read&id=some_id
+- https://freshrss.example.net/api/fever.php?api&mark=item&as=unread&id=some_id
+
+Replace `some_id` with a real ID from your `freshrss_username_entry` database.
+
+### Debugging
+
+If nothing helps and your clients still misbehaves, add these lines to the start of `fever.api`:
+
+```php
+file_put_contents(__DIR__ . '/fever.log', $_SERVER['HTTP_USER_AGENT'] . ': ' . json_encode($_REQUEST) . PHP_EOL, FILE_APPEND);
+```
+
+Then use your RSS client to query the API and afterwards check the file `fever.log`.
+
+## Credits
+
+This plugin was inspired by the [tinytinyrss-fever-plugin](https://github.com/dasmurphy/tinytinyrss-fever-plugin).
diff --git a/docs/en/users/06_Mobile_access.md b/docs/en/users/06_Mobile_access.md
index 166985585..c354f98f0 100644
--- a/docs/en/users/06_Mobile_access.md
+++ b/docs/en/users/06_Mobile_access.md
@@ -7,6 +7,9 @@ This page assumes you have completed the [server setup](../admins/02_Installatio
* Every user must define an API password.
* The reason for an API-specific password is that it may be used in less safe situations than the main password, and does not grant access to as many things.
+The rest of this page is about the Google Reader compatible API.
+See the [page about the Fever compatible API](06_Fever_API.md) for another possibility.
+
# Testing
@@ -50,3 +53,34 @@ This page assumes you have completed the [server setup](../admins/02_Installatio
* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Open source, [F-Droid](https://f-droid.org/packages/org.freshrss.easyrss/))
* Linux
* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source)
+
+
+# Google Reader compatible API
+
+Examples of basic queries:
+
+```sh
+# Initial login, using API password (Email and Passwd can be given either as GET, or POST - better)
+curl 'https://freshrss.example.net/api/greader.php/accounts/ClientLogin?Email=alice&Passwd=Abcdef123456'
+SID=alice/8e6845e089457af25303abc6f53356eb60bdb5f8
+Auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8
+
+# Examples of read-only requests
+curl -s -H "Authorization:GoogleLogin auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8" \
+ 'https://freshrss.example.net/api/greader.php/reader/api/0/subscription/list?output=json'
+
+curl -s -H "Authorization:GoogleLogin auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8" \
+ 'https://freshrss.example.net/api/greader.php/reader/api/0/unread-count?output=json'
+
+curl -s -H "Authorization:GoogleLogin auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8" \
+ 'https://freshrss.example.net/api/greader.php/reader/api/0/tag/list?output=json'
+
+# Retrieve a token for requests making modifications
+curl -H "Authorization:GoogleLogin auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8" \
+ 'https://freshrss.example.net/api/greader.php/reader/api/0/token'
+8e6845e089457af25303abc6f53356eb60bdb5f8ZZZZZZZZZZZZZZZZZ
+
+# Get articles, piped to jq for easier JSON reading
+curl -s -H "Authorization:GoogleLogin auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8" \
+ 'https://freshrss.example.net/api/greader.php/reader/api/0/stream/contents/reading-list' | jq .
+```
diff --git a/docs/en/users/07_Frequently_Asked_Questions.md b/docs/en/users/07_Frequently_Asked_Questions.md
index 132b2e7ec..42156b1a9 100644
--- a/docs/en/users/07_Frequently_Asked_Questions.md
+++ b/docs/en/users/07_Frequently_Asked_Questions.md
@@ -43,4 +43,12 @@ Since [1.8.0](https://github.com/FreshRSS/FreshRSS/releases/tag/1.8.0) release,
```sh
./cli/update_user.php --user <username> --password <password>
```
-For more information on that matter, there is a [dedicated documentation](../../cli/README.md). \ No newline at end of file
+For more information on that matter, there is a [dedicated documentation](../../cli/README.md).
+
+## Permissions under SELinux
+
+Some Linux distribution like Fedora or RedHat Enterprise Linux have SELinux system enabled. This acts like a firewall application, so all applications cannot write/modify files under certain conditions. While installing FreshRSS, step 2 can fail if the httpd process cannot write to some data sub-directories, the following command should be executed as root :
+```sh
+semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/FreshRSS/data(/.*)?'
+restorecon -Rv /usr/share/FreshRSS/data
+```
diff --git a/docs/fr/users/03_Main_view.md b/docs/fr/users/03_Main_view.md
index ebf782136..af3a5a1db 100644
--- a/docs/fr/users/03_Main_view.md
+++ b/docs/fr/users/03_Main_view.md
@@ -180,4 +180,7 @@ Attention à ne pas introduire d’espace entre l’opérateur et la valeur rech
Certains opérateurs peuvent être utilisé négativement, pour exclure des articles, avec la même syntaxe que ci-dessus, mais préfixé par `!` ou `-` :
`-author:nom`, `-intitle:mot`, `-inurl:mot`, `-#tag`, `!mot`.
-Il est également possible de combiner les mots-clefs pour faire un filtrage encore plus précis, and et il est autorisé d’avoir plusieurs instances de : `author:`, `intitle:`, `inurl:`, `#`, et texte libre.
+Il est également possible de combiner les mots-clefs pour faire un filtrage encore plus précis, et il est autorisé d’avoir plusieurs instances de : `author:`, `intitle:`, `inurl:`, `#`, et texte libre.
+
+Combiner plusieurs critères implique un *et* logique, mais le mot clef ` OR ` peut être utiliser pour combiner plusieurs critères avec un *ou* logique :
+`author:Dupont OR author:Dupond`
diff --git a/docs/fr/users/06_Fever_API.md b/docs/fr/users/06_Fever_API.md
new file mode 100644
index 000000000..6ad817041
--- /dev/null
+++ b/docs/fr/users/06_Fever_API.md
@@ -0,0 +1,19 @@
+# FreshRSS - API compatible Fever
+
+Voir la page [sur notre API compatible Google Reader](06_Mobile_access.md) pour une autre possibilité
+et des généralités sur l’accès par API.
+
+## Compatibilité
+
+Testé avec:
+
+- 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)
+
+## TODO
+
+Voir [la page en anglais](../../en/users/06_Fever_API.md).
diff --git a/docs/fr/users/06_Mobile_access.md b/docs/fr/users/06_Mobile_access.md
index 8ef3d038a..b8e7c31f0 100644
--- a/docs/fr/users/06_Mobile_access.md
+++ b/docs/fr/users/06_Mobile_access.md
@@ -7,6 +7,9 @@ Cette page suppose que vous avez fini [l’installation du serveur](01_Installat
* Chaque utilisateur doit choisir son mot de passe API.
* La raison d’être d’un mot de passe API différent du mot de passe principal est que le mot de passe API est potentiellement utilisé de manière moins sûre, mais il permet aussi moins de choses.
+Le reste de cette page concerne l’API compatible Google Reader.
+Voir la [page sur l’API compatible Fever](06_Fever_API.md) pour une autre possibilité.
+
# Tester
@@ -17,7 +20,7 @@ Cette page suppose que vous avez fini [l’installation du serveur](01_Installat
* Si vous obtenez un autre message d’erreur, passer à l’étape 5.
-# Débogger la configuration du serveur
+# Déboguer la configuration du serveur
5. Cliquer sur le second lien “Check partial server configuration (without `%2F` support)”:
* Si vous obtenez `PASS`, alors le problème est bien que votre serveur n’accepte pas les slashs `/` qui sont encodés `%2F`.
@@ -48,3 +51,34 @@ Tout client supportant une API de type Google Reader. Sélection :
* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Libre, F-Droid)
* Linux
* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre)
+
+
+# API compatible Google Reader
+
+Exemples de requêtes simples:
+
+```sh
+# Authentification utilisant le mot de passe API (Email et Passwd peuvent être passés en GET, ou POST - mieux)
+curl 'https://freshrss.example.net/api/greader.php/accounts/ClientLogin?Email=alice&Passwd=Abcdef123456'
+SID=alice/8e6845e089457af25303abc6f53356eb60bdb5f8
+Auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8
+
+# Exemples de requêtes en lecture
+curl -s -H "Authorization:GoogleLogin auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8" \
+ 'https://freshrss.example.net/api/greader.php/reader/api/0/subscription/list?output=json'
+
+curl -s -H "Authorization:GoogleLogin auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8" \
+ 'https://freshrss.example.net/api/greader.php/reader/api/0/unread-count?output=json'
+
+curl -s -H "Authorization:GoogleLogin auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8" \
+ 'https://freshrss.example.net/api/greader.php/reader/api/0/tag/list?output=json'
+
+# Demande de jeton pour faire de requêtes de modification
+curl -H "Authorization:GoogleLogin auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8" \
+ 'https://freshrss.example.net/api/greader.php/reader/api/0/token'
+8e6845e089457af25303abc6f53356eb60bdb5f8ZZZZZZZZZZZZZZZZZ
+
+# Récupère les articles, envoyés à jq pour une lecture JSON plus facile
+curl -s -H "Authorization:GoogleLogin auth=alice/8e6845e089457af25303abc6f53356eb60bdb5f8" \
+ 'https://freshrss.example.net/api/greader.php/reader/api/0/stream/contents/reading-list' | jq .
+```
diff --git a/docs/fr/users/07_Frequently_Asked_Questions.md b/docs/fr/users/07_Frequently_Asked_Questions.md
index f27c92579..2dc2cae97 100644
--- a/docs/fr/users/07_Frequently_Asked_Questions.md
+++ b/docs/fr/users/07_Frequently_Asked_Questions.md
@@ -44,3 +44,11 @@ Depuis la version [1.8.0](https://github.com/FreshRSS/FreshRSS/releases/tag/1.8.
./cli/update_user.php --user <username> --password <password>
```
Pour plus d'information à ce sujet, il existe la [documentation dédiée](../../cli/README.md).
+
+## Gérer les permissions sous SELinux
+
+Certaines distributions Linux comme Fedora ou RedHat Enterprise Linux (RHEL) activent par défaut le système SELinux. Celui-ci permet de gérer des permissions au niveau des processus. Lors de l'installation de FreshRSS, l'étape 2 procède à la vérification des droits sur certains répertoires, il faut donc exécuter la commande suivante en tant que root:
+```sh
+semanage fcontext -a -t httpd_sys_rw_content_t '/usr/share/FreshRSS/data(/.*)?'
+restorecon -Rv /usr/share/FreshRSS/data
+```
diff --git a/docs/img/FreshRSS-logo-font.svg b/docs/img/FreshRSS-logo-font.svg
new file mode 100644
index 000000000..134adfdfc
--- /dev/null
+++ b/docs/img/FreshRSS-logo-font.svg
@@ -0,0 +1,16 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 256"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>Logo FreshRSS</title>
+ <circle fill="#0062BE" cx="128" cy="128" r="33"/>
+ <g fill="none" stroke="#0062BE" stroke-width="24">
+ <g stroke-opacity="0.3">
+ <path d="M12,128 A116,116 0 1,1 128,244"/>
+ <path d="M54,128 A74,74 0 1,1 128,202"/>
+ </g>
+ <path d="M128,12 A116,116 0 0,1 244,128"/>
+ <path d="M128,54 A74,74 0 0,1 202,128"/>
+ </g>
+ <text x="309" y="206" fill="#0062bd" stroke-width="0"
+ font-family="'Open Sans'" font-weight="600" font-size="213">FreshRSS</text>
+</svg>
+
diff --git a/docs/img/FreshRSS-logo.svg b/docs/img/FreshRSS-logo.svg
new file mode 100644
index 000000000..4eb268c8a
--- /dev/null
+++ b/docs/img/FreshRSS-logo.svg
@@ -0,0 +1,25 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 256"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>Logo FreshRSS</title>
+ <circle fill="#0062BE" cx="128" cy="128" r="33"/>
+ <g fill="none" stroke="#0062BE" stroke-width="24">
+ <g stroke-opacity="0.3">
+ <path d="M12,128 A116,116 0 1,1 128,244"/>
+ <path d="M54,128 A74,74 0 1,1 128,202"/>
+ </g>
+ <path d="M128,12 A116,116 0 0,1 244,128"/>
+ <path d="M128,54 A74,74 0 0,1 202,128"/>
+ </g>
+ <g fill="#0062bd" stroke-width="0" font-family="'Open Sans'" font-weight="600" font-size="213">
+ <desc>FreshRSS</desc>
+ <path d="m 353.72168,206 -24.64893,0 0,-152.053711 85.80323,0 0,21.008789 -61.1543,0 0,47.529782 57.30615,0 0,21.1128 -57.30615,0 0,62.40234 z" />
+ <path d="m 498.39111,88.891602 q 7.38428,0 12.16846,1.040039 l -2.39209,22.776859 q -5.20019,-1.24805 -10.81641,-1.24805 -14.66455,0 -23.81689,9.56836 -9.04834,9.56836 -9.04834,24.85693 l 0,60.11426 -24.44092,0 0,-115.02832 19.13672,0 3.22412,20.28076 1.24805,0 q 5.72021,-10.29639 14.87256,-16.328612 9.25634,-6.032226 19.86474,-6.032226 z" />
+ <path d="m 582.21826,208.08008 q -26.83301,0 -42.01758,-15.60059 -15.08056,-15.70459 -15.08056,-43.16162 0,-28.18506 14.04052,-44.30566 14.04053,-16.120608 38.58545,-16.120608 22.77686,0 35.98536,13.832518 13.20849,13.83252 13.20849,38.06543 l 0,13.2085 -76.65088,0 q 0.52002,16.74463 9.04834,25.79297 8.52832,8.94433 24.0249,8.94433 10.19239,0 18.92872,-1.87207 8.84033,-1.97607 18.92871,-6.44824 l 0,19.86475 q -8.94434,4.26416 -18.09668,6.03222 -9.15235,1.76807 -20.90479,1.76807 z M 577.74609,107.4043 q -11.64843,0 -18.7207,7.38427 -6.96826,7.38428 -8.32031,21.52881 l 52.20996,0 q -0.20801,-14.24853 -6.86426,-21.52881 -6.65625,-7.38427 -18.30469,-7.38427 z" />
+ <path d="m 731.98389,173.23877 q 0,16.84863 -12.27246,25.89697 -12.27247,8.94434 -35.15332,8.94434 -22.98487,0 -36.92139,-6.96826 l 0,-21.1128 q 20.28076,9.36035 37.75342,9.36035 22.56884,0 22.56884,-13.62451 0,-4.36816 -2.49609,-7.28027 -2.49609,-2.91211 -8.21631,-6.03223 -5.72021,-3.12011 -15.9126,-7.07226 -19.86474,-7.69629 -26.93701,-15.39258 -6.96826,-7.69629 -6.96826,-19.96875 0,-14.76856 11.85645,-22.88086 11.96045,-8.216308 32.44921,-8.216308 20.28077,0 38.37745,8.216308 l -7.9043,18.40869 q -18.6167,-7.69629 -31.30518,-7.69629 -19.34472,0 -19.34472,11.02442 0,5.4082 4.99218,9.15234 5.0962,3.74414 22.04883,10.29639 14.24854,5.5122 20.69678,10.08838 6.44824,4.57617 9.56836,10.60839 3.12012,5.92823 3.12012,14.24854 z" />
+ <path d="m 859.49268,206 -24.54493,0 0,-70.72266 q 0,-13.3125 -5.4082,-19.86474 -5.3042,-6.55225 -16.95264,-6.55225 -15.39257,0 -22.67285,9.25635 -7.17627,9.15234 -7.17627,30.78516 l 0,57.09814 -24.44092,0 0,-161.830078 24.44092,0 0,41.081543 q 0,9.880371 -1.24804,21.112795 l 1.56005,0 q 4.99219,-8.320315 13.83252,-12.896487 8.94434,-4.576171 20.80079,-4.576171 41.80957,0 41.80957,42.121578 l 0,74.98682 z" />
+ <path d="m 920.95898,124.66895 17.26465,0 q 17.36866,0 25.16895,-6.44825 7.80029,-6.44824 7.80029,-19.136716 0,-12.896484 -8.42432,-18.512695 -8.42431,-5.616211 -25.37695,-5.616211 l -16.43262,0 0,49.713872 z m 0,20.59277 0,60.73828 -24.85693,0 0,-152.053711 42.95361,0 q 29.43311,0 43.57764,11.024414 14.14453,11.024414 14.14453,33.28125 0,28.393067 -29.53711,40.457517 L 1010.1943,206 l -28.28903,0 -36.40136,-60.73828 -24.54493,0 z" />
+ <path d="m 1119.8145,164.71045 q 0,20.28076 -14.6646,31.82519 -14.6645,11.54444 -40.4575,11.54444 -25.793,0 -42.2256,-8.0083 l 0,-23.50489 q 10.4004,4.88819 22.0488,7.69629 11.7525,2.80811 21.8408,2.80811 14.7686,0 21.7369,-5.61621 7.0722,-5.61621 7.0722,-15.08057 0,-8.52832 -6.4482,-14.45654 -6.4483,-5.92822 -26.625,-14.04053 -20.8008,-8.42432 -29.3291,-19.24072 -8.5283,-10.81641 -8.5283,-26.000978 0,-19.032715 13.5205,-29.953125 13.5205,-10.92041 36.2973,-10.92041 21.8409,0 43.4737,9.568359 l -7.9043,20.280762 q -20.2808,-8.52832 -36.1934,-8.52832 -12.0644,0 -18.3047,5.304199 -6.2402,5.200195 -6.2402,13.83252 0,5.928222 2.4961,10.192383 2.4961,4.16016 8.2163,7.9043 5.7202,3.74414 20.5928,9.88037 16.7446,6.96826 24.5449,13.00049 7.8003,6.03222 11.4404,13.62451 3.6402,7.59228 3.6402,17.88867 z" />
+ <path d="m 1237.1309,164.71045 q 0,20.28076 -14.6646,31.82519 -14.6645,11.54444 -40.4575,11.54444 -25.793,0 -42.2256,-8.0083 l 0,-23.50489 q 10.4004,4.88819 22.0488,7.69629 11.7525,2.80811 21.8409,2.80811 14.7685,0 21.7368,-5.61621 7.0722,-5.61621 7.0722,-15.08057 0,-8.52832 -6.4482,-14.45654 -6.4483,-5.92822 -26.625,-14.04053 -20.8008,-8.42432 -29.3291,-19.24072 -8.5283,-10.81641 -8.5283,-26.000978 0,-19.032715 13.5205,-29.953125 13.5205,-10.92041 36.2973,-10.92041 21.8409,0 43.4737,9.568359 l -7.9043,20.280762 q -20.2808,-8.52832 -36.1934,-8.52832 -12.0644,0 -18.3047,5.304199 -6.2402,5.200195 -6.2402,13.83252 0,5.928222 2.4961,10.192383 2.4961,4.16016 8.2163,7.9043 5.7202,3.74414 20.5928,9.88037 16.7446,6.96826 24.5449,13.00049 7.8003,6.03222 11.4404,13.62451 3.6402,7.59228 3.6402,17.88867 z" />
+ </g>
+</svg>
+
diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php
index d769e0ff4..733982c14 100644
--- a/lib/Minz/ModelPdo.php
+++ b/lib/Minz/ModelPdo.php
@@ -58,7 +58,7 @@ class Minz_ModelPdo {
try {
switch ($db['type']) {
case 'mysql':
- $string = 'mysql:host=' . $dbServer['host'] . ';dbname=' . $db['base'] . ';charset=utf8mb4';
+ $string = 'mysql:host=' . (empty($dbServer['host']) ? $db['host'] : $dbServer['host']) . ';dbname=' . $db['base'] . ';charset=utf8mb4';
if (!empty($dbServer['port'])) {
$string .= ';port=' . $dbServer['port'];
}
@@ -69,11 +69,11 @@ class Minz_ModelPdo {
case 'sqlite':
$string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite');
$this->prefix = '';
- $this->bd = new MinzPDOMSQLite($string, $db['user'], $db['password'], $driver_options);
+ $this->bd = new MinzPDOSQLite($string, $db['user'], $db['password'], $driver_options);
$this->bd->exec('PRAGMA foreign_keys = ON;');
break;
case 'pgsql':
- $string = 'pgsql:host=' . $dbServer['host'] . ';dbname=' . $db['base'];
+ $string = 'pgsql:host=' . (empty($dbServer['host']) ? $db['host'] : $dbServer['host']) . ';dbname=' . $db['base'];
if (!empty($dbServer['port'])) {
$string .= ';port=' . $dbServer['port'];
}
@@ -160,7 +160,7 @@ class MinzPDOMySql extends MinzPDO {
}
}
-class MinzPDOMSQLite extends MinzPDO {
+class MinzPDOSQLite extends MinzPDO {
public function lastInsertId($name = null) {
return parent::lastInsertId(); //We discard the name, only used by PostgreSQL
}
diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php
index a43509ded..e21697e42 100644
--- a/lib/Minz/Request.php
+++ b/lib/Minz/Request.php
@@ -39,6 +39,19 @@ class Minz_Request {
return $default;
}
}
+ public static function paramTernary($key) {
+ if (isset(self::$params[$key])) {
+ $p = self::$params[$key];
+ $tp = trim($p);
+ if ($p === null || $tp === '' || $tp === 'null') {
+ return null;
+ } elseif ($p == false || $tp == '0' || $tp === 'false' || $tp === 'no') {
+ return false;
+ }
+ return true;
+ }
+ return null;
+ }
public static function defaultControllerName() {
return self::$default_controller_name;
}
diff --git a/lib/SimplePie/SimplePie/File.php b/lib/SimplePie/SimplePie/File.php
index 8be38f145..b8a595571 100644
--- a/lib/SimplePie/SimplePie/File.php
+++ b/lib/SimplePie/SimplePie/File.php
@@ -110,7 +110,7 @@ class SimplePie_File
curl_setopt($fp, CURLOPT_FAILONERROR, 1);
curl_setopt($fp, CURLOPT_TIMEOUT, $timeout);
curl_setopt($fp, CURLOPT_CONNECTTIMEOUT, $timeout);
- curl_setopt($fp, CURLOPT_REFERER, $url);
+ curl_setopt($fp, CURLOPT_REFERER, SimplePie_Misc::url_remove_credentials($url));
curl_setopt($fp, CURLOPT_USERAGENT, $useragent);
curl_setopt($fp, CURLOPT_HTTPHEADER, $headers2);
if (!ini_get('open_basedir') && !ini_get('safe_mode') && version_compare(SimplePie_Misc::get_curl_version(), '7.15.2', '>='))
diff --git a/lib/SimplePie/SimplePie/Misc.php b/lib/SimplePie/SimplePie/Misc.php
index 40477c01e..1f338623c 100644
--- a/lib/SimplePie/SimplePie/Misc.php
+++ b/lib/SimplePie/SimplePie/Misc.php
@@ -1928,9 +1928,18 @@ class SimplePie_Misc
public static function atom_10_content_construct_type($attribs)
{
+ $type = '';
if (isset($attribs['']['type']))
{
- $type = strtolower(trim($attribs['']['type']));
+ $type = trim($attribs['']['type']);
+ }
+ elseif (isset($attribs[SIMPLEPIE_NAMESPACE_ATOM_10]['type']))
+ {//FreshRSS
+ $type = trim($attribs[SIMPLEPIE_NAMESPACE_ATOM_10]['type']);
+ }
+ if ($type != '')
+ {
+ $type = strtolower($type);
switch ($type)
{
case 'text':
diff --git a/lib/SimplePie/SimplePie/Parser.php b/lib/SimplePie/SimplePie/Parser.php
index 9348382ad..bdcc1a516 100644
--- a/lib/SimplePie/SimplePie/Parser.php
+++ b/lib/SimplePie/SimplePie/Parser.php
@@ -141,7 +141,7 @@ class SimplePie_Parser
$dom = new DOMDocument();
$dom->recover = true;
$dom->strictErrorChecking = false;
- @$dom->loadXML($data);
+ @$dom->loadXML($data, LIBXML_NOERROR | LIBXML_NOWARNING);
$this->encoding = $encoding = $dom->encoding = 'UTF-8';
$data2 = $dom->saveXML();
if (function_exists('mb_convert_encoding'))
diff --git a/lib/lib_rss.php b/lib/lib_rss.php
index 215c4c362..abb20f16a 100644
--- a/lib/lib_rss.php
+++ b/lib/lib_rss.php
@@ -175,7 +175,7 @@ function html_only_entity_decode($text) {
return strtr($text, $htmlEntitiesOnly);
}
-function customSimplePie() {
+function customSimplePie($attributes = array()) {
$system_conf = Minz_Configuration::get('system');
$limits = $system_conf->limits;
$simplePie = new SimplePie();
@@ -183,8 +183,17 @@ function customSimplePie() {
$simplePie->set_syslog($system_conf->simplepie_syslog_enabled);
$simplePie->set_cache_location(CACHE_PATH);
$simplePie->set_cache_duration($limits['cache_duration']);
- $simplePie->set_timeout($limits['timeout']);
- $simplePie->set_curl_options($system_conf->curl_options);
+
+ $feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']);
+ $simplePie->set_timeout($feed_timeout > 0 ? $feed_timeout : $limits['timeout']);
+
+ $curl_options = $system_conf->curl_options;
+ if (isset($attributes['ssl_verify'])) {
+ $curl_options[CURLOPT_SSL_VERIFYHOST] = $attributes['ssl_verify'] ? 2 : 0;
+ $curl_options[CURLOPT_SSL_VERIFYPEER] = $attributes['ssl_verify'] ? true : false;
+ }
+ $simplePie->set_curl_options($curl_options);
+
$simplePie->strip_htmltags(array(
'base', 'blink', 'body', 'doctype', 'embed',
'font', 'form', 'frame', 'frameset', 'html',
@@ -245,11 +254,47 @@ function sanitizeHTML($data, $base = '') {
}
/* permet de récupérer le contenu d'un article pour un flux qui n'est pas complet */
-function get_content_by_parsing ($url, $path) {
+function get_content_by_parsing($url, $path, $attributes = array()) {
require_once(LIB_PATH . '/lib_phpQuery.php');
+ $system_conf = Minz_Configuration::get('system');
+ $limits = $system_conf->limits;
+ $feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']);
+
+ if ($system_conf->simplepie_syslog_enabled) {
+ syslog(LOG_INFO, 'FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url));
+ }
- Minz_Log::notice('FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url));
- $html = file_get_contents($url);
+ $ch = curl_init();
+ curl_setopt_array($ch, array(
+ CURLOPT_URL => $url,
+ CURLOPT_REFERER => SimplePie_Misc::url_remove_credentials($url),
+ CURLOPT_HTTPHEADER => array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
+ CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
+ CURLOPT_CONNECTTIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
+ CURLOPT_TIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
+ //CURLOPT_FAILONERROR => true;
+ CURLOPT_MAXREDIRS => 4,
+ CURLOPT_RETURNTRANSFER => true,
+ ));
+ if (version_compare(PHP_VERSION, '5.6.0') >= 0 || ini_get('open_basedir') == '') {
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); //Keep option separated for open_basedir PHP bug 65646
+ }
+ if (defined('CURLOPT_ENCODING')) {
+ curl_setopt($ch, CURLOPT_ENCODING, ''); //Enable all encodings
+ }
+ curl_setopt_array($ch, $system_conf->curl_options);
+ if (isset($attributes['ssl_verify'])) {
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $attributes['ssl_verify'] ? 2 : 0);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $attributes['ssl_verify'] ? true : false);
+ }
+ $html = curl_exec($ch);
+ $c_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $c_error = curl_error($ch);
+ curl_close($ch);
+
+ if ($c_status != 200 || $c_error != '') {
+ Minz_Log::warning('Error fetching content: HTTP code ' . $c_status . ': ' . $c_error . ' ' . $url);
+ }
if ($html) {
$doc = phpQuery::newDocument($html);
diff --git a/p/api/fever.php b/p/api/fever.php
new file mode 100644
index 000000000..6c9e2085d
--- /dev/null
+++ b/p/api/fever.php
@@ -0,0 +1,640 @@
+<?php
+/**
+ * Fever API for FreshRSS
+ * Version 0.1
+ * Author: Kevin Papst / https://github.com/kevinpapst
+ *
+ * Inspired by:
+ * TinyTinyRSS Fever API plugin @dasmurphy
+ * See https://github.com/dasmurphy/tinytinyrss-fever-plugin
+ */
+
+// ================================================================================================
+// BOOTSTRAP FreshRSS
+require(__DIR__ . '/../../constants.php');
+require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
+Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
+
+// check if API is enabled globally
+FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
+if (!FreshRSS_Context::$system_conf->api_enabled) {
+ Minz_Log::warning('Fever API: serviceUnavailable() ' . debugInfo(), API_LOG);
+ header('HTTP/1.1 503 Service Unavailable');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Service Unavailable!');
+}
+
+ini_set('session.use_cookies', '0');
+register_shutdown_function('session_destroy');
+Minz_Session::init('FreshRSS');
+// ================================================================================================
+
+
+class FeverAPI_EntryDAO extends FreshRSS_EntryDAO
+{
+ /**
+ * @return array
+ */
+ public function countFever()
+ {
+ $values = array(
+ 'total' => 0,
+ 'min' => 0,
+ 'max' => 0,
+ );
+ $sql = 'SELECT COUNT(id) as `total`, MIN(id) as `min`, MAX(id) as `max` FROM `' . $this->prefix . 'entry`';
+ $stm = $this->bd->prepare($sql);
+ $stm->execute();
+ $result = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+ if (!empty($result[0])) {
+ $values = $result[0];
+ }
+
+ return $values;
+ }
+
+ /**
+ * @param string $prefix
+ * @param array $values
+ * @param array $bindArray
+ * @return string
+ */
+ protected function bindParamArray($prefix, $values, &$bindArray)
+ {
+ $str = '';
+ for ($i = 0; $i < count($values); $i++) {
+ $str .= ':' . $prefix . $i . ',';
+ $bindArray[$prefix . $i] = $values[$i];
+ }
+ return rtrim($str, ',');
+ }
+
+ /**
+ * @param array $feed_ids
+ * @param array $entry_ids
+ * @param int|null $max_id
+ * @param int|null $since_id
+ * @return FreshRSS_Entry[]
+ */
+ public function findEntries(array $feed_ids, array $entry_ids, $max_id, $since_id)
+ {
+ $values = array();
+ $order = '';
+
+ $sql = 'SELECT id, guid, title, author, '
+ . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
+ . ', link, date, is_read, is_favorite, id_feed, tags '
+ . 'FROM `' . $this->prefix . 'entry` WHERE';
+
+ if (!empty($entry_ids)) {
+ $bindEntryIds = $this->bindParamArray("id", $entry_ids, $values);
+ $sql .= " id IN($bindEntryIds)";
+ } else if (!empty($max_id)) {
+ $sql .= ' id < :id';
+ $values[':id'] = $max_id;
+ $order = ' ORDER BY id DESC';
+ } else {
+ $sql .= ' id > :id';
+ $values[':id'] = $since_id;
+ $order = ' ORDER BY id ASC';
+ }
+
+ if (!empty($feed_ids)) {
+ $bindFeedIds = $this->bindParamArray("feed", $feed_ids, $values);
+ $sql .= " AND id_feed IN($bindFeedIds)";
+ }
+
+ $sql .= $order;
+ $sql .= ' LIMIT 50';
+
+ $stm = $this->bd->prepare($sql);
+ $stm->execute($values);
+ $result = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+ $entries = array();
+ foreach ($result as $dao) {
+ $entries[] = self::daoToEntry($dao);
+ }
+
+ return $entries;
+ }
+}
+
+/**
+ * Class FeverAPI
+ */
+class FeverAPI
+{
+ const API_LEVEL = 3;
+ const STATUS_OK = 1;
+ const STATUS_ERR = 0;
+
+ /**
+ * Authenticate the user
+ *
+ * API Password sent from client is the result of the md5 sum of
+ * your FreshRSS "username:your-api-password" combination
+ */
+ private function authenticate()
+ {
+ FreshRSS_Context::$user_conf = null;
+ Minz_Session::_param('currentUser');
+ $feverKey = empty($_POST['api_key']) ? '' : substr(trim($_POST['api_key']), 0, 128);
+ if (ctype_xdigit($feverKey)) {
+ $feverKey = strtolower($feverKey);
+ $username = @file_get_contents(DATA_PATH . '/fever/.key-' . sha1(FreshRSS_Context::$system_conf->salt) . '-' . $feverKey . '.txt', false);
+ if ($username != false) {
+ $username = trim($username);
+ Minz_Session::_param('currentUser', $username);
+ $user_conf = get_user_configuration($username);
+ if ($user_conf != null && $feverKey === $user_conf->feverKey) {
+ FreshRSS_Context::$user_conf = $user_conf;
+ return true;
+ }
+ Minz_Log::error('Fever API: Reset API password for user: ' . $username, API_LOG);
+ Minz_Log::error('Fever API: Please reset your API password!');
+ Minz_Session::_param('currentUser');
+ }
+ Minz_Log::warning('Fever API: wrong credentials! ' . $feverKey, API_LOG);
+ }
+ return false;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isAuthenticatedApiUser()
+ {
+ $this->authenticate();
+
+ if (FreshRSS_Context::$user_conf !== null) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return FreshRSS_FeedDAO
+ */
+ protected function getDaoForFeeds()
+ {
+ return new FreshRSS_FeedDAO();
+ }
+
+ /**
+ * @return FreshRSS_CategoryDAO
+ */
+ protected function getDaoForCategories()
+ {
+ return new FreshRSS_CategoryDAO();
+ }
+
+ /**
+ * @return FeverAPI_EntryDAO
+ */
+ protected function getDaoForEntries()
+ {
+ return new FeverAPI_EntryDAO();
+ }
+
+ /**
+ * This does all the processing, since the fever api does not have a specific variable that specifies the operation
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function process()
+ {
+ $response_arr = array();
+
+ if (!$this->isAuthenticatedApiUser()) {
+ throw new Exception('No user given or user is not allowed to access API');
+ }
+
+ if (isset($_REQUEST["groups"])) {
+ $response_arr["groups"] = $this->getGroups();
+ $response_arr["feeds_groups"] = $this->getFeedsGroup();
+ }
+
+ if (isset($_REQUEST["feeds"])) {
+ $response_arr["feeds"] = $this->getFeeds();
+ $response_arr["feeds_groups"] = $this->getFeedsGroup();
+ }
+
+ if (isset($_REQUEST["favicons"])) {
+ $response_arr["favicons"] = $this->getFavicons();
+ }
+
+ if (isset($_REQUEST["items"])) {
+ $response_arr["total_items"] = $this->getTotalItems();
+ $response_arr["items"] = $this->getItems();
+ }
+
+ if (isset($_REQUEST["links"])) {
+ $response_arr["links"] = $this->getLinks();
+ }
+
+ if (isset($_REQUEST["unread_item_ids"])) {
+ $response_arr["unread_item_ids"] = $this->getUnreadItemIds();
+ }
+
+ if (isset($_REQUEST["saved_item_ids"])) {
+ $response_arr["saved_item_ids"] = $this->getSavedItemIds();
+ }
+
+ if (isset($_REQUEST["mark"], $_REQUEST["as"], $_REQUEST["id"]) && is_numeric($_REQUEST["id"])) {
+ $method_name = "set" . ucfirst($_REQUEST["mark"]) . "As" . ucfirst($_REQUEST["as"]);
+ $allowedMethods = array(
+ 'setFeedAsRead', 'setGroupAsRead', 'setItemAsRead',
+ 'setItemAsSaved', 'setItemAsUnread', 'setItemAsUnsaved'
+ );
+ if (in_array($method_name, $allowedMethods)) {
+ $id = intval($_REQUEST["id"]);
+ switch (strtolower($_REQUEST["mark"])) {
+ case 'item':
+ $this->{$method_name}($id);
+ break;
+ case 'feed':
+ case 'group':
+ $before = (isset($_REQUEST["before"])) ? $_REQUEST["before"] : null;
+ $this->{$method_name}($id, $before);
+ break;
+ }
+
+ switch ($_REQUEST["as"]) {
+ case "read":
+ case "unread":
+ $response_arr["unread_item_ids"] = $this->getUnreadItemIds();
+ break;
+
+ case 'saved':
+ case 'unsaved':
+ $response_arr["saved_item_ids"] = $this->getSavedItemIds();
+ break;
+ }
+ }
+ }
+
+ return $response_arr;
+ }
+
+ /**
+ * Returns the complete JSON, with 'api_version' and status as 'auth'.
+ *
+ * @param int $status
+ * @param array $reply
+ * @return string
+ */
+ public function wrap($status, array $reply = array())
+ {
+ $arr = array('api_version' => self::API_LEVEL, 'auth' => $status);
+
+ if ($status === self::STATUS_OK) {
+ $arr['last_refreshed_on_time'] = (string) $this->lastRefreshedOnTime();
+ $arr = array_merge($arr, $reply);
+ }
+
+ return json_encode($arr);
+ }
+
+ /**
+ * every authenticated method includes last_refreshed_on_time
+ *
+ * @return int
+ */
+ protected function lastRefreshedOnTime()
+ {
+ $lastUpdate = 0;
+
+ $dao = $this->getDaoForFeeds();
+ $entries = $dao->listFeedsOrderUpdate(-1, 1);
+ $feed = current($entries);
+
+ if (!empty($feed)) {
+ $lastUpdate = $feed->lastUpdate();
+ }
+
+ return $lastUpdate;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getFeeds()
+ {
+ $feeds = array();
+
+ $dao = $this->getDaoForFeeds();
+ $myFeeds = $dao->listFeeds();
+
+ /** @var FreshRSS_Feed $feed */
+ foreach ($myFeeds as $feed) {
+ $feeds[] = array(
+ "id" => $feed->id(),
+ "favicon_id" => $feed->id(),
+ "title" => $feed->name(),
+ "url" => $feed->url(),
+ "site_url" => $feed->website(),
+ "is_spark" => 0, // unsupported
+ "last_updated_on_time" => $feed->lastUpdate()
+ );
+ }
+
+ return $feeds;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getGroups()
+ {
+ $groups = array();
+
+ $dao = $this->getDaoForCategories();
+ $categories = $dao->listCategories(false, false);
+
+ /** @var FreshRSS_Category $category */
+ foreach ($categories as $category) {
+ $groups[] = array(
+ 'id' => $category->id(),
+ 'title' => $category->name()
+ );
+ }
+
+ return $groups;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getFavicons()
+ {
+ $favicons = array();
+
+ $dao = $this->getDaoForFeeds();
+ $myFeeds = $dao->listFeeds();
+
+ $salt = FreshRSS_Context::$system_conf->salt;
+
+ /** @var FreshRSS_Feed $feed */
+ foreach ($myFeeds as $feed) {
+
+ $id = hash('crc32b', $salt . $feed->url());
+ $filename = DATA_PATH . '/favicons/' . $id . '.ico';
+ if (!file_exists($filename)) {
+ continue;
+ }
+
+ $favicons[] = array(
+ "id" => $feed->id(),
+ "data" => image_type_to_mime_type(exif_imagetype($filename)) . ";base64," . base64_encode(file_get_contents($filename))
+ );
+ }
+
+ return $favicons;
+ }
+
+ /**
+ * @return int
+ */
+ protected function getTotalItems()
+ {
+ $total_items = 0;
+
+ $dao = $this->getDaoForEntries();
+ $result = $dao->countFever();
+
+ if (!empty($result)) {
+ $total_items = $result['total'];
+ }
+
+ return $total_items;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getFeedsGroup()
+ {
+ $groups = array();
+ $ids = array();
+
+ $dao = $this->getDaoForFeeds();
+ $myFeeds = $dao->listFeeds();
+
+ /** @var FreshRSS_Feed $feed */
+ foreach ($myFeeds as $feed) {
+ $ids[$feed->category()][] = $feed->id();
+ }
+
+ foreach($ids as $category => $feedIds) {
+ $groups[] = array(
+ 'group_id' => $category,
+ 'feed_ids' => implode(',', $feedIds)
+ );
+ }
+
+ return $groups;
+ }
+
+ /**
+ * AFAIK there is no 'hot links' alternative in FreshRSS
+ * @return array
+ */
+ protected function getLinks()
+ {
+ return array();
+ }
+
+ /**
+ * @param array $ids
+ * @return string
+ */
+ protected function entriesToIdList($ids = array())
+ {
+ return implode(',', array_values($ids));
+ }
+
+ /**
+ * @return string
+ */
+ protected function getUnreadItemIds()
+ {
+ $dao = $this->getDaoForEntries();
+ $entries = $dao->listIdsWhere('a', '', FreshRSS_Entry::STATE_NOT_READ, 'ASC', 0);
+ return $this->entriesToIdList($entries);
+ }
+
+ /**
+ * @return string
+ */
+ protected function getSavedItemIds()
+ {
+ $dao = $this->getDaoForEntries();
+ $entries = $dao->listIdsWhere('a', '', FreshRSS_Entry::STATE_FAVORITE, 'ASC', 0);
+ return $this->entriesToIdList($entries);
+ }
+
+ protected function setItemAsRead($id)
+ {
+ $dao = $this->getDaoForEntries();
+ $dao->markRead($id, true);
+ }
+
+ protected function setItemAsUnread($id)
+ {
+ $dao = $this->getDaoForEntries();
+ $dao->markRead($id, false);
+ }
+
+ protected function setItemAsSaved($id)
+ {
+ $dao = $this->getDaoForEntries();
+ $dao->markFavorite($id, true);
+ }
+
+ protected function setItemAsUnsaved($id)
+ {
+ $dao = $this->getDaoForEntries();
+ $dao->markFavorite($id, false);
+ }
+
+ /**
+ * @return array
+ */
+ protected function getItems()
+ {
+ $feed_ids = array();
+ $entry_ids = array();
+ $max_id = null;
+ $since_id = null;
+
+ if (isset($_REQUEST["feed_ids"]) || isset($_REQUEST["group_ids"])) {
+ if (isset($_REQUEST["feed_ids"])) {
+ $feed_ids = explode(",", $_REQUEST["feed_ids"]);
+ }
+
+ $dao = $this->getDaoForCategories();
+ if (isset($_REQUEST["group_ids"])) {
+ $group_ids = explode(",", $_REQUEST["group_ids"]);
+ foreach ($group_ids as $id) {
+ /** @var FreshRSS_Category $category */
+ $category = $dao->searchById($id);
+ /** @var FreshRSS_Feed $feed */
+ foreach ($category->feeds() as $feed) {
+ $feeds[] = $feed->id();
+ }
+ }
+
+ $feed_ids = array_unique($feeds);
+ }
+ }
+
+ if (isset($_REQUEST["max_id"])) {
+ // use the max_id argument to request the previous $item_limit items
+ if (is_numeric($_REQUEST["max_id"])) {
+ $max = ($_REQUEST["max_id"] > 0) ? intval($_REQUEST["max_id"]) : 0;
+ if ($max) {
+ $max_id = $max;
+ }
+ }
+ } else if (isset($_REQUEST["with_ids"])) {
+ $entry_ids = explode(",", $_REQUEST["with_ids"]);
+ } else {
+ // use the since_id argument to request the next $item_limit items
+ $since_id = isset($_REQUEST["since_id"]) && is_numeric($_REQUEST["since_id"]) ? intval($_REQUEST["since_id"]) : 0;
+ }
+
+ $items = array();
+
+ $dao = $this->getDaoForEntries();
+ $entries = $dao->findEntries($feed_ids, $entry_ids, $max_id, $since_id);
+
+ // Load list of extensions and enable the "system" ones.
+ Minz_ExtensionManager::init();
+
+ foreach($entries as $item) {
+ /** @var FreshRSS_Entry $entry */
+ $entry = Minz_ExtensionManager::callHook('entry_before_display', $item);
+ if (is_null($entry)) {
+ continue;
+ }
+ $items[] = array(
+ "id" => $entry->id(),
+ "feed_id" => $entry->feed(false),
+ "title" => $entry->title(),
+ "author" => $entry->author(),
+ "html" => $entry->content(),
+ "url" => $entry->link(),
+ "is_saved" => $entry->isFavorite() ? 1 : 0,
+ "is_read" => $entry->isRead() ? 1 : 0,
+ "created_on_time" => $entry->date(true)
+ );
+ }
+
+ return $items;
+ }
+
+ /**
+ * TODO replace by a dynamic fetch for id <= $before timestamp
+ *
+ * @param int $beforeTimestamp
+ * @return int
+ */
+ protected function convertBeforeToId($beforeTimestamp)
+ {
+ // if before is zero, set it to now so feeds all items are read from before this point in time
+ if ($beforeTimestamp == 0) {
+ $before = time();
+ }
+ $before = PHP_INT_MAX;
+
+ return $before;
+ }
+
+ protected function setFeedAsRead($id, $before)
+ {
+ $before = $this->convertBeforeToId($before);
+ $dao = $this->getDaoForEntries();
+ return $dao->markReadFeed($id, $before);
+ }
+
+ protected function setGroupAsRead($id, $before)
+ {
+ $dao = $this->getDaoForEntries();
+
+ // special case to mark all items as read
+ if ($id === 0) {
+ $result = $dao->countFever();
+
+ if (!empty($result)) {
+ return $dao->markReadEntries($result['max']);
+ }
+ }
+
+ $before = $this->convertBeforeToId($before);
+ return $dao->markReadCat($id, $before);
+ }
+}
+
+// ================================================================================================
+// refresh is not allowed yet, probably we find a way to support it later
+if (isset($_REQUEST["refresh"])) {
+ Minz_Log::warning('Fever API: Refresh items - notImplemented()', API_LOG);
+ header('HTTP/1.1 501 Not Implemented');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Not Implemented!');
+}
+
+// Start the Fever API handling
+$handler = new FeverAPI();
+
+header("Content-Type: application/json; charset=UTF-8");
+
+if (!$handler->isAuthenticatedApiUser()) {
+ echo $handler->wrap(FeverAPI::STATUS_ERR, array());
+} else {
+ echo $handler->wrap(FeverAPI::STATUS_OK, $handler->process());
+}
diff --git a/p/api/greader.php b/p/api/greader.php
index 9778aecf5..5ab6c8115 100644
--- a/p/api/greader.php
+++ b/p/api/greader.php
@@ -535,7 +535,7 @@ function streamContents($path, $include_target, $start_time, $count, $order, $ex
}
$entryDAO = FreshRSS_Factory::createEntryDao();
- $entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_Search(''), $start_time);
+ $entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_BooleanSearch(''), $start_time);
$items = entriesToArray($entries);
@@ -595,7 +595,7 @@ function streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude
}
$entryDAO = FreshRSS_Factory::createEntryDao();
- $ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_Search(''), $start_time);
+ $ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_BooleanSearch(''), $start_time);
if ($continuation != '') {
array_shift($ids); //Discard first element that was already sent in the previous response
@@ -745,6 +745,8 @@ if (!FreshRSS_Context::$system_conf->api_enabled) {
serviceUnavailable();
}
+ini_set('session.use_cookies', '0');
+register_shutdown_function('session_destroy');
Minz_Session::init('FreshRSS');
$user = authorizationToUser();
diff --git a/p/api/index.php b/p/api/index.php
index 429b25225..108841819 100644
--- a/p/api/index.php
+++ b/p/api/index.php
@@ -26,5 +26,16 @@ echo Minz_Url::display('/api/greader.php', 'html', true);
configuration (without <code>%2F</code> support)</a></li>
</ul>
+<h2>Fever compatible API</h2>
+<dl>
+<dt>Your API address:</dt>
+<dd><?php
+echo Minz_Url::display('/api/fever.php', 'html', true);
+?></dd>
+</dl>
+<ul>
+<li><a href="fever.php?api" rel="nofollow">Test</a></li>
+</ul>
+
</body>
</html>
diff --git a/p/ext.php b/p/ext.php
index bb16d02d3..427bdc253 100644
--- a/p/ext.php
+++ b/p/ext.php
@@ -19,13 +19,14 @@ require(__DIR__ . '/../constants.php');
*/
function is_valid_path($path) {
// It must be under the extension path.
- $in_ext_path = (substr($path, 0, strlen(EXTENSIONS_PATH)) === EXTENSIONS_PATH);
+ $real_ext_path = realpath(EXTENSIONS_PATH);
+ $in_ext_path = (substr($path, 0, strlen($real_ext_path)) === $real_ext_path);
if (!$in_ext_path) {
return false;
}
// File to serve must be under a `ext_dir/static/` directory.
- $path_relative_to_ext = substr($path, strlen(EXTENSIONS_PATH) + 1);
+ $path_relative_to_ext = substr($path, strlen($real_ext_path) + 1);
$path_splitted = explode('/', $path_relative_to_ext);
if (count($path_splitted) < 3 || $path_splitted[1] !== 'static') {
return false;