aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2023-03-04 13:30:45 +0100
committerGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2023-03-04 13:30:45 +0100
commitb3239256dc6d188cda970adab516b3fcf1b86129 (patch)
treed8e65dd9784834ba2e82ce7ee94b4718f8af19ea
parent27b71ffa99f7dff013fb8d51d020ed628e0d2ce6 (diff)
parent0fe0ce894cbad09757d719dd4b400b9862c1a12a (diff)
Merge branch 'edge' into latest
-rw-r--r--.devcontainer/Dockerfile32
-rw-r--r--.devcontainer/devcontainer.json35
-rwxr-xr-x.devcontainer/postCreateCommand.sh17
-rw-r--r--.github/workflows/tests.yml10
-rw-r--r--.markdownlintignore1
-rw-r--r--.typos.toml4
-rw-r--r--CHANGELOG.md85
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--CREDITS.md5
-rw-r--r--Docker/Dockerfile2
-rw-r--r--Docker/Dockerfile-Alpine4
-rw-r--r--Docker/Dockerfile-Newest1
-rw-r--r--Docker/Dockerfile-Oldest4
-rw-r--r--Docker/Dockerfile-QEMU-ARM2
-rw-r--r--Docker/FreshRSS.Apache.conf2
-rw-r--r--Docker/README.md6
-rwxr-xr-xDocker/entrypoint.sh10
-rw-r--r--Makefile44
-rw-r--r--README.fr.md59
-rw-r--r--README.md32
-rw-r--r--[-rwxr-xr-x]app/Controllers/configureController.php35
-rw-r--r--[-rwxr-xr-x]app/Controllers/entryController.php0
-rw-r--r--[-rwxr-xr-x]app/Controllers/feedController.php18
-rw-r--r--app/Controllers/importExportController.php2
-rw-r--r--[-rwxr-xr-x]app/Controllers/indexController.php2
-rw-r--r--[-rwxr-xr-x]app/Controllers/javascriptController.php8
-rw-r--r--app/Controllers/statsController.php37
-rw-r--r--app/Controllers/subscriptionController.php18
-rw-r--r--app/Controllers/updateController.php12
-rw-r--r--app/Controllers/userController.php3
-rw-r--r--app/FreshRSS.php30
-rw-r--r--app/Models/BooleanSearch.php8
-rw-r--r--app/Models/Category.php15
-rw-r--r--app/Models/CategoryDAO.php15
-rw-r--r--app/Models/ConfigurationSetter.php7
-rw-r--r--app/Models/Context.php20
-rw-r--r--app/Models/Days.php8
-rw-r--r--app/Models/Entry.php174
-rw-r--r--app/Models/EntryDAO.php25
-rw-r--r--app/Models/EntryDAOSQLite.php4
-rw-r--r--app/Models/Feed.php142
-rw-r--r--app/Models/FeedDAO.php6
-rw-r--r--app/Models/Searchable.php4
-rw-r--r--app/Models/SystemConfiguration.php6
-rw-r--r--app/Models/Tag.php55
-rw-r--r--app/Models/TagDAO.php40
-rw-r--r--app/Models/Themes.php1
-rw-r--r--app/Models/UserConfiguration.php8
-rw-r--r--app/Models/UserQuery.php98
-rw-r--r--app/Models/View.php1
-rw-r--r--app/SQL/install.sql.mysql.php5
-rw-r--r--app/SQL/install.sql.pgsql.php4
-rw-r--r--app/SQL/install.sql.sqlite.php7
-rw-r--r--app/Services/ExportService.php3
-rw-r--r--app/Services/ImportService.php445
-rw-r--r--app/Utils/feverUtil.php38
-rw-r--r--app/Utils/passwordUtil.php19
-rw-r--r--app/i18n/cz/conf.php10
-rw-r--r--app/i18n/cz/gen.php3
-rw-r--r--app/i18n/cz/sub.php1
-rw-r--r--app/i18n/de/conf.php10
-rw-r--r--app/i18n/de/gen.php3
-rw-r--r--app/i18n/de/sub.php1
-rw-r--r--app/i18n/el/conf.php10
-rw-r--r--app/i18n/el/gen.php3
-rw-r--r--app/i18n/el/sub.php1
-rw-r--r--app/i18n/en-us/conf.php10
-rw-r--r--app/i18n/en-us/gen.php3
-rw-r--r--app/i18n/en-us/sub.php1
-rw-r--r--app/i18n/en/conf.php10
-rw-r--r--app/i18n/en/gen.php3
-rw-r--r--app/i18n/en/sub.php1
-rw-r--r--[-rwxr-xr-x]app/i18n/es/admin.php0
-rw-r--r--[-rwxr-xr-x]app/i18n/es/conf.php10
-rw-r--r--[-rwxr-xr-x]app/i18n/es/feedback.php0
-rw-r--r--[-rwxr-xr-x]app/i18n/es/gen.php3
-rw-r--r--[-rwxr-xr-x]app/i18n/es/index.php0
-rw-r--r--[-rwxr-xr-x]app/i18n/es/install.php0
-rw-r--r--[-rwxr-xr-x]app/i18n/es/sub.php1
-rw-r--r--app/i18n/fr/conf.php10
-rw-r--r--app/i18n/fr/gen.php3
-rw-r--r--app/i18n/fr/sub.php1
-rw-r--r--app/i18n/he/conf.php10
-rw-r--r--app/i18n/he/gen.php3
-rw-r--r--app/i18n/he/sub.php1
-rw-r--r--app/i18n/id/conf.php10
-rw-r--r--app/i18n/id/gen.php3
-rw-r--r--app/i18n/id/sub.php1
-rw-r--r--app/i18n/it/conf.php10
-rw-r--r--app/i18n/it/gen.php3
-rw-r--r--app/i18n/it/sub.php1
-rw-r--r--app/i18n/ja/conf.php10
-rw-r--r--app/i18n/ja/gen.php3
-rw-r--r--app/i18n/ja/sub.php1
-rw-r--r--app/i18n/ko/conf.php10
-rw-r--r--app/i18n/ko/gen.php3
-rw-r--r--app/i18n/ko/sub.php1
-rw-r--r--app/i18n/nl/conf.php10
-rw-r--r--app/i18n/nl/gen.php3
-rw-r--r--app/i18n/nl/sub.php1
-rw-r--r--app/i18n/oc/conf.php10
-rw-r--r--app/i18n/oc/gen.php3
-rw-r--r--app/i18n/oc/sub.php1
-rw-r--r--app/i18n/pl/conf.php10
-rw-r--r--app/i18n/pl/gen.php3
-rw-r--r--app/i18n/pl/sub.php1
-rw-r--r--app/i18n/pt-br/conf.php10
-rw-r--r--app/i18n/pt-br/gen.php3
-rw-r--r--app/i18n/pt-br/sub.php1
-rw-r--r--app/i18n/ru/conf.php10
-rw-r--r--app/i18n/ru/gen.php3
-rw-r--r--app/i18n/ru/sub.php1
-rw-r--r--app/i18n/sk/conf.php10
-rw-r--r--app/i18n/sk/gen.php3
-rw-r--r--app/i18n/sk/sub.php1
-rw-r--r--app/i18n/tr/conf.php10
-rw-r--r--app/i18n/tr/gen.php3
-rw-r--r--app/i18n/tr/sub.php1
-rw-r--r--app/i18n/zh-cn/admin.php76
-rw-r--r--app/i18n/zh-cn/conf.php100
-rw-r--r--app/i18n/zh-cn/feedback.php32
-rw-r--r--app/i18n/zh-cn/gen.php21
-rw-r--r--app/i18n/zh-cn/index.php6
-rw-r--r--app/i18n/zh-cn/install.php42
-rw-r--r--app/i18n/zh-cn/sub.php25
-rw-r--r--app/i18n/zh-cn/user.php18
-rw-r--r--app/i18n/zh-tw/conf.php10
-rw-r--r--app/i18n/zh-tw/gen.php3
-rw-r--r--app/i18n/zh-tw/sub.php1
-rw-r--r--app/install.php12
-rw-r--r--app/layout/aside_configure.phtml138
-rw-r--r--app/layout/aside_feed.phtml2
-rw-r--r--app/layout/aside_subscription.phtml57
-rw-r--r--app/layout/header.phtml88
-rw-r--r--app/layout/layout.phtml2
-rw-r--r--app/layout/nav_menu.phtml67
-rw-r--r--app/layout/simple.phtml2
-rw-r--r--app/shares.php15
-rw-r--r--app/views/auth/register.phtml12
-rw-r--r--app/views/configure/display.phtml47
-rw-r--r--app/views/configure/integration.phtml4
-rw-r--r--app/views/configure/system.phtml4
-rw-r--r--app/views/feed/add.phtml2
-rw-r--r--app/views/helpers/category/update.phtml2
-rw-r--r--app/views/helpers/configure/query.phtml4
-rw-r--r--app/views/helpers/export/opml.phtml66
-rw-r--r--app/views/helpers/feed/update.phtml7
-rw-r--r--app/views/helpers/index/normal/entry_header.phtml5
-rwxr-xr-xapp/views/helpers/stream-footer.phtml11
-rw-r--r--app/views/index/logs.phtml10
-rw-r--r--app/views/index/normal.phtml2
-rw-r--r--app/views/index/reader.phtml6
-rwxr-xr-xapp/views/index/rss.phtml22
-rw-r--r--app/views/subscription/add.phtml11
-rw-r--r--app/views/user/manage.phtml16
-rw-r--r--cli/README.md4
-rw-r--r--cli/_cli.php2
-rwxr-xr-xcli/access-permissions.sh19
-rw-r--r--cli/i18n/I18nFile.php2
-rwxr-xr-xcli/sensitive-log.sh9
-rw-r--r--composer.json4
-rw-r--r--composer.lock417
-rw-r--r--config-user.default.php3
-rw-r--r--config.default.php2
-rw-r--r--constants.php4
-rw-r--r--docs/en/admins/02_Prerequisites.md2
-rw-r--r--docs/en/admins/05_Configuring_email_validation.md2
-rw-r--r--docs/en/admins/06_LinuxInstall.md9
-rw-r--r--docs/en/admins/07_LinuxUpdate.md12
-rw-r--r--docs/en/admins/08_FeedUpdates.md2
-rw-r--r--docs/en/admins/10_ServerConfig.md6
-rw-r--r--docs/en/developers/02_Github.md2
-rw-r--r--docs/en/developers/03_Backend/05_Extensions.md190
-rw-r--r--docs/en/developers/Minz/index.md198
-rw-r--r--docs/en/developers/OPML.md6
-rw-r--r--docs/en/internationalization.md2
-rw-r--r--docs/en/users/02_First_steps.md6
-rw-r--r--docs/en/users/03_Main_view.md261
-rw-r--r--docs/en/users/04_Subscriptions.md4
-rw-r--r--docs/en/users/05_Configuration.md27
-rw-r--r--docs/en/users/09_refreshing_feeds.md134
-rw-r--r--docs/en/users/10_filter.md152
-rw-r--r--docs/fr/developers/01_First_steps.md2
-rw-r--r--docs/fr/developers/03_Backend/02_Minz.md29
-rw-r--r--docs/fr/developers/03_Backend/05_Extensions.md324
-rw-r--r--docs/fr/developers/Minz/index.md249
-rw-r--r--docs/fr/developers/Minz/migration.md3
-rw-r--r--docs/fr/internationalization.md79
-rw-r--r--docs/fr/users/01_Installation.md2
-rw-r--r--docs/fr/users/03_Main_view.md2
-rw-r--r--docs/fr/users/05_Configuration.md27
-rw-r--r--lib/.gitignore8
-rw-r--r--lib/Minz/Configuration.php6
-rw-r--r--lib/Minz/Migrator.php2
-rw-r--r--lib/Minz/ModelPdo.php2
-rw-r--r--lib/Minz/Translate.php6
-rw-r--r--lib/Minz/View.php60
-rw-r--r--lib/SimplePie/SimplePie.php10
-rw-r--r--lib/SimplePie/SimplePie/Enclosure.php2
-rw-r--r--lib/SimplePie/SimplePie/Item.php25
-rw-r--r--lib/SimplePie/SimplePie/Locator.php8
-rw-r--r--[-rwxr-xr-x]lib/SimplePie/SimplePie/Registry.php0
-rw-r--r--lib/composer.json3
-rw-r--r--lib/http-conditional.php2
-rw-r--r--lib/lib_install.php10
-rw-r--r--lib/lib_opml.php353
-rw-r--r--lib/lib_rss.php234
-rw-r--r--lib/marienfressinaud/lib_opml/.gitattributes8
-rw-r--r--lib/marienfressinaud/lib_opml/.gitignore2
-rw-r--r--lib/marienfressinaud/lib_opml/CHANGELOG.md63
-rw-r--r--lib/marienfressinaud/lib_opml/LICENSE21
-rw-r--r--lib/marienfressinaud/lib_opml/README.md338
-rw-r--r--lib/marienfressinaud/lib_opml/composer.json35
-rw-r--r--lib/marienfressinaud/lib_opml/src/LibOpml/Exception.php15
-rw-r--r--lib/marienfressinaud/lib_opml/src/LibOpml/LibOpml.php770
-rw-r--r--p/.htaccess2
-rw-r--r--p/api/fever.php201
-rw-r--r--p/api/greader.php1865
-rw-r--r--p/api/pshb.php17
-rw-r--r--p/ext.php17
-rw-r--r--p/f.php2
-rw-r--r--[-rwxr-xr-x]p/i/index.php4
-rw-r--r--p/index.html2
-rw-r--r--p/scripts/extra.js26
-rw-r--r--p/scripts/feed.js11
-rw-r--r--p/scripts/main.js40
-rw-r--r--p/themes/.htaccess7
-rw-r--r--p/themes/Alternative-Dark/adark.css121
-rw-r--r--p/themes/Alternative-Dark/adark.rtl.css121
-rw-r--r--p/themes/Ansum/_components.scss56
-rw-r--r--p/themes/Ansum/_forms.scss7
-rw-r--r--p/themes/Ansum/_layout.scss45
-rw-r--r--p/themes/Ansum/_list-view.scss10
-rw-r--r--p/themes/Ansum/_mobile.scss40
-rw-r--r--p/themes/Ansum/_sidebar.scss66
-rw-r--r--p/themes/Ansum/_tables.scss3
-rw-r--r--p/themes/Ansum/ansum.css173
-rw-r--r--p/themes/Ansum/ansum.rtl.css173
-rw-r--r--p/themes/Ansum/icons/up.svg60
-rw-r--r--p/themes/BlueLagoon/BlueLagoon.css91
-rw-r--r--p/themes/BlueLagoon/BlueLagoon.rtl.css91
-rw-r--r--p/themes/BlueLagoon/icons/bookmark.svg3
-rw-r--r--p/themes/BlueLagoon/metadata.json3
-rw-r--r--p/themes/Dark-pink/pinkdark.css8
-rw-r--r--p/themes/Dark-pink/pinkdark.rtl.css8
-rw-r--r--p/themes/Dark-pink/thumbs/original.pngbin204893 -> 188590 bytes
-rw-r--r--p/themes/Dark/dark.css118
-rw-r--r--p/themes/Dark/dark.rtl.css118
-rw-r--r--p/themes/Flat/flat.css106
-rw-r--r--p/themes/Flat/flat.rtl.css106
-rw-r--r--p/themes/Flat/metadata.json3
-rw-r--r--p/themes/Mapco/_components.scss43
-rw-r--r--p/themes/Mapco/_fonts.scss2
-rw-r--r--p/themes/Mapco/_forms.scss7
-rw-r--r--p/themes/Mapco/_layout.scss50
-rw-r--r--p/themes/Mapco/_list-view.scss10
-rw-r--r--p/themes/Mapco/_mobile.scss35
-rw-r--r--p/themes/Mapco/_sidebar.scss66
-rw-r--r--p/themes/Mapco/_tables.scss3
-rw-r--r--p/themes/Mapco/icons/up.svg4
-rw-r--r--p/themes/Mapco/mapco.css170
-rw-r--r--p/themes/Mapco/mapco.rtl.css170
-rw-r--r--p/themes/Nord/nord.css330
-rw-r--r--p/themes/Nord/nord.rtl.css330
-rw-r--r--p/themes/Origine-compact/origine-compact.css80
-rw-r--r--p/themes/Origine-compact/origine-compact.rtl.css80
-rw-r--r--p/themes/Origine/origine.css292
-rw-r--r--p/themes/Origine/origine.rtl.css292
-rw-r--r--p/themes/Pafat/icons/all.svg12
-rw-r--r--p/themes/Pafat/icons/bookmark.svg5
-rw-r--r--p/themes/Pafat/pafat.css577
-rw-r--r--p/themes/Pafat/pafat.rtl.css577
-rw-r--r--p/themes/Screwdriver/icons/bookmark.svg5
-rw-r--r--p/themes/Screwdriver/metadata.json3
-rw-r--r--p/themes/Screwdriver/screwdriver.css97
-rw-r--r--p/themes/Screwdriver/screwdriver.rtl.css97
-rw-r--r--p/themes/Swage/icons/bookmark.svg6
-rw-r--r--p/themes/Swage/swage.css112
-rw-r--r--p/themes/Swage/swage.rtl.css112
-rw-r--r--p/themes/Swage/swage.scss148
-rw-r--r--p/themes/base-theme/base.css49
-rw-r--r--p/themes/base-theme/base.rtl.css49
-rw-r--r--p/themes/base-theme/frss.css221
-rw-r--r--p/themes/base-theme/frss.rtl.css221
-rw-r--r--p/themes/icons/bookmark.svg5
-rw-r--r--[-rwxr-xr-x]p/themes/icons/look.svg0
-rw-r--r--package-lock.json1748
-rw-r--r--package.json17
-rw-r--r--phpcs.xml1
-rw-r--r--phpstan.neon22
-rw-r--r--tests/app/Models/CategoryTest.php50
-rw-r--r--tests/app/Models/SearchTest.php13
-rw-r--r--tests/app/Models/UserQueryTest.php20
293 files changed, 9426 insertions, 7504 deletions
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 000000000..a2c1a2a2b
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,32 @@
+FROM alpine:3.17
+
+ENV TZ UTC
+SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
+
+RUN apk add --no-cache \
+ tzdata \
+ apache2 php-apache2 \
+ php php-curl php-gmp php-intl php-mbstring php-xml php-zip \
+ php-ctype php-dom php-fileinfo php-iconv php-json php-opcache php-openssl php-phar php-session php-simplexml php-xmlreader php-xmlwriter php-xml php-tokenizer php-zlib \
+ php-pdo_sqlite php-pdo_mysql php-pdo_pgsql \
+ bash composer curl docker-cli-buildx git gpg make nodejs npm shellcheck shfmt sudo
+
+RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
+ /etc/apache2/conf.d/status.conf /etc/apache2/conf.d/userdir.conf && \
+ sed -r -i "/^\s*LoadModule .*mod_(alias|autoindex|negotiation|status).so$/s/^/#/" \
+ /etc/apache2/httpd.conf && \
+ sed -r -i "/^\s*#\s*LoadModule .*mod_(deflate|expires|headers|mime|remoteip|setenvif).so$/s/^\s*#//" \
+ /etc/apache2/httpd.conf && \
+ sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" \
+ /etc/apache2/httpd.conf
+
+RUN adduser --ingroup www-data --disabled-password developer && \
+ echo "developer ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/developer
+
+ENV COPY_LOG_TO_SYSLOG On
+ENV COPY_SYSLOG_TO_STDERR On
+ENV CRON_MIN ''
+ENV FRESHRSS_ENV 'development'
+ENV LISTEN '0.0.0.0:8080'
+
+EXPOSE 8080
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..a30fdf5c7
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,35 @@
+// For format details, see https://aka.ms/devcontainer.json
+{
+ "name": "FreshRSS-dev-Alpine",
+ "build": {
+ "dockerfile": "Dockerfile"
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "bmewburn.vscode-intelephense-client",
+ "DavidAnson.vscode-markdownlint",
+ "dbaeumer.vscode-eslint",
+ "eamodio.gitlens",
+ "EditorConfig.EditorConfig",
+ "foxundermoon.shell-format",
+ "mrmlnc.vscode-apache",
+ "ms-azuretools.vscode-docker",
+ "redhat.vscode-yaml",
+ "timonwong.shellcheck",
+ "ValeryanM.vscode-phpsab"
+ ]
+ }
+ },
+ "forwardPorts": [
+ 8080
+ ],
+ "portsAttributes": {
+ "8080": {
+ "label": "FreshRSS Apache",
+ "onAutoForward": "notify"
+ }
+ },
+ "remoteUser": "developer",
+ "postCreateCommand": "sudo .devcontainer/postCreateCommand.sh"
+}
diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh
new file mode 100755
index 000000000..f5398efb9
--- /dev/null
+++ b/.devcontainer/postCreateCommand.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+ln -s "$(pwd)" /var/www/FreshRSS
+
+cp ./Docker/*.Apache.conf /etc/apache2/conf.d/
+
+cat <<EOT >./constants.local.php
+<?php
+define('DATA_PATH', '/home/developer/freshrss-data');
+EOT
+
+./Docker/entrypoint.sh
+
+chown -R developer:www-data /home/developer/freshrss-data
+chmod -R g+w /home/developer/freshrss-data
+
+httpd
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5751f685b..6e1e6b0a6 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -55,7 +55,7 @@ jobs:
uses: actions/setup-node@v3
with:
# https://nodejs.org/en/about/releases/
- node-version: '16'
+ node-version: '18'
cache: 'npm'
- run: npm ci
@@ -79,14 +79,14 @@ jobs:
uses: actions/cache@v3
with:
path: bin
- key: ${{ runner.os }}-bin-shfmt@v3.5.1-hadolint@v2.10.0-typos@v1.10.1
+ key: ${{ runner.os }}-bin-shfmt@v3.6.0-hadolint@v2.12.0-typos@v1.13.6
- name: Add ./bin/ to $PATH
run: mkdir -p bin/ && echo "${PWD}/bin" >> $GITHUB_PATH
- name: Install shfmt
if: steps.shell-cache.outputs.cache-hit != 'true'
- run: GOBIN=${PWD}/bin/ go install mvdan.cc/sh/v3/cmd/shfmt@v3.5.1
+ run: GOBIN=${PWD}/bin/ go install mvdan.cc/sh/v3/cmd/shfmt@v3.6.0
- name: Check shell script syntax
# shellcheck is pre-installed https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2204-Readme.md
@@ -94,7 +94,7 @@ jobs:
- name: Install hadolint
if: steps.shell-cache.outputs.cache-hit != 'true'
- run: curl -sL -o ./bin/hadolint "https://github.com/hadolint/hadolint/releases/download/v2.10.0/hadolint-$(uname -s)-$(uname -m)" && chmod 700 ./bin/hadolint
+ run: curl -sL -o ./bin/hadolint "https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-$(uname -s)-$(uname -m)" && chmod 700 ./bin/hadolint
- name: Check Dockerfile syntax
run: find . -name 'Dockerfile*' -print0 | xargs -0 -n1 ./bin/hadolint --failure-threshold warning
@@ -103,7 +103,7 @@ jobs:
if: steps.shell-cache.outputs.cache-hit != 'true'
run: |
cd bin ;
- wget -q 'https://github.com/crate-ci/typos/releases/download/v1.10.1/typos-v1.10.1-x86_64-unknown-linux-musl.tar.gz' &&
+ wget -q 'https://github.com/crate-ci/typos/releases/download/v1.13.6/typos-v1.13.6-x86_64-unknown-linux-musl.tar.gz' &&
tar -xvf *.tar.gz './typos' &&
chmod +x typos &&
rm *.tar.gz ;
diff --git a/.markdownlintignore b/.markdownlintignore
index 6e1cfb9c4..fa771b056 100644
--- a/.markdownlintignore
+++ b/.markdownlintignore
@@ -1,4 +1,5 @@
.git/
+lib/marienfressinaud/
lib/phpgt/
lib/phpmailer/
node_modules/
diff --git a/.typos.toml b/.typos.toml
index f4b7d5f5a..2170f5e85 100644
--- a/.typos.toml
+++ b/.typos.toml
@@ -3,7 +3,7 @@ ot = "ot"
Ths2 = "Ths2"
[default.extend-words]
-ba = "ba"
+referer = "referer"
[files]
extend-exclude = [
@@ -33,8 +33,10 @@ extend-exclude = [
"app/i18n/zh-cn/",
"bin/",
"CHANGELOG-old.md",
+ "composer.lock",
"data/",
"docs/fr/",
+ "lib/marienfressinaud/",
"lib/phpgt/",
"lib/phpmailer/",
"lib/SimplePie/",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b14a835e..5916d1299 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,92 @@
# FreshRSS changelog
+## 2023-03-04 FreshRSS 1.21.0
+
+* Features
+ * New *XML+XPath* mode for fetching XML documents when there is no RSS/ATOM feed [#5076](https://github.com/FreshRSS/FreshRSS/pull/5076)
+ * Better support of feed enclosures (image / audio / video attachments) [#4944](https://github.com/FreshRSS/FreshRSS/pull/4944)
+ * User-defined time-zone [#4906](https://github.com/FreshRSS/FreshRSS/pull/4906)
+ * Improve HTML+XPath mode by allowing HTML content [#4878](https://github.com/FreshRSS/FreshRSS/pull/4878)
+ * Search only on full tag names and not on parts of tag names [#4882](https://github.com/FreshRSS/FreshRSS/pull/4882)
+ * Allows searching for parentheses with `\(` or `\)` [#4989](https://github.com/FreshRSS/FreshRSS/pull/4989)
+ * Firefox-compatible sharing service for `mailto:` links for webmail services [#4680](https://github.com/FreshRSS/FreshRSS/pull/4680)
+ * Add sharing to [archive.org](https://archive.org/) [#5096](https://github.com/FreshRSS/FreshRSS/pull/5096)
+ * Increase max HTTP timeout to 15 minutes [#5074](https://github.com/FreshRSS/FreshRSS/pull/5074)
+* Compatibility
+ * Require PHP 7.2+ (drop support for PHP 7.0 and 7.1) [#4848](https://github.com/FreshRSS/FreshRSS/pull/4848)
+ * Workaround disabled `openlog()` or `syslog()` [#5054](https://github.com/FreshRSS/FreshRSS/pull/5054)
+* Deployment
+ * Docker default image (Debian 11 Bullseye) updated to PHP 7.4.33
+ * Docker: alternative image updated to Alpine 3.17 with PHP 8.1.16 and Apache 2.4.55 [#4886](https://github.com/FreshRSS/FreshRSS/pull/4886)
+ * More uniform time-zone behaviour [#4903](https://github.com/FreshRSS/FreshRSS/pull/4903), [#4905](https://github.com/FreshRSS/FreshRSS/pull/4905)
+ * New CLI script `cli/sensitive-log.sh` to help e.g. Apache clear logs for sensitive information such as credentials [#5001](https://github.com/FreshRSS/FreshRSS/pull/5001)
+ * New CLI script `cli/access-permissions.sh` to help apply file permissions correctly [#5062](https://github.com/FreshRSS/FreshRSS/pull/5062)
+ * Improve file permissions on `./extensions/` [#4956](https://github.com/FreshRSS/FreshRSS/pull/4956)
+ * Update Apache mime type `font/woff` [#4894](https://github.com/FreshRSS/FreshRSS/pull/4894)
+ * Re-added a git `latest` branch (instead of a tag) to track the latest FreshRSS stable releases [#5148](https://github.com/FreshRSS/FreshRSS/pull/5148)
+* Bug fixing
+ * Fix allow disabling curl proxy for specific feed, when proxy is defined globally [#5082](https://github.com/FreshRSS/FreshRSS/pull/5082)
+ * NFS-friendly `is_writable()` checks [#4780](https://github.com/FreshRSS/FreshRSS/pull/4780)
+ * Fix error handling when updating feed URL [#5039](https://github.com/FreshRSS/FreshRSS/pull/5039)
+ * Fix feed favicon after editing feed URL [#4975](https://github.com/FreshRSS/FreshRSS/pull/4975)
+ * Fix allow <kbd>Ctrl</kbd>+<kbd>Click</kbd> to open *Manage feeds* in new tab [#4980](https://github.com/FreshRSS/FreshRSS/pull/4980)
+ * Fix empty window opened when pressing space after page load [#5146](https://github.com/FreshRSS/FreshRSS/pull/5146)
+ * Fix keep current view when searching [#4981](https://github.com/FreshRSS/FreshRSS/pull/4981)
+ * Fix mobile view: scroll main area again after closing slider [#5092](https://github.com/FreshRSS/FreshRSS/pull/5092)
+ * Fix change confirmation when leaving sharing service config [#5098](https://github.com/FreshRSS/FreshRSS/pull/5098)
+ * Fix sharing to Lemmy [#5020](https://github.com/FreshRSS/FreshRSS/pull/5020)
+* Security
+ * API avoid logging passwords [CVE-2023-22481](https://github.com/FreshRSS/FreshRSS/security/advisories/GHSA-8vvv-jxg6-8578)
+ * Remove execution rights on some files not needing it [#5065](https://github.com/FreshRSS/FreshRSS/pull/5065)
+ * More robust application of file access permissions [#5062](https://github.com/FreshRSS/FreshRSS/pull/5062)
+* UI
+ * Improve search box [#4994](https://github.com/FreshRSS/FreshRSS/pull/4994)
+ * Improve navigation menu structure [#4937](https://github.com/FreshRSS/FreshRSS/pull/4937)
+ * More consistent sorting of feeds alphabetically [#4841](https://github.com/FreshRSS/FreshRSS/pull/4841)
+ * Improve reader view on mobile screen [#4868](https://github.com/FreshRSS/FreshRSS/pull/4868)
+ * Various UI and style improvements [#4681](https://github.com/FreshRSS/FreshRSS/pull/4681), [#4794](https://github.com/FreshRSS/FreshRSS/pull/4794)
+ [#4800](https://github.com/FreshRSS/FreshRSS/pull/4800), [#4850](https://github.com/FreshRSS/FreshRSS/pull/4850), [#4865](https://github.com/FreshRSS/FreshRSS/pull/4865),
+ [#4872](https://github.com/FreshRSS/FreshRSS/pull/4872), [#4874](https://github.com/FreshRSS/FreshRSS/pull/4874), [#4889](https://github.com/FreshRSS/FreshRSS/pull/4889),
+ [#4890](https://github.com/FreshRSS/FreshRSS/pull/4890), [#4891](https://github.com/FreshRSS/FreshRSS/pull/4891), [#4897](https://github.com/FreshRSS/FreshRSS/pull/4897),
+ [#4899](https://github.com/FreshRSS/FreshRSS/pull/4899), [#4910](https://github.com/FreshRSS/FreshRSS/pull/4910), [#4923](https://github.com/FreshRSS/FreshRSS/pull/4923),
+ [#4927](https://github.com/FreshRSS/FreshRSS/pull/4927), [#4960](https://github.com/FreshRSS/FreshRSS/pull/4960), [#4985](https://github.com/FreshRSS/FreshRSS/pull/4985),
+ [#4998](https://github.com/FreshRSS/FreshRSS/pull/4998), [#5034](https://github.com/FreshRSS/FreshRSS/pull/5034), [#5040](https://github.com/FreshRSS/FreshRSS/pull/5040),
+ [#5055](https://github.com/FreshRSS/FreshRSS/pull/5055), [#5058](https://github.com/FreshRSS/FreshRSS/pull/5058), [#5097](https://github.com/FreshRSS/FreshRSS/pull/5097),
+ [#5100](https://github.com/FreshRSS/FreshRSS/pull/5100)
+* Themes
+ * Dark mode for *Origine* and *Origine compact* themes [#4843](https://github.com/FreshRSS/FreshRSS/pull/4843)
+ * Improve *Ansum* and *Mapco* [#4938](https://github.com/FreshRSS/FreshRSS/pull/4938), [#4959](https://github.com/FreshRSS/FreshRSS/pull/4959), [#4967](https://github.com/FreshRSS/FreshRSS/pull/4967),
+ [#4983](https://github.com/FreshRSS/FreshRSS/pull/4983), [#4995](https://github.com/FreshRSS/FreshRSS/pull/4995)
+ * Improve *Dark pink* [#4881](https://github.com/FreshRSS/FreshRSS/pull/4881)
+ * Improve *Nord theme* [#4892](https://github.com/FreshRSS/FreshRSS/pull/4892), [#4979](https://github.com/FreshRSS/FreshRSS/pull/4979)
+ * Improve *Origine* [#4893](https://github.com/FreshRSS/FreshRSS/pull/4893)
+ * Improve *Origine compact* [#4873](https://github.com/FreshRSS/FreshRSS/pull/4873)
+ * Improve *Pafat* [#4909](https://github.com/FreshRSS/FreshRSS/pull/4909)
+ * Improve *Swage* [#4875](https://github.com/FreshRSS/FreshRSS/pull/4875), [#4922](https://github.com/FreshRSS/FreshRSS/pull/4922), [#4936](https://github.com/FreshRSS/FreshRSS/pull/4936),
+ [#5029](https://github.com/FreshRSS/FreshRSS/pull/5029)
+ * Mark some themes as tentatively deprecated: *BlueLagoon*, *Flat*, *Screwdriver* [#4807](https://github.com/FreshRSS/FreshRSS/pull/4807)
+* i18n
+ * Improve Chinese [#4853](https://github.com/FreshRSS/FreshRSS/pull/4853), [#4856](https://github.com/FreshRSS/FreshRSS/pull/4856)
+* SimplePie
+ * No URL Decode for enclosure links [#768](https://github.com/simplepie/simplepie/pull/768)
+ * Fix case of multiple RSS2.0 enclosures [#769](https://github.com/simplepie/simplepie/pull/769)
+ * Sanitize thumbnail URL [#770](https://github.com/simplepie/simplepie/pull/770)
+ * Use single constant for default HTTP Accept header [#784](https://github.com/simplepie/simplepie/pull/784)
+* Misc.
+ * Increase max feed URL length and drop unicity in database [#5038](https://github.com/FreshRSS/FreshRSS/pull/5038)
+ * New support of [Development Containers](https://containers.dev) / [GitHub Codespaces](https://github.com/features/codespaces) to ease development [#4859](https://github.com/FreshRSS/FreshRSS/pull/4859)
+ * Update library `lib_opml` [#4403](https://github.com/FreshRSS/FreshRSS/pull/4403)
+ * Code improvements [#4232](https://github.com/FreshRSS/FreshRSS/pull/4232), [#4651](https://github.com/FreshRSS/FreshRSS/pull/4651),
+ [#5024](https://github.com/FreshRSS/FreshRSS/pull/5024), [#5025](https://github.com/FreshRSS/FreshRSS/pull/5025), [#5028](https://github.com/FreshRSS/FreshRSS/pull/5028),
+ [#5032](https://github.com/FreshRSS/FreshRSS/pull/5032), [#5158](https://github.com/FreshRSS/FreshRSS/pull/5158), [#5045](https://github.com/FreshRSS/FreshRSS/pull/5045),
+ [#5049](https://github.com/FreshRSS/FreshRSS/pull/5049), [#5063](https://github.com/FreshRSS/FreshRSS/pull/5063), [#5084](https://github.com/FreshRSS/FreshRSS/pull/5084)
+ * Update dev dependencies [#4993](https://github.com/FreshRSS/FreshRSS/pull/4993), [#5006](https://github.com/FreshRSS/FreshRSS/pull/5006), [#5109](https://github.com/FreshRSS/FreshRSS/pull/5109)
+
+
## 2022-12-08 FreshRSS 1.20.2
* Security fixes
- * Fix security vulnerability in `ext.php` [#4928](https://github.com/FreshRSS/FreshRSS/pull/4928)
+ * [CVE-2022-23497](https://github.com/FreshRSS/FreshRSS/security/advisories/GHSA-hvrj-5fwj-p7v6) Fix security vulnerability in `ext.php` [#4928](https://github.com/FreshRSS/FreshRSS/pull/4928)
* Apache `TraceEnable Off` [#4863](https://github.com/FreshRSS/FreshRSS/pull/4863)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 32da43cc8..76db326a6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -33,7 +33,7 @@ Did you want to fix a bug? To keep a great coordination between collaborators, y
3. [Create a new branch](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/). The name of the branch must be explicit and being prefixed by the related ticket id. For instance, `783-contributing-file` to fix [ticket #783](https://github.com/FreshRSS/FreshRSS/issues/783).
4. Make your changes to your fork and [send a pull request](https://help.github.com/articles/using-pull-requests/) on the **edge branch**. Don’t forget to add your name to `CREDITS.md` if you’re contributing to FreshRSS for the very first time.
-If you have to write code, please follow [our coding style recommendations](https://freshrss.github.io/FreshRSS/en/developers/01_First_steps.html).
+If you have to write code, please follow [our coding style recommendations](https://freshrss.github.io/FreshRSS/en/developers/02_First_steps.html).
**Tip:** if you are searching for bugs easy to fix, have a look at the « [Good first issue](https://github.com/FreshRSS/FreshRSS/issues?q=label%3A%22good+first+issue+%3Ababy%3A%22) » and/or « [Help wanted](https://github.com/FreshRSS/FreshRSS/issues?q=label%3A%22help+wanted+%3Aoctocat%3A%22) » ticket labels.
diff --git a/CREDITS.md b/CREDITS.md
index d5af0ba54..c60b9531b 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -26,6 +26,7 @@ People are sorted by name so please keep this order.
* [ArthurHoaro](https://github.com/ArthurHoaro): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ArthurHoaro)
* [Artur Weigandt](https://github.com/Art4): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Art4), [Web](https://ruhr.social/@Art4)
* [ASMfreaK](https://github.com/ASMfreaK): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ASMfreaK)
+* [Axel Leroy](https://github.com/axeleroy): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:axeleroy), [Web](https://axel.leroy.sh/)
* [azlux](https://github.com/azlux): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:azlux), [Web](https://azlux.fr/)
* [Bartosz Taudul](https://github.com/wolfpld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:wolfpld), [Web](https://wolf.nereid.pl/)
* [Benjamin Bouvier](https://github.com/bnjbvr): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:bnjbvr), [Web](https://benj.me/)
@@ -74,6 +75,7 @@ People are sorted by name so please keep this order.
* [happymacarts](https://github.com/happymacarts): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:happymacarts)
* [Harshad Hirapara](https://github.com/harshad389): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:harshad389)
* [hesch](https://github.com/hesch): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hesch)
+* [Hippolyte Thomas](https://github.com/hippothomas): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hippothomas), [Web](https://hippolyte-thomas.fr/)
* [hoilc](https://github.com/hoilc): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hoilc)
* [ibiruai](https://github.com/ibiruai): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ibiruai)
* [id-konstantin-stepanov](https://github.com/id-konstantin-stepanov): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:id-konstantin-stepanov)
@@ -121,6 +123,7 @@ People are sorted by name so please keep this order.
* [Miika Launiainen](https://gitlab.com/miicat): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:miicat), [Web](https://miicat.eu/)
* [Mike Vanbuskirk](https://github.com/codevbus): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:codevbus) [Web](http://mikevanbuskirk.io/)
* [miles](https://github.com/miles170): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:miles170)
+* [mincerafter42](https://github.com/mincerafter42): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mincerafter42), [Web](https://mincerafter42.github.io)
* [MSZ](https://github.com/mszkb): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mszkb)
* [Myuki](https://github.com/Myuki): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Myuki)
* [Nainor](https://github.com/Nainor): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Nainor)
@@ -170,6 +173,7 @@ People are sorted by name so please keep this order.
* [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:romibi)
* [Rosemary Le Faive](https://github.com/rosiel): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rosiel)
* [ryoku-cha](https://github.com/ryoku-cha): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ryoku-cha)
+* [Sadetdin EYILI](https://github.com/sad270): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:sad270)
* [Sandro Jäckel](https://github.com/SuperSandro2000): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:SuperSandro2000), [Web](https://supersandro.de/)
* [Sebastian K](https://github.com/skrollme): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:skrollme)
* [shn7798](https://github.com/shn7798): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:shn7798), [Web](http://www.code2talk.com/)
@@ -202,3 +206,4 @@ People are sorted by name so please keep this order.
* [xnaas](https://github.com/xnaas): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:xnaas), [Web](https://xnaas.info/)
* [Yamakuni](https://github.com/Yamakuni): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Yamakuni), [Web](https://ofanch.me/)
* [yzqzss|一座桥在水上](https://github.com/yzqzss): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:yzqzss), [Web](https://blog.othing.xyz/)
+* [Zhiyuan Zheng](https://github.com/zhzy0077): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:zhzy0077)
diff --git a/Docker/Dockerfile b/Docker/Dockerfile
index b462263fb..08e6dce43 100644
--- a/Docker/Dockerfile
+++ b/Docker/Dockerfile
@@ -2,8 +2,8 @@ FROM debian:11-slim
ENV TZ UTC
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
-RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install --no-install-recommends -y \
ca-certificates cron \
diff --git a/Docker/Dockerfile-Alpine b/Docker/Dockerfile-Alpine
index 1e5a95125..65c2de6be 100644
--- a/Docker/Dockerfile-Alpine
+++ b/Docker/Dockerfile-Alpine
@@ -1,8 +1,10 @@
-FROM alpine:3.16
+FROM alpine:3.17
ENV TZ UTC
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
+
RUN apk add --no-cache \
+ tzdata \
apache2 php-apache2 \
php php-curl php-gmp php-intl php-mbstring php-xml php-zip \
php-ctype php-dom php-fileinfo php-iconv php-json php-opcache php-openssl php-phar php-session php-simplexml php-xmlreader php-xmlwriter php-xml php-tokenizer php-zlib \
diff --git a/Docker/Dockerfile-Newest b/Docker/Dockerfile-Newest
index d5942c77e..37783494b 100644
--- a/Docker/Dockerfile-Newest
+++ b/Docker/Dockerfile-Newest
@@ -4,6 +4,7 @@ ENV TZ UTC
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories && \
apk add --no-cache \
+ tzdata \
apache2 php82-apache2 \
php82 php82-curl php82-gmp php82-intl php82-mbstring php82-xml php82-zip \
php82-ctype php82-dom php82-fileinfo php82-iconv php82-json php82-opcache php82-openssl php82-phar php82-session php82-simplexml php82-xmlreader php82-xmlwriter php82-xml php82-tokenizer php82-zlib \
diff --git a/Docker/Dockerfile-Oldest b/Docker/Dockerfile-Oldest
index d0b27cc72..ede1ba643 100644
--- a/Docker/Dockerfile-Oldest
+++ b/Docker/Dockerfile-Oldest
@@ -1,8 +1,10 @@
-FROM alpine:3.5
+FROM alpine:3.8
ENV TZ UTC
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
+
RUN apk add --no-cache \
+ tzdata \
apache2 php7-apache2 \
php7 php7-curl php7-gmp php7-intl php7-mbstring php7-xml php7-zip \
php7-ctype php7-dom php7-iconv php7-json php7-opcache php7-openssl php7-phar php7-session php7-xmlreader php7-xml php7-zlib \
diff --git a/Docker/Dockerfile-QEMU-ARM b/Docker/Dockerfile-QEMU-ARM
index 042597dec..892cbc955 100644
--- a/Docker/Dockerfile-QEMU-ARM
+++ b/Docker/Dockerfile-QEMU-ARM
@@ -8,8 +8,8 @@ COPY ./Docker/qemu-arm-* /usr/bin/
ENV TZ UTC
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
-RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install --no-install-recommends -y \
ca-certificates cron \
diff --git a/Docker/FreshRSS.Apache.conf b/Docker/FreshRSS.Apache.conf
index 2cfb9cbf9..6281e59e5 100644
--- a/Docker/FreshRSS.Apache.conf
+++ b/Docker/FreshRSS.Apache.conf
@@ -4,7 +4,7 @@ DocumentRoot /var/www/FreshRSS/p/
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 10.0.0.1/8 172.16.0.1/12 192.168.0.1/16
LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined_proxy
-CustomLog /dev/stdout combined_proxy
+CustomLog "|/var/www/FreshRSS/cli/sensitive-log.sh" combined_proxy
ErrorLog /dev/stderr
AllowEncodedSlashes On
ServerTokens OS
diff --git a/Docker/README.md b/Docker/README.md
index 44963ba0c..a4c0e8289 100644
--- a/Docker/README.md
+++ b/Docker/README.md
@@ -81,7 +81,7 @@ and with newer packages in general (Apache, PHP).
## Environment variables
-* `TZ`: (default is `UTC`) A [server timezone](http://php.net/timezones) (default is `UTC`)
+* `TZ`: (default is `UTC`) A [server timezone](http://php.net/timezones)
* `CRON_MIN`: (default is disabled) Define minutes for the built-in cron job to automatically refresh feeds (see below for more advanced options)
* `FRESHRSS_ENV`: (default is `production`) Enables additional development information if set to `development` (increases the level of logging and ensures that errors are displayed) (see below for more development options)
* `COPY_LOG_TO_SYSLOG`: (default is `On`) Copy all the logs to syslog
@@ -303,6 +303,7 @@ services:
options:
max-size: 10m
volumes:
+ # Recommended volume for FreshRSS persistent data such as configuration and SQLite databases
- data:/var/www/FreshRSS/data
# Optional volume for storing third-party extensions
- extensions:/var/www/FreshRSS/extensions
@@ -314,8 +315,11 @@ services:
# If you want to open a port 8080 on the local machine:
- "8080:80"
environment:
+ # A timezone http://php.net/timezones (default is UTC)
TZ: Europe/Paris
+ # Cron job to refresh feeds at specified minutes
CRON_MIN: '2,32'
+ # 'development' for additional logs; default is 'production'
FRESHRSS_ENV: development
# Optional advanced parameter controlling the internal Apache listening port
LISTEN: 0.0.0.0:80
diff --git a/Docker/entrypoint.sh b/Docker/entrypoint.sh
index 82c2e358e..cbc2443d6 100755
--- a/Docker/entrypoint.sh
+++ b/Docker/entrypoint.sh
@@ -1,6 +1,7 @@
#!/bin/sh
-php -f ./cli/prepare.php >/dev/null
+ln -snf "/usr/share/zoneinfo/$TZ" /etc/localtime
+echo "$TZ" >/etc/timezone
find /etc/php*/ -type f -name php.ini -exec sed -r -i "\\#^;?date.timezone#s#^.*#date.timezone = $TZ#" {} \;
find /etc/php*/ -type f -name php.ini -exec sed -r -i "\\#^;?post_max_size#s#^.*#post_max_size = 32M#" {} \;
@@ -21,6 +22,10 @@ if [ -n "$CRON_MIN" ]; then
-r "s#^[^ ]+ #$CRON_MIN #" | crontab -
fi
+./cli/access-permissions.sh
+
+php -f ./cli/prepare.php >/dev/null
+
if [ -n "$FRESHRSS_INSTALL" ]; then
# shellcheck disable=SC2046
php -f ./cli/do-install.php -- \
@@ -54,7 +59,6 @@ if [ -n "$FRESHRSS_USER" ]; then
fi
fi
-chown -R :www-data .
-chmod -R g+r . && chmod -R g+w ./data/
+./cli/access-permissions.sh
exec "$@"
diff --git a/Makefile b/Makefile
index 6216dfcec..d94fd704a 100644
--- a/Makefile
+++ b/Makefile
@@ -60,40 +60,37 @@ stop: ## Stop FreshRSS container if any
## Tests and linter ##
######################
.PHONY: test
-test: bin/phpunit ## Run the test suite
- $(PHP) ./bin/phpunit --bootstrap ./tests/bootstrap.php ./tests
+test: vendor/bin/phpunit ## Run the test suite
+ $(PHP) vendor/bin/phpunit --bootstrap ./tests/bootstrap.php ./tests
.PHONY: lint
-lint: bin/phpcs ## Run the linter on the PHP files
- $(PHP) ./bin/phpcs . -p -s
+lint: vendor/bin/phpcs ## Run the linter on the PHP files
+ $(PHP) vendor/bin/phpcs . -p -s
.PHONY: lint-fix
-lint-fix: bin/phpcbf ## Fix the errors detected by the linter
- $(PHP) ./bin/phpcbf . -p -s
+lint-fix: vendor/bin/phpcbf ## Fix the errors detected by the linter
+ $(PHP) vendor/bin/phpcbf . -p -s
bin/composer:
mkdir -p bin/
- wget 'https://raw.githubusercontent.com/composer/getcomposer.org/76a7060ccb93902cd7576b67264ad91c8a2700e2/web/installer' -O - -q | php -- --quiet --install-dir='./bin/' --filename='composer'
+ wget 'https://raw.githubusercontent.com/composer/getcomposer.org/b5dbe5ebdec95ce71b3128b359bd5a85cb0a722d/web/installer' -O - -q | php -- --quiet --install-dir='./bin/' --filename='composer'
-bin/phpunit:
- mkdir -p bin/
- wget -O bin/phpunit 'https://phar.phpunit.de/phpunit-9.5.20.phar'
- echo '6becad2da5c37f5ad101cc665ef05a2f1a6a45d2427c8edcc74f72c92fb1e05a bin/phpunit' | sha256sum -c - || rm bin/phpunit
+vendor/bin/phpunit: bin/composer
+ bin/composer install --prefer-dist --no-progress
+ ln -s ../vendor/bin/phpunit bin/phpunit
-bin/phpcs:
- mkdir -p bin/
- wget -O bin/phpcs 'https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.7.1/phpcs.phar'
- echo '7a14323a14af9f58302d15442492ee1076a8cd72c018a816cb44965bf3a9b015 bin/phpcs' | sha256sum -c - || rm bin/phpcs
+vendor/bin/phpcs: bin/composer
+ bin/composer install --prefer-dist --no-progress
+ ln -s ../vendor/bin/phpcs bin/phpcs
-bin/phpcbf:
- mkdir -p bin/
- wget -O bin/phpcbf 'https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.7.1/phpcbf.phar'
- echo 'c93c0e83cbda21c21f849ccf0f4b42979d20004a5a6172ed0ea270eca7ae6fa8 bin/phpcbf' | sha256sum -c - || rm bin/phpcbf
+vendor/bin/phpcbf: bin/composer
+ bin/composer install --prefer-dist --no-progress
+ ln -s ../vendor/bin/phpcbf bin/phpcbf
bin/typos:
mkdir -p bin/
cd bin ; \
- wget -q 'https://github.com/crate-ci/typos/releases/download/v1.10.1/typos-v1.10.1-x86_64-unknown-linux-musl.tar.gz' && \
+ wget -q 'https://github.com/crate-ci/typos/releases/download/v1.13.6/typos-v1.13.6-x86_64-unknown-linux-musl.tar.gz' && \
tar -xvf *.tar.gz './typos' && \
chmod +x typos && \
rm *.tar.gz ; \
@@ -102,6 +99,9 @@ bin/typos:
node_modules/.bin/eslint:
npm install
+node_modules/.bin/rtlcss:
+ npm install
+
vendor/bin/phpstan: bin/composer
bin/composer install --prefer-dist --no-progress
@@ -181,8 +181,8 @@ endif
## TOOLS ##
###########
.PHONY: rtl
-rtl: ## Generate RTL CSS files
- rtlcss -d p/themes/ && find p/themes/ -type f -name '*.rtl.rtl.css' -delete
+rtl: node_modules/.bin/rtlcss ## Generate RTL CSS files
+ npm run-script rtlcss
.PHONY: pot
pot: ## Generate POT templates for docs
diff --git a/README.fr.md b/README.fr.md
index d5119de4c..559a57745 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -5,7 +5,7 @@
# FreshRSS
-FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](https://github.com/LeedRSS/Leed) ou de [Kriss Feed](https://tontof.net/kriss/feed/).
+FreshRSS est un agrégateur de flux RSS à auto-héberger.
Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
@@ -19,21 +19,29 @@ FreshRSS supporte nativement le moissonnage du Web (Web Scraping) basique, basé
Enfin, il permet l’ajout d’[extensions](#extensions) pour encore plus de personnalisation.
-Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues).
-Nous sommes une communauté amicale.
-
* Site officiel : <https://freshrss.org>
* Démo : <http://demo.freshrss.org/>
* Licence : [GNU AGPL 3](https://www.gnu.org/licenses/agpl-3.0.fr.html)
![Logo de FreshRSS](docs/img/FreshRSS-logo.png)
-# Avertissements
+## Contributions
-FreshRSS n’est fourni avec aucune garantie.
+Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues).
+Nous sommes une communauté amicale.
+
+Pour faciliter les contributions, l’option suivante est disponible :
+
+[![Ouvrir dans GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=edge&repo=6322699)
+
+## Capture d’écran
![Capture d’écran de FreshRSS](docs/img/FreshRSS-screenshot.png)
+## Avertissements
+
+FreshRSS n’est fourni avec aucune garantie.
+
# [Documentation](https://freshrss.github.io/FreshRSS/fr/)
* La [documentation utilisateurs](https://freshrss.github.io/FreshRSS/fr/users/02_First_steps.html) pour découvrir les fonctionnalités de FreshRSS.
@@ -41,28 +49,24 @@ FreshRSS n’est fourni avec aucune garantie.
* La [documentation développeurs](https://freshrss.github.io/FreshRSS/fr/developers/01_First_steps.html) pour savoir comment contribuer et mieux comprendre le code source de FreshRSS.
* Le [guide de contribution](https://freshrss.github.io/FreshRSS/fr/contributing.html) pour nous aider à développer FreshRSS.
-# Prérequis
+## Prérequis
* Un navigateur Web récent tel que Firefox / IceCat, Edge, Chromium / Chrome, Opera, Safari.
* Fonctionne aussi sur mobile (sauf certaines fonctionnalités)
* Serveur modeste, par exemple sous Linux ou Windows
* Fonctionne même sur un Raspberry Pi 1 avec des temps de réponse < 1s (testé sur 150 flux, 22k articles)
* Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres)
-* PHP 7.0+
+* PHP 7.2+
* Requis : [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype), et [PDO_MySQL](https://www.php.net/pdo-mysql) ou [PDO_SQLite](https://www.php.net/pdo-sqlite) ou [PDO_PGSQL](https://www.php.net/pdo-pgsql)
* Recommandés : [GMP](https://www.php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](https://www.php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](https://www.php.net/mbstring) (pour le texte Unicode), [iconv](https://www.php.net/iconv) (pour conversion d’encodages), [ZIP](https://www.php.net/zip) (pour import/export), [zlib](https://www.php.net/zlib) (pour les flux compressés)
* MySQL 5.5.3+ ou équivalent MariaDB, ou SQLite 3.7.4+, ou PostgreSQL 9.5+
-
-# Téléchargement
+# [Installation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html)
Si vous préférez que votre FreshRSS soit stable, vous devriez télécharger la dernière version. De nouvelles versions sont publiées tous les 2 ou 3 mois. Voir la [liste des versions](https://github.com/FreshRSS/FreshRSS/releases).
Si vous voulez une publication continue (rolling release) avec les dernières nouveautés, ou bien aider à tester ou développer la future version stable, vous pouvez utiliser [la branche edge](https://github.com/FreshRSS/FreshRSS/tree/edge/).
-
-# [Installation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html)
-
## Installation automatisée
* [<img src="https://www.docker.com/wp-content/uploads/2022/03/horizontal-logo-monochromatic-white.png" width="200" alt="Docker" />](./Docker/)
@@ -83,7 +87,7 @@ Si vous voulez une publication continue (rolling release) avec les dernières no
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.html).
-### Exemple d’installation complète sur Linux Debian/Ubuntu
+## Exemple d’installation complète sur Linux Debian/Ubuntu
```sh
# Si vous utilisez le serveur Web Apache (sinon il faut un autre serveur Web)
@@ -105,11 +109,12 @@ sudo apt-get install git
sudo git clone https://github.com/FreshRSS/FreshRSS.git
cd FreshRSS
-# Si vous souhaitez utiliser la dernière version stable de FreshRSS
-sudo git checkout $(git describe --tags --abbrev=0)
+# La branche par défault “edge” est la celle de la publication continue,
+# mais vous pouvez changer de branche pour “latest” si vous préférez les versions stables de FreshRSS
+sudo git checkout latest
# Mettre les droits d’accès pour le serveur Web
-sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
+sudo cli/access-permissions.sh
# Si vous souhaitez permettre les mises à jour par l’interface Web
sudo chmod -R g+w .
@@ -122,7 +127,7 @@ sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
# Mettre à jour FreshRSS vers une nouvelle version par git
cd /usr/share/FreshRSS
sudo git pull
-sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
+sudo cli/access-permissions.sh
```
Voir la [documentation de la ligne de commande](cli/README.md) pour plus de détails.
@@ -155,7 +160,7 @@ Créer `/etc/cron.d/FreshRSS` avec :
7,37 * * * * www-data php -f /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
```
-## Conseils
+# Conseils
* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
@@ -175,19 +180,7 @@ Créer `/etc/cron.d/FreshRSS` avec :
* Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/users/*/config.php`
* Vous pouvez exporter votre liste de flux au format OPML soit depuis l’interface Web, soit [en ligne de commande](cli/README.md)
-Pour sauvegarder les articles eux-mêmes :
-
-## Dans le cas où vous utilisez MySQL
-
-Vous pouvez utiliser [phpMyAdmin](https://www.phpmyadmin.net) ou les outils de MySQL :
-
-```sh
-mysqldump --skip-comments --disable-keys --user=<db_user> --password --host <db_host> --result-file=freshrss.dump.sql --databases <freshrss_db>
-```
-
-## Pour toutes les bases supportées
-
-Vous pouvez utiliser la [ligne de commande](cli/README.md) pour exporter votre base de données vers une base de données au format SQLite :
+Pour sauvegarder les articles eux-mêmes, vous pouvez utiliser la [ligne de commande](cli/README.md) pour exporter votre base de données vers une base de données au format SQLite :
```sh
./cli/export-sqlite-for-user.php --user <username> --filename </path/to/db.sqlite>
@@ -250,7 +243,7 @@ et [l’API Fever](https://freshrss.github.io/FreshRSS/fr/users/06_Fever_API.htm
* [SimplePie](https://simplepie.org/)
* [MINZ](https://framagit.org/marienfressinaud/MINZ)
* [php-http-304](https://alexandre.alapetite.fr/doc-alex/php-http-304/)
-* [lib_opml](https://github.com/marienfressinaud/lib_opml)
+* [lib_opml](https://framagit.org/marienfressinaud/lib_opml)
* [PhpGt/CssXPath](https://github.com/PhpGt/CssXPath)
* [PHPMailer](https://github.com/PHPMailer/PHPMailer)
* [Chart.js](https://www.chartjs.org)
diff --git a/README.md b/README.md
index 71b04431d..b0581aae4 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
# FreshRSS
-FreshRSS is a self-hosted RSS feed aggregator like [Leed](https://github.com/LeedRSS/Leed) or [Kriss Feed](https://tontof.net/kriss/feed/).
+FreshRSS is a self-hosted RSS feed aggregator.
It is lightweight, easy to work with, powerful, and customizable.
@@ -19,21 +19,29 @@ FreshRSS natively supports basic Web scraping, based on [XPath](https://www.w3.o
Finally, it supports [extensions](#extensions) for further tuning.
-Feature requests, bug reports, and other contributions are welcome. The best way to contribute is to [open an issue on GitHub](https://github.com/FreshRSS/FreshRSS/issues).
-We are a friendly community.
-
* Official website: <https://freshrss.org>
* Demo: <https://demo.freshrss.org/>
* License: [GNU AGPL 3](https://www.gnu.org/licenses/agpl-3.0.html)
![FreshRSS logo](docs/img/FreshRSS-logo.png)
-# Disclaimer
+## Feedback and contributions
-FreshRSS comes with absolutely no warranty.
+Feature requests, bug reports, and other contributions are welcome. The best way is to [open an issue on GitHub](https://github.com/FreshRSS/FreshRSS/issues).
+We are a friendly community.
+
+To facilitate contributions, the following option is available:
+
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=edge&repo=6322699)
+
+## Screenshot
![FreshRSS screenshot](docs/img/FreshRSS-screenshot.png)
+## Disclaimer
+
+FreshRSS comes with absolutely no warranty.
+
# [Documentation](https://freshrss.github.io/FreshRSS/en/)
* [User documentation](https://freshrss.github.io/FreshRSS/en/users/02_First_steps.html), where you can discover all the possibilities offered by FreshRSS
@@ -48,21 +56,17 @@ FreshRSS comes with absolutely no warranty.
* Light server running Linux or Windows
* It even works on Raspberry Pi 1 with response time under a second (tested with 150 feeds, 22k articles)
* A web server: Apache2 (recommended), nginx, lighttpd (not tested on others)
-* PHP 7.0+
+* PHP 7.2+
* Required extensions: [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype), and [PDO_MySQL](https://www.php.net/pdo-mysql) or [PDO_SQLite](https://www.php.net/pdo-sqlite) or [PDO_PGSQL](https://www.php.net/pdo-pgsql)
* Recommended extensions: [GMP](https://www.php.net/gmp) (for API access on 32-bit platforms), [IDN](https://www.php.net/intl.idn) (for Internationalized Domain Names), [mbstring](https://www.php.net/mbstring) (for Unicode strings), [iconv](https://www.php.net/iconv) (for charset conversion), [ZIP](https://www.php.net/zip) (for import/export), [zlib](https://www.php.net/zlib) (for compressed feeds)
* MySQL 5.5.3+ or MariaDB equivalent, or SQLite 3.7.4+, or PostgreSQL 9.5+
-
-# Releases
+# [Installation](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html)
The latest stable release can be found [here](https://github.com/FreshRSS/FreshRSS/releases/latest). New versions are released every two to three months.
If you want a rolling release with the newest features, or want to help testing or developing the next stable version, you can use [the `edge` branch](https://github.com/FreshRSS/FreshRSS/tree/edge/).
-
-# [Installation](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html)
-
## Automated install
* [<img src="https://www.docker.com/wp-content/uploads/2022/03/horizontal-logo-monochromatic-white.png" width="200" alt="Docker" />](./Docker/)
@@ -83,7 +87,7 @@ If you want a rolling release with the newest features, or want to help testing
More detailed information about installation and server configuration can be found in [our documentation](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html).
-## Advice
+# Advice
* For better security, expose only the `./p/` folder to the Web.
* Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it.
@@ -138,7 +142,7 @@ and [Fever API](https://freshrss.github.io/FreshRSS/en/users/06_Fever_API.html)
* [SimplePie](https://simplepie.org/)
* [MINZ](https://framagit.org/marienfressinaud/MINZ)
* [php-http-304](https://alexandre.alapetite.fr/doc-alex/php-http-304/)
-* [lib_opml](https://github.com/marienfressinaud/lib_opml)
+* [lib_opml](https://framagit.org/marienfressinaud/lib_opml)
* [PhpGt/CssXPath](https://github.com/PhpGt/CssXPath)
* [PHPMailer](https://github.com/PHPMailer/PHPMailer)
* [Chart.js](https://www.chartjs.org)
diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php
index 613bacade..791d58d6d 100755..100644
--- a/app/Controllers/configureController.php
+++ b/app/Controllers/configureController.php
@@ -25,6 +25,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
* The options available on the page are:
* - language (default: en)
* - theme (default: Origin)
+ * - darkMode (default: no)
* - content width (default: thin)
* - display of read action in header
* - display of favorite action in header
@@ -42,7 +43,9 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
public function displayAction() {
if (Minz_Request::isPost()) {
FreshRSS_Context::$user_conf->language = Minz_Request::param('language', 'en');
+ FreshRSS_Context::$user_conf->timezone = Minz_Request::param('timezone', '');
FreshRSS_Context::$user_conf->theme = Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme);
+ FreshRSS_Context::$user_conf->darkMode = Minz_Request::param('darkMode', 'no');
FreshRSS_Context::$user_conf->content_width = Minz_Request::param('content_width', 'thin');
FreshRSS_Context::$user_conf->topline_read = Minz_Request::param('topline_read', false);
FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::param('topline_favorite', false);
@@ -106,32 +109,32 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
FreshRSS_Context::$user_conf->posts_per_page = Minz_Request::param('posts_per_page', 10);
FreshRSS_Context::$user_conf->view_mode = Minz_Request::param('view_mode', 'normal');
FreshRSS_Context::$user_conf->default_view = Minz_Request::param('default_view', 'adaptive');
- FreshRSS_Context::$user_conf->show_fav_unread = Minz_Request::param('show_fav_unread', false);
- FreshRSS_Context::$user_conf->auto_load_more = Minz_Request::param('auto_load_more', false);
- FreshRSS_Context::$user_conf->display_posts = Minz_Request::param('display_posts', false);
+ FreshRSS_Context::$user_conf->show_fav_unread = Minz_Request::paramBoolean('show_fav_unread');
+ FreshRSS_Context::$user_conf->auto_load_more = Minz_Request::paramBoolean('auto_load_more');
+ FreshRSS_Context::$user_conf->display_posts = Minz_Request::paramBoolean('display_posts');
FreshRSS_Context::$user_conf->display_categories = Minz_Request::param('display_categories', 'active');
FreshRSS_Context::$user_conf->show_tags = Minz_Request::param('show_tags', '0');
FreshRSS_Context::$user_conf->show_tags_max = Minz_Request::param('show_tags_max', '0');
FreshRSS_Context::$user_conf->show_author_date = Minz_Request::param('show_author_date', '0');
FreshRSS_Context::$user_conf->show_feed_name = Minz_Request::param('show_feed_name', 't');
- FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::param('hide_read_feeds', false);
- FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::param('onread_jump_next', false);
- FreshRSS_Context::$user_conf->lazyload = Minz_Request::param('lazyload', false);
- FreshRSS_Context::$user_conf->sides_close_article = Minz_Request::param('sides_close_article', false);
- FreshRSS_Context::$user_conf->sticky_post = Minz_Request::param('sticky_post', false);
- FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::param('reading_confirm', false);
- FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::param('auto_remove_article', false);
- FreshRSS_Context::$user_conf->mark_updated_article_unread = Minz_Request::param('mark_updated_article_unread', false);
+ FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::paramBoolean('hide_read_feeds');
+ FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::paramBoolean('onread_jump_next');
+ FreshRSS_Context::$user_conf->lazyload = Minz_Request::paramBoolean('lazyload');
+ FreshRSS_Context::$user_conf->sides_close_article = Minz_Request::paramBoolean('sides_close_article');
+ FreshRSS_Context::$user_conf->sticky_post = Minz_Request::paramBoolean('sticky_post');
+ FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::paramBoolean('reading_confirm');
+ FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::paramBoolean('auto_remove_article');
+ FreshRSS_Context::$user_conf->mark_updated_article_unread = Minz_Request::paramBoolean('mark_updated_article_unread');
FreshRSS_Context::$user_conf->sort_order = Minz_Request::param('sort_order', 'DESC');
FreshRSS_Context::$user_conf->mark_when = array(
- 'article' => Minz_Request::param('mark_open_article', false),
- 'gone' => Minz_Request::param('read_upon_gone', false),
+ 'article' => Minz_Request::paramBoolean('mark_open_article'),
+ 'gone' => Minz_Request::paramBoolean('read_upon_gone'),
'max_n_unread' => Minz_Request::paramBoolean('enable_keep_max_n_unread') ? Minz_Request::param('keep_max_n_unread', false) : false,
- 'reception' => Minz_Request::param('mark_upon_reception', false),
+ 'reception' => Minz_Request::paramBoolean('mark_upon_reception'),
'same_title_in_feed' => Minz_Request::paramBoolean('enable_read_when_same_title_in_feed') ?
Minz_Request::param('read_when_same_title_in_feed', false) : false,
- 'scroll' => Minz_Request::param('mark_scroll', false),
- 'site' => Minz_Request::param('mark_open_site', false),
+ 'scroll' => Minz_Request::paramBoolean('mark_scroll'),
+ 'site' => Minz_Request::paramBoolean('mark_open_site'),
);
FreshRSS_Context::$user_conf->save();
invalidateHttpCache();
diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php
index 6750de43b..6750de43b 100755..100644
--- a/app/Controllers/entryController.php
+++ b/app/Controllers/entryController.php
diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php
index 319faece8..84f38fe5e 100755..100644
--- a/app/Controllers/feedController.php
+++ b/app/Controllers/feedController.php
@@ -81,6 +81,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$feed->load(true); //Throws FreshRSS_Feed_Exception, Minz_FileNotExistException
break;
case FreshRSS_Feed::KIND_HTML_XPATH:
+ case FreshRSS_Feed::KIND_XML_XPATH:
$feed->_website($url);
break;
}
@@ -172,7 +173,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$proxy_address = Minz_Request::param('curl_params', '');
$proxy_type = Minz_Request::param('proxy_type', '');
$opts = [];
- if ($proxy_address !== '' && $proxy_type !== '' && in_array($proxy_type, [0, 2, 4, 5, 6, 7])) {
+ if ($proxy_type !== '') {
$opts[CURLOPT_PROXY] = $proxy_address;
$opts[CURLOPT_PROXYTYPE] = intval($proxy_type);
}
@@ -201,8 +202,8 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$timeout = intval(Minz_Request::param('timeout', 0));
$attributes['timeout'] = $timeout > 0 ? $timeout : null;
- $feed_kind = Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS);
- if ($feed_kind == FreshRSS_Feed::KIND_HTML_XPATH) {
+ $feed_kind = (int)Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS);
+ if ($feed_kind === FreshRSS_Feed::KIND_HTML_XPATH || $feed_kind === FreshRSS_Feed::KIND_XML_XPATH) {
$xPathSettings = [];
if (Minz_Request::param('xPathFeedTitle', '') != '') $xPathSettings['feedTitle'] = Minz_Request::param('xPathFeedTitle', '', true);
if (Minz_Request::param('xPathItem', '') != '') $xPathSettings['item'] = Minz_Request::param('xPathItem', '', true);
@@ -385,10 +386,15 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
if ($simplePiePush) {
$simplePie = $simplePiePush; //Used by WebSub
} elseif ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH) {
- $simplePie = $feed->loadHtmlXpath(false, $isNewFeed);
- if ($simplePie == null) {
+ $simplePie = $feed->loadHtmlXpath();
+ if ($simplePie === null) {
throw new FreshRSS_Feed_Exception('HTML+XPath Web scraping failed for [' . $feed->url(false) . ']');
}
+ } elseif ($feed->kind() === FreshRSS_Feed::KIND_XML_XPATH) {
+ $simplePie = $feed->loadHtmlXpath();
+ if ($simplePie === null) {
+ throw new FreshRSS_Feed_Exception('XML+XPath parsing failed for [' . $feed->url(false) . ']');
+ }
} else {
$simplePie = $feed->load(false, $isNewFeed);
}
@@ -949,7 +955,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$this->view->htmlContent = $fullContent;
} else {
$this->view->selectorSuccess = false;
- $this->view->htmlContent = $entry->content();
+ $this->view->htmlContent = $entry->content(false);
}
} catch (Exception $e) {
$this->view->fatalError = _t('feedback.sub.feed.selector_preview.http_error');
diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php
index a1e1106c1..6c4b684e9 100644
--- a/app/Controllers/importExportController.php
+++ b/app/Controllers/importExportController.php
@@ -21,8 +21,6 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
Minz_Error::error(403);
}
- require_once(LIB_PATH . '/lib_opml.php');
-
$this->entryDAO = FreshRSS_Factory::createEntryDao();
$this->feedDAO = FreshRSS_Factory::createFeedDao();
}
diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php
index 7fced48af..968518e3f 100755..100644
--- a/app/Controllers/indexController.php
+++ b/app/Controllers/indexController.php
@@ -237,8 +237,6 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
return;
}
- require_once(LIB_PATH . '/lib_opml.php');
-
// No layout for OPML output.
$this->view->_layout(false);
header('Content-Type: application/xml; charset=utf-8');
diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php
index c2a5cb872..b4e769738 100755..100644
--- a/app/Controllers/javascriptController.php
+++ b/app/Controllers/javascriptController.php
@@ -1,11 +1,11 @@
<?php
class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
- public function firstAction() {
+ public function firstAction(): void {
$this->view->_layout(false);
}
- public function actualizeAction() {
+ public function actualizeAction(): void {
header('Content-Type: application/json; charset=UTF-8');
Minz_Session::_param('actualize_feeds', false);
@@ -16,7 +16,7 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
$this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
}
- public function nbUnreadsPerFeedAction() {
+ public function nbUnreadsPerFeedAction(): void {
header('Content-Type: application/json; charset=UTF-8');
$catDAO = FreshRSS_Factory::createCategoryDao();
$this->view->categories = $catDAO->listCategories(true, false);
@@ -25,7 +25,7 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
}
//For Web-form login
- public function nonceAction() {
+ public function nonceAction(): void {
header('Content-Type: application/json; charset=UTF-8');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T'));
header('Expires: 0');
diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php
index 1798ee3cf..16b09d702 100644
--- a/app/Controllers/statsController.php
+++ b/app/Controllers/statsController.php
@@ -10,7 +10,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
* the common boiler plate for every action. It is triggered by the
* underlying framework.
*/
- public function firstAction() {
+ public function firstAction(): void {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
@@ -32,27 +32,6 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
FreshRSS_View::prependTitle(_t('admin.stats.title') . ' · ');
}
- private function convertToSeries($data) {
- $series = array();
-
- foreach ($data as $key => $value) {
- $series[] = array($key, $value);
- }
-
- return $series;
- }
-
- private function convertToPieSeries($data) {
- $series = array();
-
- foreach ($data as $value) {
- $value['data'] = array(array(0, (int) $value['data']));
- $series[] = $value;
- }
-
- return $series;
- }
-
/**
* This action handles the statistic main page.
*
@@ -64,7 +43,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
* - number of article by category (entryByCategory)
* - list of most prolific feed (topFeed)
*/
- public function indexAction() {
+ public function indexAction(): void {
$statsDAO = FreshRSS_Factory::createStatsDAO();
FreshRSS_View::appendScript(Minz_Url::display('/scripts/vendor/chart.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/vendor/chart.min.js')));
@@ -94,7 +73,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
$last30DaysLabels = [];
for ($i = 0; $i < 30; $i++) {
- $last30DaysLabels[$i] = date('d.m.Y', strtotime((-30 + $i) . ' days'));
+ $last30DaysLabels[$i] = date('d.m.Y', strtotime((-30 + $i) . ' days') ?: null);
}
$this->view->last30DaysLabels = $last30DaysLabels;
@@ -106,9 +85,9 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
* to use the subscription controller to save it,
* but shows the stats idle page
*/
- public function feedAction() {
- $id = Minz_Request::param('id');
- $ajax = Minz_Request::param('ajax');
+ public function feedAction(): void {
+ $id = '' . Minz_Request::param('id', '');
+ $ajax = '' . Minz_Request::param('ajax', '');
if ($ajax) {
$url_redirect = array('c' => 'subscription', 'a' => 'feed', 'params' => array('id' => $id, 'from' => 'stats', 'ajax' => $ajax));
} else {
@@ -131,7 +110,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
* - last month
* - last week
*/
- public function idleAction() {
+ public function idleAction(): void {
FreshRSS_View::appendScript(Minz_Url::display('/scripts/feed.js?' . @filemtime(PUBLIC_PATH . '/scripts/feed.js')));
$feed_dao = FreshRSS_Factory::createFeedDao();
$statsDAO = FreshRSS_Factory::createStatsDAO();
@@ -216,7 +195,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
* @todo verify that the metrics used here make some sense. Especially
* for the average.
*/
- public function repartitionAction() {
+ public function repartitionAction(): void {
$statsDAO = FreshRSS_Factory::createStatsDAO();
$categoryDAO = FreshRSS_Factory::createCategoryDao();
$feedDAO = FreshRSS_Factory::createFeedDao();
diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php
index 4a63d1ee4..f0355a82a 100644
--- a/app/Controllers/subscriptionController.php
+++ b/app/Controllers/subscriptionController.php
@@ -118,8 +118,6 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$httpAuth = $user . ':' . $pass;
}
- $cat = intval(Minz_Request::param('category', 0));
-
$feed->_ttl(intval(Minz_Request::param('ttl', FreshRSS_Feed::TTL_DEFAULT)));
$feed->_mute(boolval(Minz_Request::param('mute', false)));
@@ -149,7 +147,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$proxy_address = Minz_Request::param('curl_params', '');
$proxy_type = Minz_Request::param('proxy_type', '');
$opts = [];
- if ($proxy_address !== '' && $proxy_type !== '' && in_array($proxy_type, [0, 2, 4, 5, 6, 7])) {
+ if ($proxy_type !== '') {
$opts[CURLOPT_PROXY] = $proxy_address;
$opts[CURLOPT_PROXYTYPE] = intval($proxy_type);
}
@@ -205,7 +203,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::param('filteractions_read', '')));
$feed->_kind(intval(Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS)));
- if ($feed->kind() == FreshRSS_Feed::KIND_HTML_XPATH) {
+ if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH || $feed->kind() === FreshRSS_Feed::KIND_XML_XPATH) {
$xPathSettings = [];
if (Minz_Request::param('xPathItem', '') != '') $xPathSettings['item'] = Minz_Request::param('xPathItem', '', true);
if (Minz_Request::param('xPathItemTitle', '') != '') $xPathSettings['itemTitle'] = Minz_Request::param('xPathItemTitle', '', true);
@@ -230,7 +228,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
'website' => checkUrl(Minz_Request::param('website', '')),
'url' => checkUrl(Minz_Request::param('url', '')),
- 'category' => $cat,
+ 'category' => intval(Minz_Request::param('category', 0)),
'pathEntries' => Minz_Request::param('path_entries', ''),
'priority' => intval(Minz_Request::param('priority', FreshRSS_Feed::PRIORITY_MAIN_STREAM)),
'httpAuth' => $httpAuth,
@@ -258,12 +256,18 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$url_redirect = array('c' => 'subscription', 'params' => array('id' => $id));
}
- if ($feedDAO->updateFeed($id, $values) !== false) {
- $feed->_categoryId($cat);
+ if ($values['url'] != '' && $feedDAO->updateFeed($id, $values) !== false) {
+ $feed->_categoryId($values['category']);
+ // update url and website values for faviconPrepare
+ $feed->_url($values['url'], false);
+ $feed->_website($values['website'], false);
$feed->faviconPrepare();
Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect);
} else {
+ if ($values['url'] == '') {
+ Minz_Log::warning('Invalid feed URL!');
+ }
Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect);
}
}
diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php
index 675bd7def..f638ce96c 100644
--- a/app/Controllers/updateController.php
+++ b/app/Controllers/updateController.php
@@ -14,7 +14,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
public static function migrateToGitEdge() {
$errorMessage = 'Error during git checkout to edge branch. Please change branch manually!';
- if (!is_writable(FRESHRSS_PATH . '/.git/')) {
+ if (!is_writable(FRESHRSS_PATH . '/.git/config')) {
throw new Exception($errorMessage);
}
@@ -23,7 +23,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
if ($return != 0) {
throw new Exception($errorMessage);
}
- $line = is_array($output) ? implode('', $output) : $output;
+ $line = implode('', $output);
if ($line !== 'master' && $line !== 'dev') {
return true; // not on master or dev, nothing to do
}
@@ -54,14 +54,14 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
$output = [];
exec('git status -sb --porcelain remote', $output, $return);
} else {
- $line = is_array($output) ? implode('; ', $output) : $output;
+ $line = implode('; ', $output);
Minz_Log::warning('git fetch warning: ' . $line);
}
} catch (Exception $e) {
Minz_Log::warning('git fetch error: ' . $e->getMessage());
}
chdir($cwd);
- $line = is_array($output) ? implode('; ', $output) : $output;
+ $line = implode('; ', $output);
return $line == '' ||
strpos($line, '[behind') !== false || strpos($line, '[ahead') !== false || strpos($line, '[gone') !== false;
}
@@ -118,7 +118,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
if ($version == '') {
$version = 'unknown';
}
- if (is_writable(FRESHRSS_PATH)) {
+ if (touch(FRESHRSS_PATH . '/index.html')) {
$this->view->update_to_apply = true;
$this->view->message = array(
'status' => 'good',
@@ -217,7 +217,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
}
public function applyAction() {
- if (!file_exists(UPDATE_FILENAME) || !is_writable(FRESHRSS_PATH) || Minz_Configuration::get('system')->disable_update) {
+ if (FreshRSS_Context::$system_conf->disable_update || !file_exists(UPDATE_FILENAME) || !touch(FRESHRSS_PATH . '/index.html')) {
Minz_Request::forward(array('c' => 'update'), true);
}
diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php
index 06dbab9fa..ac8f3be82 100644
--- a/app/Controllers/userController.php
+++ b/app/Controllers/userController.php
@@ -242,7 +242,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
}
if ($ok) {
if (!is_dir($homeDir)) {
- mkdir($homeDir);
+ mkdir($homeDir, 0770, true);
}
$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
}
@@ -344,6 +344,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
$ok = self::createUser($new_user_name, $email, $passwordPlain, array(
'language' => Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language),
+ 'timezone' => Minz_Request::param('new_user_timezone', ''),
'is_admin' => Minz_Request::paramBoolean('new_user_is_admin'),
'enabled' => true,
));
diff --git a/app/FreshRSS.php b/app/FreshRSS.php
index 602c46658..76ced841c 100644
--- a/app/FreshRSS.php
+++ b/app/FreshRSS.php
@@ -18,7 +18,7 @@ class FreshRSS extends Minz_FrontController {
* - Init notifications
* - Enable user extensions (need all the other initializations)
*/
- public function init() {
+ public function init(): void {
if (!isset($_SESSION)) {
Minz_Session::init('FreshRSS');
}
@@ -71,10 +71,10 @@ class FreshRSS extends Minz_FrontController {
Minz_ExtensionManager::callHook('freshrss_init');
}
- private static function initAuth() {
+ private static function initAuth(): void {
FreshRSS_Auth::init();
if (Minz_Request::isPost()) {
- if (!(FreshRSS_Auth::isCsrfOk() ||
+ if (FreshRSS_Context::$system_conf == null || !(FreshRSS_Auth::isCsrfOk() ||
(Minz_Request::controllerName() === 'auth' && Minz_Request::actionName() === 'login') ||
(Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'create' && !FreshRSS_Auth::hasAccess('admin')) ||
(Minz_Request::controllerName() === 'feed' && Minz_Request::actionName() === 'actualize'
@@ -92,21 +92,30 @@ class FreshRSS extends Minz_FrontController {
}
}
- private static function initI18n() {
+ private static function initI18n(): void {
$userLanguage = isset(FreshRSS_Context::$user_conf) ? FreshRSS_Context::$user_conf->language : null;
$systemLanguage = isset(FreshRSS_Context::$system_conf) ? FreshRSS_Context::$system_conf->language : null;
$language = Minz_Translate::getLanguage($userLanguage, Minz_Request::getPreferredLanguages(), $systemLanguage);
Minz_Session::_param('language', $language);
Minz_Translate::init($language);
+
+ $timezone = isset(FreshRSS_Context::$user_conf) ? FreshRSS_Context::$user_conf->timezone : '';
+ if ($timezone == '') {
+ $timezone = FreshRSS_Context::defaultTimeZone();
+ }
+ date_default_timezone_set($timezone);
}
- private static function getThemeFileUrl($theme_id, $filename) {
+ private static function getThemeFileUrl(string $theme_id, string $filename): string {
$filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename);
return '/themes/' . $theme_id . '/' . $filename . '?' . $filetime;
}
- public static function loadStylesAndScripts() {
+ public static function loadStylesAndScripts(): void {
+ if (FreshRSS_Context::$user_conf == null) {
+ return;
+ }
$theme = FreshRSS_Themes::load(FreshRSS_Context::$user_conf->theme);
if ($theme) {
foreach(array_reverse($theme['files']) as $file) {
@@ -140,22 +149,23 @@ class FreshRSS extends Minz_FrontController {
FreshRSS_View::prependScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
}
- private static function loadNotifications() {
+ private static function loadNotifications(): void {
$notif = Minz_Request::getNotification();
if ($notif) {
FreshRSS_View::_param('notification', $notif);
}
}
- public static function preLayout() {
+ public static function preLayout(): void {
header("X-Content-Type-Options: nosniff");
FreshRSS_Share::load(join_path(APP_PATH, 'shares.php'));
self::loadStylesAndScripts();
}
- private static function checkEmailValidated() {
- $email_not_verified = FreshRSS_Auth::hasAccess() && FreshRSS_Context::$user_conf->email_validation_token !== '';
+ private static function checkEmailValidated(): void {
+ $email_not_verified = FreshRSS_Auth::hasAccess() &&
+ FreshRSS_Context::$user_conf !== null && FreshRSS_Context::$user_conf->email_validation_token !== '';
$action_is_allowed = (
Minz_Request::is('user', 'validateEmail') ||
Minz_Request::is('user', 'sendValidationEmail') ||
diff --git a/app/Models/BooleanSearch.php b/app/Models/BooleanSearch.php
index b1c7bbd3b..279040a5a 100644
--- a/app/Models/BooleanSearch.php
+++ b/app/Models/BooleanSearch.php
@@ -118,8 +118,9 @@ class FreshRSS_BooleanSearch {
$nextOperator = 'AND';
while ($i < $length) {
$c = $input[$i];
+ $backslashed = $i >= 1 ? $input[$i - 1] === '\\' : false;
- if ($c === '(') {
+ if ($c === '(' && !$backslashed) {
$hasParenthesis = true;
$before = trim($before);
@@ -164,11 +165,12 @@ class FreshRSS_BooleanSearch {
$i++;
while ($i < $length) {
$c = $input[$i];
- if ($c === '(') {
+ $backslashed = $input[$i - 1] === '\\';
+ if ($c === '(' && !$backslashed) {
// One nested level deeper
$parentheses++;
$sub .= $c;
- } elseif ($c === ')') {
+ } elseif ($c === ')' && !$backslashed) {
$parentheses--;
if ($parentheses === 0) {
// Found the matching closing parenthesis
diff --git a/app/Models/Category.php b/app/Models/Category.php
index c4ca12fd3..b23e8da0a 100644
--- a/app/Models/Category.php
+++ b/app/Models/Category.php
@@ -103,9 +103,7 @@ class FreshRSS_Category extends Minz_Model {
$this->hasFeedsWithError |= $feed->inError();
}
- usort($this->feeds, function ($a, $b) {
- return strnatcasecmp($a->name(), $b->name());
- });
+ $this->sortFeeds();
}
return $this->feeds;
@@ -144,6 +142,7 @@ class FreshRSS_Category extends Minz_Model {
}
$this->feeds = $values;
+ $this->sortFeeds();
}
/**
@@ -155,6 +154,8 @@ class FreshRSS_Category extends Minz_Model {
$this->feeds = [];
}
$this->feeds[] = $feed;
+
+ $this->sortFeeds();
}
public function _attributes($key, $value) {
@@ -194,7 +195,7 @@ class FreshRSS_Category extends Minz_Model {
} else {
$dryRunCategory = new FreshRSS_Category();
$importService = new FreshRSS_Import_Service();
- $importService->importOpml($opml, $dryRunCategory, true, true);
+ $importService->importOpml($opml, $dryRunCategory, true);
if ($importService->lastStatus()) {
$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -245,4 +246,10 @@ class FreshRSS_Category extends Minz_Model {
return $ok;
}
+
+ private function sortFeeds() {
+ usort($this->feeds, static function ($a, $b) {
+ return strnatcasecmp($a->name(), $b->name());
+ });
+ }
}
diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php
index 20a92d52a..c855f1495 100644
--- a/app/Models/CategoryDAO.php
+++ b/app/Models/CategoryDAO.php
@@ -265,7 +265,7 @@ SQL;
return $categories;
}
- uasort($categories, function ($a, $b) {
+ uasort($categories, static function ($a, $b) {
$aPosition = $a->attributes('position');
$bPosition = $b->attributes('position');
if ($aPosition === $bPosition) {
@@ -310,9 +310,9 @@ SQL;
}
/** @return array<FreshRSS_Category> */
- public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0) {
+ public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0): array {
$sql = 'SELECT * FROM `_category` WHERE kind = :kind AND `lastUpdate` < :lu ORDER BY `lastUpdate`'
- . ($limit < 1 ? '' : ' LIMIT ' . intval($limit));
+ . ($limit < 1 ? '' : ' LIMIT ' . $limit);
$stm = $this->pdo->prepare($sql);
if ($stm &&
$stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) &&
@@ -387,7 +387,7 @@ SQL;
return $res[0]['count'];
}
- public function countFeed($id) {
+ public function countFeed(int $id) {
$sql = 'SELECT COUNT(*) AS count FROM `_feed` WHERE category=:id';
$stm = $this->pdo->prepare($sql);
$stm->bindParam(':id', $id, PDO::PARAM_INT);
@@ -396,7 +396,7 @@ SQL;
return $res[0]['count'];
}
- public function countNotRead($id) {
+ public function countNotRead(int $id) {
$sql = 'SELECT COUNT(*) AS count FROM `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id WHERE category=:id AND e.is_read=0';
$stm = $this->pdo->prepare($sql);
$stm->bindParam(':id', $id, PDO::PARAM_INT);
@@ -409,7 +409,7 @@ SQL;
* @param array<FreshRSS_Category> $categories
* @param int $feed_id
*/
- public static function findFeed($categories, $feed_id) {
+ public static function findFeed(array $categories, int $feed_id) {
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
if ($feed->id() === $feed_id) {
@@ -422,9 +422,8 @@ SQL;
/**
* @param array<FreshRSS_Category> $categories
- * @param int $minPriority
*/
- public static function CountUnreads($categories, $minPriority = 0) {
+ public static function countUnread(array $categories, int $minPriority = 0): int {
$n = 0;
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php
index c822bcf4d..258c2ad58 100644
--- a/app/Models/ConfigurationSetter.php
+++ b/app/Models/ConfigurationSetter.php
@@ -234,6 +234,13 @@ class FreshRSS_ConfigurationSetter {
$data['sticky_post'] = $this->handleBool($value);
}
+ private function _darkMode(&$data, $value) {
+ if (!in_array($value, [ 'no', 'auto'], true)) {
+ $value = 'no';
+ }
+ $data['darkMode'] = $value;
+ }
+
private function _bottomline_date(&$data, $value) {
$data['bottomline_date'] = $this->handleBool($value);
}
diff --git a/app/Models/Context.php b/app/Models/Context.php
index fed2a6767..734458d7f 100644
--- a/app/Models/Context.php
+++ b/app/Models/Context.php
@@ -58,12 +58,7 @@ class FreshRSS_Context {
public static function initSystem($reload = false) {
if ($reload || FreshRSS_Context::$system_conf == null) {
//TODO: Keep in session what we need instead of always reloading from disk
- Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
- /**
- * @var FreshRSS_SystemConfiguration $system_conf
- */
- $system_conf = Minz_Configuration::get('system');
- FreshRSS_Context::$system_conf = $system_conf;
+ FreshRSS_Context::$system_conf = FreshRSS_SystemConfiguration::init(DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
// Register the configuration setter for the system configuration
$configurationSetter = new FreshRSS_ConfigurationSetter();
FreshRSS_Context::$system_conf->_configurationSetter($configurationSetter);
@@ -88,17 +83,12 @@ class FreshRSS_Context {
(!$userMustExist || FreshRSS_user_Controller::userExists($username))) {
try {
//TODO: Keep in session what we need instead of always reloading from disk
- Minz_Configuration::register('user',
+ FreshRSS_Context::$user_conf = FreshRSS_UserConfiguration::init(
USERS_PATH . '/' . $username . '/config.php',
FRESHRSS_PATH . '/config-user.default.php',
FreshRSS_Context::$system_conf->configurationSetter());
Minz_Session::_param('currentUser', $username);
- /**
- * @var FreshRSS_UserConfiguration $user_conf
- */
- $user_conf = Minz_Configuration::get('user');
- FreshRSS_Context::$user_conf = $user_conf;
} catch (Exception $ex) {
Minz_Log::warning($ex->getMessage(), USERS_PATH . '/_/' . LOG_FILENAME);
}
@@ -163,7 +153,7 @@ class FreshRSS_Context {
// Update number of read / unread variables.
$entryDAO = FreshRSS_Factory::createEntryDao();
self::$total_starred = $entryDAO->countUnreadReadFavorites();
- self::$total_unread = FreshRSS_CategoryDAO::CountUnreads(
+ self::$total_unread = FreshRSS_CategoryDAO::countUnread(
self::$categories, 1
);
@@ -510,4 +500,8 @@ class FreshRSS_Context {
return false;
}
+ public static function defaultTimeZone(): string {
+ $timezone = ini_get('date.timezone');
+ return $timezone != '' ? $timezone : 'UTC';
+ }
}
diff --git a/app/Models/Days.php b/app/Models/Days.php
index 2d770c30b..d3f1ba075 100644
--- a/app/Models/Days.php
+++ b/app/Models/Days.php
@@ -1,7 +1,9 @@
<?php
+declare(strict_types=1);
+
class FreshRSS_Days {
- const TODAY = 0;
- const YESTERDAY = 1;
- const BEFORE_YESTERDAY = 2;
+ public const TODAY = 0;
+ public const YESTERDAY = 1;
+ public const BEFORE_YESTERDAY = 2;
}
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index 47fcf3b4a..81ece1ce4 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -17,10 +17,14 @@ class FreshRSS_Entry extends Minz_Model {
*/
private $guid;
+ /** @var string */
private $title;
private $authors;
+ /** @var string */
private $content;
+ /** @var string */
private $link;
+ /** @var int */
private $date;
private $date_added = 0; //In microseconds
/**
@@ -67,14 +71,16 @@ class FreshRSS_Entry extends Minz_Model {
$dao['content'] = '';
}
if (!empty($dao['thumbnail'])) {
- $dao['content'] .= '<p class="enclosure-content"><img src="' . $dao['thumbnail'] . '" alt="" /></p>';
+ $dao['attributes']['thumbnail'] = [
+ 'url' => $dao['thumbnail'],
+ ];
}
$entry = new FreshRSS_Entry(
$dao['id_feed'] ?? 0,
$dao['guid'] ?? '',
$dao['title'] ?? '',
$dao['author'] ?? '',
- $dao['content'] ?? '',
+ $dao['content'],
$dao['link'] ?? '',
$dao['date'] ?? 0,
$dao['is_read'] ?? false,
@@ -116,15 +122,117 @@ class FreshRSS_Entry extends Minz_Model {
return $this->authors;
}
}
- public function content(): string {
- return $this->content;
+
+ /**
+ * Basic test without ambition to catch all cases such as unquoted addresses, variants of entities, HTML comments, etc.
+ */
+ private static function containsLink(string $html, string $link): bool {
+ return preg_match('/(?P<delim>[\'"])' . preg_quote($link, '/') . '(?P=delim)/', $html) == 1;
+ }
+
+ private static function enclosureIsImage(array $enclosure): bool {
+ $elink = $enclosure['url'] ?? '';
+ $length = $enclosure['length'] ?? 0;
+ $medium = $enclosure['medium'] ?? '';
+ $mime = $enclosure['type'] ?? '';
+
+ return $elink != '' && $medium === 'image' || strpos($mime, 'image') === 0 ||
+ ($mime == '' && $length == 0 && preg_match('/[.](avif|gif|jpe?g|png|svg|webp)$/i', $elink));
}
- /** @return array<array<string,string>> */
- public function enclosures(bool $searchBodyImages = false): array {
- $results = [];
+ /**
+ * @param bool $withEnclosures Set to true to include the enclosures in the returned HTML, false otherwise.
+ * @param bool $allowDuplicateEnclosures Set to false to remove obvious enclosure duplicates (based on simple string comparison), true otherwise.
+ * @return string HTML content
+ */
+ public function content(bool $withEnclosures = true, bool $allowDuplicateEnclosures = false): string {
+ if (!$withEnclosures) {
+ return $this->content;
+ }
+
+ $content = $this->content;
+
+ $thumbnail = $this->attributes('thumbnail');
+ if (!empty($thumbnail['url'])) {
+ $elink = $thumbnail['url'];
+ if ($allowDuplicateEnclosures || !self::containsLink($content, $elink)) {
+ $content .= <<<HTML
+<figure class="enclosure">
+ <p class="enclosure-content">
+ <img class="enclosure-thumbnail" src="{$elink}" alt="" />
+ </p>
+</figure>
+HTML;
+ }
+ }
+
+ $attributeEnclosures = $this->attributes('enclosures');
+ if (empty($attributeEnclosures)) {
+ return $content;
+ }
+
+ foreach ($attributeEnclosures as $enclosure) {
+ $elink = $enclosure['url'] ?? '';
+ if ($elink == '') {
+ continue;
+ }
+ if (!$allowDuplicateEnclosures && self::containsLink($content, $elink)) {
+ continue;
+ }
+ $credit = $enclosure['credit'] ?? '';
+ $description = $enclosure['description'] ?? '';
+ $length = $enclosure['length'] ?? 0;
+ $medium = $enclosure['medium'] ?? '';
+ $mime = $enclosure['type'] ?? '';
+ $thumbnails = $enclosure['thumbnails'] ?? [];
+ $etitle = $enclosure['title'] ?? '';
+
+ $content .= '<figure class="enclosure">';
+
+ foreach ($thumbnails as $thumbnail) {
+ $content .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" title="' . $etitle . '" /></p>';
+ }
+
+ if (self::enclosureIsImage($enclosure)) {
+ $content .= '<p class="enclosure-content"><img src="' . $elink . '" alt="" title="' . $etitle . '" /></p>';
+ } elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) {
+ $content .= '<p class="enclosure-content"><audio preload="none" src="' . $elink
+ . ($length == null ? '' : '" data-length="' . intval($length))
+ . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
+ . '" controls="controls" title="' . $etitle . '"></audio> <a download="" href="' . $elink . '">💾</a></p>';
+ } elseif ($medium === 'video' || strpos($mime, 'video') === 0) {
+ $content .= '<p class="enclosure-content"><video preload="none" src="' . $elink
+ . ($length == null ? '' : '" data-length="' . intval($length))
+ . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
+ . '" controls="controls" title="' . $etitle . '"></video> <a download="" href="' . $elink . '">💾</a></p>';
+ } else { //e.g. application, text, unknown
+ $content .= '<p class="enclosure-content"><a download="" href="' . $elink
+ . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
+ . ($medium == '' ? '' : '" data-medium="' . htmlspecialchars($medium, ENT_COMPAT, 'UTF-8'))
+ . '" title="' . $etitle . '">💾</a></p>';
+ }
+
+ if ($credit != '') {
+ $content .= '<p class="enclosure-credits">© ' . $credit . '</p>';
+ }
+ if ($description != '') {
+ $content .= '<figcaption class="enclosure-description">' . $description . '</figcaption>';
+ }
+ $content .= "</figure>\n";
+ }
+
+ return $content;
+ }
+
+ /** @return iterable<array<string,string>> */
+ public function enclosures(bool $searchBodyImages = false) {
+ $attributeEnclosures = $this->attributes('enclosures');
+ if (is_array($attributeEnclosures)) {
+ // FreshRSS 1.20.1+: The enclosures are saved as attributes
+ yield from $attributeEnclosures;
+ }
try {
- $searchEnclosures = strpos($this->content, '<p class="enclosure-content') !== false;
+ $searchEnclosures = !is_array($attributeEnclosures) && (strpos($this->content, '<p class="enclosure-content') !== false);
$searchBodyImages &= (stripos($this->content, '<img') !== false);
$xpath = null;
if ($searchEnclosures || $searchBodyImages) {
@@ -133,6 +241,7 @@ class FreshRSS_Entry extends Minz_Model {
$xpath = new DOMXpath($dom);
}
if ($searchEnclosures) {
+ // Legacy code for database entries < FreshRSS 1.20.1
$enclosures = $xpath->query('//div[@class="enclosure"]/p[@class="enclosure-content"]/*[@src]');
foreach ($enclosures as $enclosure) {
$result = [
@@ -148,7 +257,7 @@ class FreshRSS_Entry extends Minz_Model {
case 'audio': $result['medium'] = 'audio'; break;
}
}
- $results[] = $result;
+ yield Minz_Helper::htmlspecialchars_utf8($result);
}
}
if ($searchBodyImages) {
@@ -159,26 +268,31 @@ class FreshRSS_Entry extends Minz_Model {
$src = $img->getAttribute('data-src');
}
if ($src != null) {
- $results[] = [
+ $result = [
'url' => $src,
- 'alt' => $img->getAttribute('alt'),
];
+ yield Minz_Helper::htmlspecialchars_utf8($result);
}
}
}
- return $results;
} catch (Exception $ex) {
- return $results;
+ Minz_Log::debug(__METHOD__ . ' ' . $ex->getMessage());
}
}
/**
* @return array<string,string>|null
*/
- public function thumbnail() {
- foreach ($this->enclosures(true) as $enclosure) {
- if (!empty($enclosure['url']) && empty($enclosure['type'])) {
- return $enclosure;
+ public function thumbnail(bool $searchEnclosures = true) {
+ $thumbnail = $this->attributes('thumbnail');
+ if (!empty($thumbnail['url'])) {
+ return $thumbnail;
+ }
+ if ($searchEnclosures) {
+ foreach ($this->enclosures(true) as $enclosure) {
+ if (self::enclosureIsImage($enclosure)) {
+ return $enclosure;
+ }
}
}
return null;
@@ -188,6 +302,7 @@ class FreshRSS_Entry extends Minz_Model {
public function link(): string {
return $this->link;
}
+ /** @return string|int */
public function date(bool $raw = false) {
if ($raw) {
return $this->date;
@@ -587,7 +702,7 @@ class FreshRSS_Entry extends Minz_Model {
if ($entry) {
// l’article existe déjà en BDD, en se contente de recharger ce contenu
- $this->content = $entry->content();
+ $this->content = $entry->content(false);
} else {
try {
// The article is not yet in the database, so let’s fetch it
@@ -629,7 +744,7 @@ class FreshRSS_Entry extends Minz_Model {
'guid' => $this->guid(),
'title' => $this->title(),
'author' => $this->authors(true),
- 'content' => $this->content(),
+ 'content' => $this->content(false),
'link' => $this->link(),
'date' => $this->date(true),
'hash' => $this->hash(),
@@ -677,7 +792,6 @@ class FreshRSS_Entry extends Minz_Model {
'published' => $this->date(true),
// 'updated' => $this->date(true),
'title' => $this->title(),
- 'summary' => ['content' => $this->content()],
'canonical' => [
['href' => htmlspecialchars_decode($this->link(), ENT_QUOTES)],
],
@@ -697,13 +811,16 @@ class FreshRSS_Entry extends Minz_Model {
if ($mode === 'compat') {
$item['title'] = escapeToUnicodeAlternative($this->title(), false);
unset($item['alternate'][0]['type']);
- if (mb_strlen($this->content(), 'UTF-8') > self::API_MAX_COMPAT_CONTENT_LENGTH) {
- $item['summary']['content'] = mb_strcut($this->content(), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8');
- }
- } elseif ($mode === 'freshrss') {
+ $item['summary'] = [
+ 'content' => mb_strcut($this->content(true), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8'),
+ ];
+ } else {
+ $item['content'] = [
+ 'content' => $this->content(false),
+ ];
+ }
+ if ($mode === 'freshrss') {
$item['guid'] = $this->guid();
- unset($item['summary']);
- $item['content'] = ['content' => $this->content()];
}
if ($category != null && $mode !== 'freshrss') {
$item['categories'][] = 'user/-/label/' . htmlspecialchars_decode($category->name(), ENT_QUOTES);
@@ -718,10 +835,11 @@ class FreshRSS_Entry extends Minz_Model {
}
}
foreach ($this->enclosures() as $enclosure) {
- if (!empty($enclosure['url']) && !empty($enclosure['type'])) {
+ if (!empty($enclosure['url'])) {
$media = [
'href' => $enclosure['url'],
- 'type' => $enclosure['type'],
+ 'type' => $enclosure['type'] ?? $enclosure['medium'] ??
+ (self::enclosureIsImage($enclosure) ? 'image' : ''),
];
if (!empty($enclosure['length'])) {
$media['length'] = intval($enclosure['length']);
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index b63515223..3b7c1ac3f 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -10,6 +10,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return true;
}
+ protected static function sqlConcat($s1, $s2) {
+ return 'CONCAT(' . $s1 . ',' . $s2 . ')'; //MySQL
+ }
+
public static function sqlHexDecode(string $x): string {
return 'unhex(' . $x . ')';
}
@@ -943,8 +947,8 @@ SQL;
}
if ($filter->getTags()) {
foreach ($filter->getTags() as $tag) {
- $sub_search .= 'AND ' . $alias . 'tags LIKE ? ';
- $values[] = "%{$tag}%";
+ $sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' LIKE ? ';
+ $values[] = "%{$tag} #%";
}
}
if ($filter->getInurl()) {
@@ -968,8 +972,8 @@ SQL;
}
if ($filter->getNotTags()) {
foreach ($filter->getNotTags() as $tag) {
- $sub_search .= 'AND ' . $alias . 'tags NOT LIKE ? ';
- $values[] = "%{$tag}%";
+ $sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' NOT LIKE ? ';
+ $values[] = "%{$tag} #%";
}
}
if ($filter->getNotInurl()) {
@@ -1161,10 +1165,12 @@ SQL;
}
}
- public function listByIds($ids, $order = 'DESC') {
+ /** @param array<string> $ids */
+ public function listByIds(array $ids, string $order = 'DESC') {
if (count($ids) < 1) {
- yield false;
- } elseif (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) {
+ return;
+ }
+ if (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) {
// Split a query with too many variables parameters
$idsChunks = array_chunk($ids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER);
foreach ($idsChunks as $idsChunk) {
@@ -1191,15 +1197,16 @@ SQL;
/**
* For API
+ * @return array<string>
*/
public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL,
- $order = 'DESC', $limit = 1, $firstId = '', $filters = null) {
+ $order = 'DESC', $limit = 1, $firstId = '', $filters = null): array {
list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
$stm = $this->pdo->prepare($sql);
$stm->execute($values);
- return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+ return $stm->fetchAll(PDO::FETCH_COLUMN, 0) ?: [];
}
public function listHashForFeedGuids($id_feed, $guids) {
diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php
index 8039581e6..35f3ef676 100644
--- a/app/Models/EntryDAOSQLite.php
+++ b/app/Models/EntryDAOSQLite.php
@@ -10,6 +10,10 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
return false;
}
+ protected static function sqlConcat($s1, $s2) {
+ return $s1 . '||' . $s2;
+ }
+
public static function sqlHexDecode(string $x): string {
return $x;
}
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index f24ec1884..7c46199a5 100644
--- a/app/Models/Feed.php
+++ b/app/Models/Feed.php
@@ -18,6 +18,11 @@ class FreshRSS_Feed extends Minz_Model {
*/
const KIND_HTML_XPATH = 10;
/**
+ * Normal XML with XPath scraping
+ * @var int
+ */
+ const KIND_XML_XPATH = 15;
+ /**
* Normal JSON with XPath scraping
* @var int
*/
@@ -259,13 +264,14 @@ class FreshRSS_Feed extends Minz_Model {
}
public function _url(string $value, bool $validate = true) {
$this->hash = '';
+ $url = $value;
if ($validate) {
- $value = checkUrl($value);
+ $url = checkUrl($url);
}
- if ($value == '') {
+ if ($url == '') {
throw new FreshRSS_BadUrl_Exception($value);
}
- $this->url = $value;
+ $this->url = $url;
}
public function _kind(int $value) {
$this->kind = $value;
@@ -502,61 +508,46 @@ class FreshRSS_Feed extends Minz_Model {
$content = html_only_entity_decode($item->get_content());
- if ($item->get_enclosures() != null) {
- $elinks = array();
+ $attributeThumbnail = $item->get_thumbnail() ?? [];
+ if (empty($attributeThumbnail['url'])) {
+ $attributeThumbnail['url'] = '';
+ }
+
+ $attributeEnclosures = [];
+ if (!empty($item->get_enclosures())) {
foreach ($item->get_enclosures() as $enclosure) {
$elink = $enclosure->get_link();
- if ($elink != '' && empty($elinks[$elink])) {
- $content .= '<div class="enclosure">';
-
- if ($enclosure->get_title() != '') {
- $content .= '<p class="enclosure-title">' . $enclosure->get_title() . '</p>';
- }
-
- $enclosureContent = '';
- $elinks[$elink] = true;
+ if ($elink != '') {
+ $etitle = $enclosure->get_title() ?? '';
+ $credit = $enclosure->get_credit() ?? null;
+ $description = $enclosure->get_description() ?? '';
$mime = strtolower($enclosure->get_type() ?? '');
$medium = strtolower($enclosure->get_medium() ?? '');
$height = $enclosure->get_height();
$width = $enclosure->get_width();
$length = $enclosure->get_length();
- if ($medium === 'image' || strpos($mime, 'image') === 0 ||
- ($mime == '' && $length == null && ($width != 0 || $height != 0 || preg_match('/[.](avif|gif|jpe?g|png|svg|webp)$/i', $elink)))) {
- $enclosureContent .= '<p class="enclosure-content"><img src="' . $elink . '" alt="" /></p>';
- } elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) {
- $enclosureContent .= '<p class="enclosure-content"><audio preload="none" src="' . $elink
- . ($length == null ? '' : '" data-length="' . intval($length))
- . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
- . '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
- } elseif ($medium === 'video' || strpos($mime, 'video') === 0) {
- $enclosureContent .= '<p class="enclosure-content"><video preload="none" src="' . $elink
- . ($length == null ? '' : '" data-length="' . intval($length))
- . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
- . '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
- } else { //e.g. application, text, unknown
- $enclosureContent .= '<p class="enclosure-content"><a download="" href="' . $elink
- . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
- . ($medium == '' ? '' : '" data-medium="' . htmlspecialchars($medium, ENT_COMPAT, 'UTF-8'))
- . '">💾</a></p>';
- }
- $thumbnailContent = '';
- if ($enclosure->get_thumbnails() != null) {
+ $attributeEnclosure = [
+ 'url' => $elink,
+ ];
+ if ($etitle != '') $attributeEnclosure['title'] = $etitle;
+ if ($credit != null) $attributeEnclosure['credit'] = $credit->get_name();
+ if ($description != '') $attributeEnclosure['description'] = $description;
+ if ($mime != '') $attributeEnclosure['type'] = $mime;
+ if ($medium != '') $attributeEnclosure['medium'] = $medium;
+ if ($length != '') $attributeEnclosure['length'] = intval($length);
+ if ($height != '') $attributeEnclosure['height'] = intval($height);
+ if ($width != '') $attributeEnclosure['width'] = intval($width);
+
+ if (!empty($enclosure->get_thumbnails())) {
foreach ($enclosure->get_thumbnails() as $thumbnail) {
- if (empty($elinks[$thumbnail])) {
- $elinks[$thumbnail] = true;
- $thumbnailContent .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" /></p>';
+ if ($thumbnail !== $attributeThumbnail['url']) {
+ $attributeEnclosure['thumbnails'][] = $thumbnail;
}
}
}
- $content .= $thumbnailContent;
- $content .= $enclosureContent;
-
- if ($enclosure->get_description() != '') {
- $content .= '<p class="enclosure-description">' . $enclosure->get_description() . '</p>';
- }
- $content .= "</div>\n";
+ $attributeEnclosures[] = $attributeEnclosure;
}
}
}
@@ -586,6 +577,10 @@ class FreshRSS_Feed extends Minz_Model {
);
$entry->_tags($tags);
$entry->_feed($this);
+ if (!empty($attributeThumbnail['url'])) {
+ $entry->_attributes('thumbnail', $attributeThumbnail);
+ }
+ $entry->_attributes('enclosures', $attributeEnclosures);
$entry->hash(); //Must be computed before loading full content
$entry->loadCompleteContent(); // Optionally load full content for truncated feeds
@@ -596,7 +591,7 @@ class FreshRSS_Feed extends Minz_Model {
/**
* @return SimplePie|null
*/
- public function loadHtmlXpath(bool $loadDetails = false, bool $noCache = false) {
+ public function loadHtmlXpath() {
if ($this->url == '') {
return null;
}
@@ -624,8 +619,9 @@ class FreshRSS_Feed extends Minz_Model {
return null;
}
- $cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $this->attributes(), FreshRSS_Feed::KIND_HTML_XPATH);
- $html = httpGet($feedSourceUrl, $cachePath, 'html', $this->attributes());
+ $cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $this->attributes(), $this->kind());
+ $html = httpGet($feedSourceUrl, $cachePath,
+ $this->kind() === FreshRSS_Feed::KIND_XML_XPATH ? 'xml' : 'html', $this->attributes());
if (strlen($html) <= 0) {
return null;
}
@@ -640,7 +636,18 @@ class FreshRSS_Feed extends Minz_Model {
$doc = new DOMDocument();
$doc->recover = true;
$doc->strictErrorChecking = false;
- $doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
+
+ switch ($this->kind()) {
+ case FreshRSS_Feed::KIND_HTML_XPATH:
+ $doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
+ break;
+ case FreshRSS_Feed::KIND_XML_XPATH:
+ $doc->loadXML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
+ break;
+ default:
+ return null;
+ }
+
$xpath = new DOMXPath($doc);
$view->rss_title = $xPathFeedTitle == '' ? $this->name() :
htmlspecialchars(@$xpath->evaluate('normalize-space(' . $xPathFeedTitle . ')'), ENT_COMPAT, 'UTF-8');
@@ -653,7 +660,23 @@ class FreshRSS_Feed extends Minz_Model {
foreach ($nodes as $node) {
$item = [];
$item['title'] = $xPathItemTitle == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTitle . ')', $node);
- $item['content'] = $xPathItemContent == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemContent . ')', $node);
+
+ $item['content'] = '';
+ if ($xPathItemContent != '') {
+ $result = @$xpath->evaluate($xPathItemContent, $node);
+ if ($result instanceof DOMNodeList) {
+ // List of nodes, save as HTML
+ $content = '';
+ foreach ($result as $child) {
+ $content .= $doc->saveHTML($child) . "\n";
+ }
+ $item['content'] = $content;
+ } else {
+ // Typed expression, save as-is
+ $item['content'] = strval($result);
+ }
+ }
+
$item['link'] = $xPathItemUri == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemUri . ')', $node);
$item['author'] = $xPathItemAuthor == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemAuthor . ')', $node);
$item['timestamp'] = $xPathItemTimestamp == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTimestamp . ')', $node);
@@ -679,8 +702,15 @@ class FreshRSS_Feed extends Minz_Model {
$item['guid'] = 'urn:sha1:' . sha1($item['title'] . $item['content'] . $item['link']);
}
- if ($item['title'] . $item['content'] . $item['link'] != '') {
- $item = Minz_Helper::htmlspecialchars_utf8($item);
+ if ($item['title'] != '' || $item['content'] != '' || $item['link'] != '') {
+ // HTML-encoding/escaping of the relevant fields (all except 'content')
+ foreach (['author', 'categories', 'guid', 'link', 'thumbnail', 'timestamp', 'title'] as $key) {
+ if (!empty($item[$key])) {
+ $item[$key] = Minz_Helper::htmlspecialchars_utf8($item[$key]);
+ }
+ }
+ // CDATA protection
+ $item['content'] = str_replace(']]>', ']]&gt;', $item['content']);
$view->entries[] = FreshRSS_Entry::fromArray($item);
}
}
@@ -763,8 +793,10 @@ class FreshRSS_Feed extends Minz_Model {
public static function cacheFilename(string $url, array $attributes, int $kind = FreshRSS_Feed::KIND_RSS): string {
$simplePie = customSimplePie($attributes);
$filename = $simplePie->get_cache_filename($url);
- if ($kind == FreshRSS_Feed::KIND_HTML_XPATH) {
+ if ($kind === FreshRSS_Feed::KIND_HTML_XPATH) {
return CACHE_PATH . '/' . $filename . '.html';
+ } elseif ($kind === FreshRSS_Feed::KIND_XML_XPATH) {
+ return CACHE_PATH . '/' . $filename . '.xml';
} else {
return CACHE_PATH . '/' . $filename . '.spc';
}
@@ -966,14 +998,14 @@ class FreshRSS_Feed extends Minz_Model {
$key = $hubJson['key']; //To renew our lease
}
} else {
- @mkdir($path, 0777, true);
+ @mkdir($path, 0770, true);
$key = sha1($path . FreshRSS_Context::$system_conf->salt);
$hubJson = array(
'hub' => $this->hubUrl,
'key' => $key,
);
file_put_contents($hubFilename, json_encode($hubJson));
- @mkdir(PSHB_PATH . '/keys/');
+ @mkdir(PSHB_PATH . '/keys/', 0770, true);
file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', $this->selfUrl);
$text = 'WebSub prepared for ' . $this->url;
Minz_Log::debug($text);
diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php
index 5993f50dc..1aae5fee5 100644
--- a/app/Models/FeedDAO.php
+++ b/app/Models/FeedDAO.php
@@ -49,11 +49,11 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
$values = array(
- substr($valuesTmp['url'], 0, 511),
+ $valuesTmp['url'],
$valuesTmp['kind'] ?? FreshRSS_Feed::KIND_RSS,
$valuesTmp['category'],
mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'),
- substr($valuesTmp['website'], 0, 255),
+ $valuesTmp['website'],
sanitizeHTML($valuesTmp['description'], '', 1023),
$valuesTmp['lastUpdate'],
isset($valuesTmp['priority']) ? intval($valuesTmp['priority']) : FreshRSS_Feed::PRIORITY_MAIN_STREAM,
@@ -434,7 +434,7 @@ SQL;
. '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `_entry` e2 WHERE e2.id_feed=`_feed`.id AND e2.is_read=0)'
. ($id != 0 ? ' WHERE id=:id' : '');
$stm = $this->pdo->prepare($sql);
- if ($id != 0) {
+ if ($stm && $id != 0) {
$stm->bindParam(':id', $id, PDO::PARAM_INT);
}
diff --git a/app/Models/Searchable.php b/app/Models/Searchable.php
index d5bcea49d..a15a44ed7 100644
--- a/app/Models/Searchable.php
+++ b/app/Models/Searchable.php
@@ -2,5 +2,9 @@
interface FreshRSS_Searchable {
+ /**
+ * @param int|string $id
+ * @return Minz_Model
+ */
public function searchById($id);
}
diff --git a/app/Models/SystemConfiguration.php b/app/Models/SystemConfiguration.php
index ec5960c0e..9fc79969d 100644
--- a/app/Models/SystemConfiguration.php
+++ b/app/Models/SystemConfiguration.php
@@ -25,6 +25,10 @@
* @property string $unsafe_autologin_enabled
* @property-read array<string> $trusted_sources
*/
-class FreshRSS_SystemConfiguration extends Minz_Configuration {
+final class FreshRSS_SystemConfiguration extends Minz_Configuration {
+ public static function init($config_filename, $default_filename = null): FreshRSS_SystemConfiguration {
+ parent::register('system', $config_filename, $default_filename);
+ return parent::get('system');
+ }
}
diff --git a/app/Models/Tag.php b/app/Models/Tag.php
index 589648e26..c1290d192 100644
--- a/app/Models/Tag.php
+++ b/app/Models/Tag.php
@@ -5,40 +5,61 @@ class FreshRSS_Tag extends Minz_Model {
* @var int
*/
private $id = 0;
+ /**
+ * @var string
+ */
private $name;
+ /**
+ * @var array<string,mixed>
+ */
private $attributes = [];
+ /**
+ * @var int
+ */
private $nbEntries = -1;
+ /**
+ * @var int
+ */
private $nbUnread = -1;
- public function __construct($name = '') {
+ public function __construct(string $name = '') {
$this->_name($name);
}
- public function id() {
+ public function id(): int {
return $this->id;
}
- public function _id($value) {
+ /**
+ * @param int|string $value
+ */
+ public function _id($value): void {
$this->id = (int)$value;
}
- public function name() {
+ public function name(): string {
return $this->name;
}
- public function _name($value) {
+ public function _name(string $value): void {
$this->name = trim($value);
}
- public function attributes($key = '') {
+ /**
+ * @return mixed|string|array<string,mixed>|null
+ */
+ public function attributes(string $key = '') {
if ($key == '') {
return $this->attributes;
} else {
- return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
+ return $this->attributes[$key] ?? null;
}
}
- public function _attributes($key, $value) {
+ /**
+ * @param mixed|string|array<string,mixed>|null $value
+ */
+ public function _attributes(string $key, $value = null): void {
if ($key == '') {
if (is_string($value)) {
$value = json_decode($value, true);
@@ -53,27 +74,33 @@ class FreshRSS_Tag extends Minz_Model {
}
}
- public function nbEntries() {
+ public function nbEntries(): int {
if ($this->nbEntries < 0) {
$tagDAO = FreshRSS_Factory::createTagDao();
- $this->nbEntries = $tagDAO->countEntries($this->id());
+ $this->nbEntries = $tagDAO->countEntries($this->id()) ?: 0;
}
return $this->nbEntries;
}
- public function _nbEntries($value) {
+ /**
+ * @param string|int $value
+ */
+ public function _nbEntries($value): void {
$this->nbEntries = (int)$value;
}
- public function nbUnread() {
+ public function nbUnread(): int {
if ($this->nbUnread < 0) {
$tagDAO = FreshRSS_Factory::createTagDao();
- $this->nbUnread = $tagDAO->countNotRead($this->id());
+ $this->nbUnread = $tagDAO->countNotRead($this->id()) ?: 0;
}
return $this->nbUnread;
}
- public function _nbUnread($value) {
+ /**
+ * @param string|int$value
+ */
+ public function _nbUnread($value): void {
$this->nbUnread = (int)$value;
}
}
diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php
index f232b2f9f..35123606b 100644
--- a/app/Models/TagDAO.php
+++ b/app/Models/TagDAO.php
@@ -267,12 +267,13 @@ SQL;
return $newestItemUsec;
}
+ /** @return int|false */
public function count() {
$sql = 'SELECT COUNT(*) AS count FROM `_tag`';
$stm = $this->pdo->query($sql);
if ($stm !== false) {
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
- return $res[0]['count'];
+ return (int)$res[0]['count'];
} else {
$info = $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
@@ -283,16 +284,27 @@ SQL;
}
}
- public function countEntries($id) {
+ /**
+ * @return int|false
+ */
+ public function countEntries(int $id) {
$sql = 'SELECT COUNT(*) AS count FROM `_entrytag` WHERE id_tag=?';
- $stm = $this->pdo->prepare($sql);
$values = array($id);
- $stm->execute($values);
- $res = $stm->fetchAll(PDO::FETCH_ASSOC);
- return $res[0]['count'];
+ if (($stm = $this->pdo->prepare($sql)) !== false &&
+ $stm->execute($values) &&
+ ($res = $stm->fetchAll(PDO::FETCH_ASSOC)) !== false) {
+ return (int)$res[0]['count'];
+ } else {
+ $info = is_object($stm) ? $stm->errorInfo() : $this->pdo->errorInfo();
+ Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
+ return false;
+ }
}
- public function countNotRead($id = null) {
+ /**
+ * @return int|false
+ */
+ public function countNotRead(?int $id = null) {
$sql = 'SELECT COUNT(*) AS count FROM `_entrytag` et '
. 'INNER JOIN `_entry` e ON et.id_entry=e.id '
. 'WHERE e.is_read=0';
@@ -303,11 +315,15 @@ SQL;
$values = [$id];
}
- $stm = $this->pdo->prepare($sql);
-
- $stm->execute($values);
- $res = $stm->fetchAll(PDO::FETCH_ASSOC);
- return $res[0]['count'];
+ if (($stm = $this->pdo->prepare($sql)) !== false &&
+ $stm->execute($values) &&
+ ($res = $stm->fetchAll(PDO::FETCH_ASSOC)) !== false) {
+ return (int)$res[0]['count'];
+ } else {
+ $info = is_object($stm) ? $stm->errorInfo() : $this->pdo->errorInfo();
+ Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
+ return false;
+ }
}
public function tagEntry($id_tag, $id_entry, $checked = true) {
diff --git a/app/Models/Themes.php b/app/Models/Themes.php
index d652ada5b..86125c5f5 100644
--- a/app/Models/Themes.php
+++ b/app/Models/Themes.php
@@ -79,7 +79,6 @@ class FreshRSS_Themes extends Minz_Model {
static $alts = array(
'add' => '➕', //✚
'all' => '☰',
- 'bookmark' => '✨', //★
'bookmark-add' => '➕', //✚
'bookmark-tag' => '📑',
'category' => '🗂️', //☷
diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php
index 05c3c08ac..53b12cc2e 100644
--- a/app/Models/UserConfiguration.php
+++ b/app/Models/UserConfiguration.php
@@ -28,6 +28,7 @@
* @property-read string $is_admin
* @property int|null $keep_history_default
* @property string $language
+ * @property string $timezone
* @property bool $lazyload
* @property string $mail_login
* @property bool $mark_updated_article_unread
@@ -52,6 +53,7 @@
* @property bool $sides_close_article
* @property bool $sticky_post
* @property string $theme
+ * @property string $darkMode
* @property string $token
* @property bool $topline_date
* @property bool $topline_display_authors
@@ -66,6 +68,10 @@
* @property string $view_mode
* @property array<string,mixed> $volatile
*/
-class FreshRSS_UserConfiguration extends Minz_Configuration {
+final class FreshRSS_UserConfiguration extends Minz_Configuration {
+ public static function init($config_filename, $default_filename = null, $configuration_setter = null): FreshRSS_UserConfiguration {
+ parent::register('user', $config_filename, $default_filename, $configuration_setter);
+ return parent::get('user');
+ }
}
diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php
index 964324bf7..278074362 100644
--- a/app/Models/UserQuery.php
+++ b/app/Models/UserQuery.php
@@ -8,26 +8,35 @@
*/
class FreshRSS_UserQuery {
+ /** @var bool */
private $deprecated = false;
- private $get;
- private $get_name;
- private $get_type;
- private $name;
- private $order;
+ /** @var string */
+ private $get = '';
+ /** @var string */
+ private $get_name = '';
+ /** @var string */
+ private $get_type = '';
+ /** @var string */
+ private $name = '';
+ /** @var string */
+ private $order = '';
/** @var FreshRSS_BooleanSearch */
private $search;
- private $state;
- private $url;
+ /** @var int */
+ private $state = 0;
+ /** @var string */
+ private $url = '';
+ /** @var FreshRSS_FeedDAO|null */
private $feed_dao;
+ /** @var FreshRSS_CategoryDAO|null */
private $category_dao;
+ /** @var FreshRSS_TagDAO|null */
private $tag_dao;
/**
* @param array<string,string> $query
- * @param FreshRSS_Searchable $feed_dao
- * @param FreshRSS_Searchable $category_dao
*/
- public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null, FreshRSS_Searchable $tag_dao = null) {
+ public function __construct(array $query, FreshRSS_FeedDAO $feed_dao = null, FreshRSS_CategoryDAO $category_dao = null, FreshRSS_TagDAO $tag_dao = null) {
$this->category_dao = $category_dao;
$this->feed_dao = $feed_dao;
$this->tag_dao = $tag_dao;
@@ -53,17 +62,17 @@ class FreshRSS_UserQuery {
}
// linked too deeply with the search object, need to use dependency injection
$this->search = new FreshRSS_BooleanSearch($query['search']);
- if (isset($query['state'])) {
- $this->state = $query['state'];
+ if (!empty($query['state'])) {
+ $this->state = intval($query['state']);
}
}
/**
* Convert the current object to an array.
*
- * @return array<string,string>
+ * @return array<string,string|int>
*/
- public function toArray() {
+ public function toArray(): array {
return array_filter(array(
'get' => $this->get,
'name' => $this->name,
@@ -75,29 +84,27 @@ class FreshRSS_UserQuery {
}
/**
- * Parse the get parameter in the query string to extract its name and
- * type
- *
- * @param string $get
+ * Parse the get parameter in the query string to extract its name and type
*/
- private function parseGet($get) {
+ private function parseGet(string $get): void {
$this->get = $get;
if (preg_match('/(?P<type>[acfst])(_(?P<id>\d+))?/', $get, $matches)) {
+ $id = intval($matches['id'] ?? '0');
switch ($matches['type']) {
case 'a':
$this->parseAll();
break;
case 'c':
- $this->parseCategory($matches['id']);
+ $this->parseCategory($id);
break;
case 'f':
- $this->parseFeed($matches['id']);
+ $this->parseFeed($id);
break;
case 's':
$this->parseFavorite();
break;
case 't':
- $this->parseTag($matches['id']);
+ $this->parseTag($id);
break;
}
}
@@ -106,7 +113,7 @@ class FreshRSS_UserQuery {
/**
* Parse the query string when it is an "all" query
*/
- private function parseAll() {
+ private function parseAll(): void {
$this->get_name = 'all';
$this->get_type = 'all';
}
@@ -114,11 +121,10 @@ class FreshRSS_UserQuery {
/**
* Parse the query string when it is a "category" query
*
- * @param integer $id
* @throws FreshRSS_DAO_Exception
*/
- private function parseCategory($id) {
- if (is_null($this->category_dao)) {
+ private function parseCategory(int $id): void {
+ if ($this->category_dao === null) {
throw new FreshRSS_DAO_Exception('Category DAO is not loaded in UserQuery');
}
$category = $this->category_dao->searchById($id);
@@ -133,11 +139,10 @@ class FreshRSS_UserQuery {
/**
* Parse the query string when it is a "feed" query
*
- * @param integer $id
* @throws FreshRSS_DAO_Exception
*/
- private function parseFeed($id) {
- if (is_null($this->feed_dao)) {
+ private function parseFeed(int $id): void {
+ if ($this->feed_dao === null) {
throw new FreshRSS_DAO_Exception('Feed DAO is not loaded in UserQuery');
}
$feed = $this->feed_dao->searchById($id);
@@ -152,10 +157,9 @@ class FreshRSS_UserQuery {
/**
* Parse the query string when it is a "tag" query
*
- * @param integer $id
* @throws FreshRSS_DAO_Exception
*/
- private function parseTag($id) {
+ private function parseTag(int $id): void {
if ($this->tag_dao == null) {
throw new FreshRSS_DAO_Exception('Tag DAO is not loaded in UserQuery');
}
@@ -171,7 +175,7 @@ class FreshRSS_UserQuery {
/**
* Parse the query string when it is a "favorite" query
*/
- private function parseFavorite() {
+ private function parseFavorite(): void {
$this->get_name = 'favorite';
$this->get_type = 'favorite';
}
@@ -180,20 +184,16 @@ class FreshRSS_UserQuery {
* Check if the current user query is deprecated.
* It is deprecated if the category or the feed used in the query are
* not existing.
- *
- * @return boolean
*/
- public function isDeprecated() {
+ public function isDeprecated(): bool {
return $this->deprecated;
}
/**
* Check if the user query has parameters.
* If the type is 'all', it is considered equal to no parameters
- *
- * @return boolean
*/
- public function hasParameters() {
+ public function hasParameters(): bool {
if ($this->get_type === 'all') {
return false;
}
@@ -214,42 +214,40 @@ class FreshRSS_UserQuery {
/**
* Check if there is a search in the search object
- *
- * @return boolean
*/
- public function hasSearch() {
- return $this->search->getRawInput() != "";
+ public function hasSearch(): bool {
+ return $this->search->getRawInput() !== '';
}
- public function getGet() {
+ public function getGet(): string {
return $this->get;
}
- public function getGetName() {
+ public function getGetName(): string {
return $this->get_name;
}
- public function getGetType() {
+ public function getGetType(): string {
return $this->get_type;
}
- public function getName() {
+ public function getName(): string {
return $this->name;
}
- public function getOrder() {
+ public function getOrder(): string {
return $this->order;
}
- public function getSearch() {
+ public function getSearch(): FreshRSS_BooleanSearch {
return $this->search;
}
- public function getState() {
+ public function getState(): int {
return $this->state;
}
- public function getUrl() {
+ public function getUrl(): string {
return $this->url;
}
diff --git a/app/Models/View.php b/app/Models/View.php
index ab1780405..309773c93 100644
--- a/app/Models/View.php
+++ b/app/Models/View.php
@@ -39,6 +39,7 @@ class FreshRSS_View extends Minz_View {
public $details;
public $disable_aside;
public $show_email_field;
+ /** @var string */
public $username;
public $users;
diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php
index d85bd3dc3..b8fff170a 100644
--- a/app/SQL/install.sql.mysql.php
+++ b/app/SQL/install.sql.mysql.php
@@ -18,11 +18,11 @@ ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `_feed` (
`id` INT NOT NULL AUTO_INCREMENT, -- v0.7
- `url` VARCHAR(511) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
+ `url` VARCHAR(32768) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`kind` SMALLINT DEFAULT 0, -- 1.20.0
`category` INT DEFAULT 0, -- 1.20.0
`name` VARCHAR(191) NOT NULL,
- `website` VARCHAR(255) CHARACTER SET latin1 COLLATE latin1_bin,
+ `website` TEXT CHARACTER SET latin1 COLLATE latin1_bin,
`description` TEXT,
`lastUpdate` INT(11) DEFAULT 0, -- Until year 2038
`priority` TINYINT(2) NOT NULL DEFAULT 10,
@@ -35,7 +35,6 @@ CREATE TABLE IF NOT EXISTS `_feed` (
`cache_nbUnreads` INT DEFAULT 0, -- v0.7
PRIMARY KEY (`id`),
FOREIGN KEY (`category`) REFERENCES `_category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
- UNIQUE KEY (`url`), -- v0.7
INDEX (`name`), -- v0.7
INDEX (`priority`) -- v0.7
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
diff --git a/app/SQL/install.sql.pgsql.php b/app/SQL/install.sql.pgsql.php
index c4da2afad..00a30a8c7 100644
--- a/app/SQL/install.sql.pgsql.php
+++ b/app/SQL/install.sql.pgsql.php
@@ -15,11 +15,11 @@ CREATE TABLE IF NOT EXISTS `_category` (
CREATE TABLE IF NOT EXISTS `_feed` (
"id" SERIAL PRIMARY KEY,
- "url" VARCHAR(511) UNIQUE NOT NULL,
+ "url" VARCHAR(32768) NOT NULL,
"kind" SMALLINT DEFAULT 0, -- 1.20.0
"category" INT DEFAULT 0, -- 1.20.0
"name" VARCHAR(255) NOT NULL,
- "website" VARCHAR(255),
+ "website" VARCHAR(32768),
"description" TEXT,
"lastUpdate" INT DEFAULT 0,
"priority" SMALLINT NOT NULL DEFAULT 10,
diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php
index ccf256d6a..8762b33eb 100644
--- a/app/SQL/install.sql.sqlite.php
+++ b/app/SQL/install.sql.sqlite.php
@@ -16,11 +16,11 @@ CREATE TABLE IF NOT EXISTS `category` (
CREATE TABLE IF NOT EXISTS `feed` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
- `url` VARCHAR(511) NOT NULL,
+ `url` VARCHAR(32768) NOT NULL,
`kind` SMALLINT DEFAULT 0, -- 1.20.0
`category` INTEGER DEFAULT 0, -- 1.20.0
`name` VARCHAR(255) NOT NULL,
- `website` VARCHAR(255),
+ `website` VARCHAR(32768),
`description` TEXT,
`lastUpdate` INT(11) DEFAULT 0, -- Until year 2038
`priority` TINYINT(2) NOT NULL DEFAULT 10,
@@ -31,8 +31,7 @@ CREATE TABLE IF NOT EXISTS `feed` (
`attributes` TEXT, -- v1.11.0
`cache_nbEntries` INT DEFAULT 0,
`cache_nbUnreads` INT DEFAULT 0,
- FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
- UNIQUE (`url`)
+ FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE INDEX IF NOT EXISTS feed_name_index ON `feed`(`name`);
CREATE INDEX IF NOT EXISTS feed_priority_index ON `feed`(`priority`);
diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php
index ad0f5f5a8..6b0a3f178 100644
--- a/app/Services/ExportService.php
+++ b/app/Services/ExportService.php
@@ -21,6 +21,7 @@ class FreshRSS_Export_Service {
const FRSS_NAMESPACE = 'https://freshrss.org/opml';
const TYPE_HTML_XPATH = 'HTML+XPath';
+ const TYPE_XML_XPATH = 'XML+XPath';
const TYPE_RSS_ATOM = 'rss';
/**
@@ -43,8 +44,6 @@ class FreshRSS_Export_Service {
* @return array First item is the filename, second item is the content
*/
public function generateOpml() {
- require_once(LIB_PATH . '/lib_opml.php');
-
$view = new FreshRSS_View();
$day = date('Y-m-d');
$view->categories = $this->category_dao->listCategories(true, true);
diff --git a/app/Services/ImportService.php b/app/Services/ImportService.php
index 28286a753..55aa28679 100644
--- a/app/Services/ImportService.php
+++ b/app/Services/ImportService.php
@@ -19,8 +19,6 @@ class FreshRSS_Import_Service {
* @param string $username
*/
public function __construct($username = null) {
- require_once(LIB_PATH . '/lib_opml.php');
-
$this->catDAO = FreshRSS_Factory::createCategoryDao($username);
$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
}
@@ -34,153 +32,194 @@ class FreshRSS_Import_Service {
* This method parses and imports an OPML file.
*
* @param string $opml_file the OPML file content.
- * @param FreshRSS_Category|null $parent_cat the name of the parent category.
- * @param boolean $flatten true to disable categories, false otherwise.
- * @return array<FreshRSS_Category>|false an array of categories containing some feeds, or false if an error occurred.
+ * @param FreshRSS_Category|null $forced_category force the feeds to be associated to this category.
+ * @param boolean $dry_run true to not create categories and feeds in database.
*/
- public function importOpml(string $opml_file, $parent_cat = null, $flatten = false, $dryRun = false) {
+ public function importOpml(string $opml_file, $forced_category = null, $dry_run = false) {
$this->lastStatus = true;
$opml_array = array();
try {
- $opml_array = libopml_parse_string($opml_file, false);
- } catch (LibOPML_Exception $e) {
- if (FreshRSS_Context::$isCli) {
- fwrite(STDERR, 'FreshRSS error during OPML parsing: ' . $e->getMessage() . "\n");
- } else {
- Minz_Log::warning($e->getMessage());
- }
+ $libopml = new \marienfressinaud\LibOpml\LibOpml(false);
+ $opml_array = $libopml->parseString($opml_file);
+ } catch (\marienfressinaud\LibOpml\Exception $e) {
+ self::log($e->getMessage());
$this->lastStatus = false;
- return false;
+ return;
}
- return $this->addOpmlElements($opml_array['body'], $parent_cat, $flatten, $dryRun);
- }
+ $this->catDAO->checkDefault();
+ $default_category = $this->catDAO->getDefault();
+ if (!$default_category) {
+ self::log('Cannot get the default category');
+ $this->lastStatus = false;
+ return;
+ }
- /**
- * This method imports an OPML file based on its body.
- *
- * @param array $opml_elements an OPML element (body or outline).
- * @param FreshRSS_Category|null $parent_cat the name of the parent category.
- * @param boolean $flatten true to disable categories, false otherwise.
- * @return array<FreshRSS_Category> an array of categories containing some feeds
- */
- private function addOpmlElements($opml_elements, $parent_cat = null, $flatten = false, $dryRun = false) {
+ // Get the categories by names so we can use this array to retrieve
+ // existing categories later.
+ $categories = $this->catDAO->listCategories(false);
+ $categories_by_names = [];
+ foreach ($categories as $category) {
+ $categories_by_names[$category->name()] = $category;
+ }
+
+ // Get current numbers of categories and feeds, and the limits to
+ // verify the user can import its categories/feeds.
+ $nb_categories = count($categories);
$nb_feeds = count($this->feedDAO->listFeeds());
- $nb_cats = count($this->catDAO->listCategories(false));
$limits = FreshRSS_Context::$system_conf->limits;
- //Sort with categories first
- usort($opml_elements, static function ($a, $b) {
- return strcmp(
- (isset($a['xmlUrl']) ? 'Z' : 'A') . (isset($a['text']) ? $a['text'] : ''),
- (isset($b['xmlUrl']) ? 'Z' : 'A') . (isset($b['text']) ? $b['text'] : ''));
- });
-
- $categories = [];
-
- foreach ($opml_elements as $elt) {
- if (isset($elt['xmlUrl'])) {
- // If xmlUrl exists, it means it is a feed
- if (FreshRSS_Context::$isCli && $nb_feeds >= $limits['max_feeds']) {
- Minz_Log::warning(_t('feedback.sub.feed.over_max',
- $limits['max_feeds']));
- $this->lastStatus = false;
- continue;
- }
+ // Process the OPML outlines to get a list of categories and a list of
+ // feeds elements indexed by their categories names.
+ list (
+ $categories_elements,
+ $categories_to_feeds,
+ ) = $this->loadFromOutlines($opml_array['body'], '');
- if ($this->addFeedOpml($elt, $parent_cat, $dryRun)) {
- $nb_feeds++;
+ foreach ($categories_to_feeds as $category_name => $feeds_elements) {
+ $category_element = $categories_elements[$category_name] ?? null;
+
+ $category = null;
+ if ($forced_category) {
+ // If the category is forced, ignore the actual category name
+ $category = $forced_category;
+ } elseif (isset($categories_by_names[$category_name])) {
+ // If the category already exists, get it from $categories_by_names
+ $category = $categories_by_names[$category_name];
+ } elseif ($category_element) {
+ // Otherwise, create the category (if possible)
+ $limit_reached = $nb_categories >= $limits['max_categories'];
+ $can_create_category = FreshRSS_Context::$isCli || !$limit_reached;
+
+ if ($can_create_category) {
+ $category = $this->createCategory($category_element, $dry_run);
+ if ($category) {
+ $categories_by_names[$category->name()] = $category;
+ $nb_categories++;
+ }
} else {
- $this->lastStatus = false;
+ Minz_Log::warning(
+ _t('feedback.sub.category.over_max', $limits['max_categories'])
+ );
}
- } elseif (!empty($elt['text'])) {
- // No xmlUrl? It should be a category!
- $limit_reached = !$flatten && ($nb_cats >= $limits['max_categories']);
- if (!FreshRSS_Context::$isCli && $limit_reached) {
- Minz_Log::warning(_t('feedback.sub.category.over_max',
- $limits['max_categories']));
+ }
+
+ if (!$category) {
+ // Category can be null if the feeds weren't in a category
+ // outline, or if we weren't able to create the category.
+ $category = $default_category;
+ }
+
+ // Then, create the feeds one by one and attach them to the
+ // category we just got.
+ foreach ($feeds_elements as $feed_element) {
+ $limit_reached = $nb_feeds >= $limits['max_feeds'];
+ $can_create_feed = FreshRSS_Context::$isCli || !$limit_reached;
+ if (!$can_create_feed) {
+ Minz_Log::warning(
+ _t('feedback.sub.feed.over_max', $limits['max_feeds'])
+ );
$this->lastStatus = false;
- $flatten = true;
+ break;
}
- $category = $this->addCategoryOpml($elt, $parent_cat, $flatten, $dryRun);
-
- if ($category) {
- $nb_cats++;
- $categories[] = $category;
+ if ($this->createFeed($feed_element, $category, $dry_run)) {
+ // TODO what if the feed already exists in the database?
+ $nb_feeds++;
+ } else {
+ $this->lastStatus = false;
}
}
}
- return $categories;
+ return;
}
/**
- * This method imports an OPML feed element.
+ * Create a feed from a feed element (i.e. OPML outline).
*
- * @param array $feed_elt an OPML element (must be a feed element).
- * @param FreshRSS_Category|null $parent_cat the name of the parent category.
- * @return FreshRSS_Feed|null a feed.
+ * @param array<string, string> $feed_elt An OPML element (must be a feed element).
+ * @param FreshRSS_Category $category The category to associate to the feed.
+ * @param boolean $dry_run true to not create the feed in database.
+ *
+ * @return FreshRSS_Feed|null The created feed, or null if it failed.
*/
- private function addFeedOpml($feed_elt, $parent_cat, $dryRun = false) {
- if (empty($feed_elt['xmlUrl'])) {
- return null;
- }
- if ($parent_cat == null) {
- // This feed has no parent category so we get the default one
- $this->catDAO->checkDefault();
- $parent_cat = $this->catDAO->getDefault();
- if ($parent_cat == null) {
- $this->lastStatus = false;
- return null;
- }
- }
-
- // We get different useful information
+ private function createFeed($feed_elt, $category, $dry_run) {
$url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
- $name = Minz_Helper::htmlspecialchars_utf8($feed_elt['text'] ?? '');
+ $name = $feed_elt['text'] ?? $feed_elt['title'] ?? '';
+ $name = Minz_Helper::htmlspecialchars_utf8($name);
$website = Minz_Helper::htmlspecialchars_utf8($feed_elt['htmlUrl'] ?? '');
$description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description'] ?? '');
try {
// Create a Feed object and add it in DB
$feed = new FreshRSS_Feed($url);
- $feed->_categoryId($parent_cat->id());
- $parent_cat->addFeed($feed);
+ $feed->_categoryId($category->id());
+ $category->addFeed($feed);
$feed->_name($name);
$feed->_website($website);
$feed->_description($description);
- switch ($feed_elt['type'] ?? '') {
- case FreshRSS_Export_Service::TYPE_HTML_XPATH:
+ switch (strtolower($feed_elt['type'] ?? '')) {
+ case strtolower(FreshRSS_Export_Service::TYPE_HTML_XPATH):
$feed->_kind(FreshRSS_Feed::KIND_HTML_XPATH);
break;
- case FreshRSS_Export_Service::TYPE_RSS_ATOM:
+ case strtolower(FreshRSS_Export_Service::TYPE_XML_XPATH):
+ $feed->_kind(FreshRSS_Feed::KIND_XML_XPATH);
+ break;
+ case strtolower(FreshRSS_Export_Service::TYPE_RSS_ATOM):
default:
$feed->_kind(FreshRSS_Feed::KIND_RSS);
break;
}
+ if (isset($feed_elt['frss:cssFullContent'])) {
+ $feed->_pathEntries(Minz_Helper::htmlspecialchars_utf8($feed_elt['frss:cssFullContent']));
+ }
+
+ if (isset($feed_elt['frss:cssFullContentFilter'])) {
+ $feed->_attributes('path_entries_filter', $feed_elt['frss:cssFullContentFilter']);
+ }
+
+ if (isset($feed_elt['frss:filtersActionRead'])) {
+ $feed->_filtersAction(
+ 'read',
+ preg_split('/[\n\r]+/', $feed_elt['frss:filtersActionRead'])
+ );
+ }
+
$xPathSettings = [];
- foreach ($feed_elt as $key => $value) {
- if (is_array($value) && !empty($value['value']) && ($value['namespace'] ?? '') === FreshRSS_Export_Service::FRSS_NAMESPACE) {
- switch ($key) {
- case 'cssFullContent': $feed->_pathEntries(Minz_Helper::htmlspecialchars_utf8($value['value'])); break;
- case 'cssFullContentFilter': $feed->_attributes('path_entries_filter', $value['value']); break;
- case 'filtersActionRead': $feed->_filtersAction('read', preg_split('/[\n\r]+/', $value['value'])); break;
- case 'xPathItem': $xPathSettings['item'] = $value['value']; break;
- case 'xPathItemTitle': $xPathSettings['itemTitle'] = $value['value']; break;
- case 'xPathItemContent': $xPathSettings['itemContent'] = $value['value']; break;
- case 'xPathItemUri': $xPathSettings['itemUri'] = $value['value']; break;
- case 'xPathItemAuthor': $xPathSettings['itemAuthor'] = $value['value']; break;
- case 'xPathItemTimestamp': $xPathSettings['itemTimestamp'] = $value['value']; break;
- case 'xPathItemTimeFormat': $xPathSettings['itemTimeFormat'] = $value['value']; break;
- case 'xPathItemThumbnail': $xPathSettings['itemThumbnail'] = $value['value']; break;
- case 'xPathItemCategories': $xPathSettings['itemCategories'] = $value['value']; break;
- case 'xPathItemUid': $xPathSettings['itemUid'] = $value['value']; break;
- }
- }
+ if (isset($feed_elt['frss:xPathItem'])) {
+ $xPathSettings['item'] = $feed_elt['frss:xPathItem'];
+ }
+ if (isset($feed_elt['frss:xPathItemTitle'])) {
+ $xPathSettings['itemTitle'] = $feed_elt['frss:xPathItemTitle'];
+ }
+ if (isset($feed_elt['frss:xPathItemContent'])) {
+ $xPathSettings['itemContent'] = $feed_elt['frss:xPathItemContent'];
+ }
+ if (isset($feed_elt['frss:xPathItemUri'])) {
+ $xPathSettings['itemUri'] = $feed_elt['frss:xPathItemUri'];
+ }
+ if (isset($feed_elt['frss:xPathItemAuthor'])) {
+ $xPathSettings['itemAuthor'] = $feed_elt['frss:xPathItemAuthor'];
+ }
+ if (isset($feed_elt['frss:xPathItemTimestamp'])) {
+ $xPathSettings['itemTimestamp'] = $feed_elt['frss:xPathItemTimestamp'];
+ }
+ if (isset($feed_elt['frss:xPathItemTimeFormat'])) {
+ $xPathSettings['itemTimeFormat'] = $feed_elt['frss:xPathItemTimeFormat'];
}
+ if (isset($feed_elt['frss:xPathItemThumbnail'])) {
+ $xPathSettings['itemThumbnail'] = $feed_elt['frss:xPathItemThumbnail'];
+ }
+ if (isset($feed_elt['frss:xPathItemCategories'])) {
+ $xPathSettings['itemCategories'] = $feed_elt['frss:xPathItemCategories'];
+ }
+ if (isset($feed_elt['frss:xPathItemUid'])) {
+ $xPathSettings['itemUid'] = $feed_elt['frss:xPathItemUid'];
+ }
+
if (!empty($xPathSettings)) {
$feed->_attributes('xpath', $xPathSettings);
}
@@ -188,9 +227,11 @@ class FreshRSS_Import_Service {
// Call the extension hook
/** @var FreshRSS_Feed|null */
$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
- if ($dryRun) {
+
+ if ($dry_run) {
return $feed;
}
+
if ($feed != null) {
// addFeedObject checks if feed is already in DB
$id = $this->feedDAO->addFeedObject($feed);
@@ -202,81 +243,163 @@ class FreshRSS_Import_Service {
}
}
} catch (FreshRSS_Feed_Exception $e) {
- if (FreshRSS_Context::$isCli) {
- fwrite(STDERR, 'FreshRSS error during OPML feed import: ' . $e->getMessage() . "\n");
- } else {
- Minz_Log::warning($e->getMessage());
- }
+ self::log($e->getMessage());
$this->lastStatus = false;
}
- if (FreshRSS_Context::$isCli) {
- fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' .
- SimplePie_Misc::url_remove_credentials($url) . ' in category ' . $parent_cat->id() . "\n");
- } else {
- Minz_Log::warning('Error during OPML feed import from URL: ' .
- SimplePie_Misc::url_remove_credentials($url) . ' in category ' . $parent_cat->id());
- }
-
+ $clean_url = SimplePie_Misc::url_remove_credentials($url);
+ self::log("Cannot create {$clean_url} feed in category {$category->name()}");
return null;
}
/**
- * This method imports an OPML category element.
+ * Create and return a category.
+ *
+ * @param array<string, string> $category_element An OPML element (must be a category element).
+ * @param boolean $dry_run true to not create the category in database.
*
- * @param array $cat_elt an OPML element (must be a category element).
- * @param FreshRSS_Category|null $parent_cat the name of the parent category.
- * @param boolean $flatten true to disable categories, false otherwise.
- * @return FreshRSS_Category|null a new category containing some feeds, or null if no category was created, or false if an error occurred.
+ * @return FreshRSS_Category|null The created category, or null if it failed.
*/
- private function addCategoryOpml($cat_elt, $parent_cat, $flatten = false, $dryRun = false) {
- $error = false;
- $cat = null;
- if (!$flatten) {
- $catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']);
- $cat = new FreshRSS_Category($catName);
-
- foreach ($cat_elt as $key => $value) {
- if (is_array($value) && !empty($value['value']) && ($value['namespace'] ?? '') === FreshRSS_Export_Service::FRSS_NAMESPACE) {
- switch ($key) {
- case 'opmlUrl':
- $opml_url = checkUrl($value['value']);
- if ($opml_url != '') {
- $cat->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
- $cat->_attributes('opml_url', $opml_url);
- }
- break;
- }
- }
+ private function createCategory($category_element, $dry_run) {
+ $name = $category_element['text'] ?? $category_element['title'] ?? '';
+ $name = Minz_Helper::htmlspecialchars_utf8($name);
+ $category = new FreshRSS_Category($name);
+
+ if (isset($category_element['frss:opmlUrl'])) {
+ $opml_url = checkUrl($category_element['frss:opmlUrl']);
+ if ($opml_url != '') {
+ $category->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
+ $category->_attributes('opml_url', $opml_url);
}
+ }
- if (!$dryRun) {
- $id = $this->catDAO->addCategoryObject($cat);
- if ($id == false) {
- $this->lastStatus = false;
- $error = true;
- } else {
- $cat->_id($id);
+ if ($dry_run) {
+ return $category;
+ }
+
+ $id = $this->catDAO->addCategoryObject($category);
+ if ($id !== false) {
+ $category->_id($id);
+ return $category;
+ } else {
+ self::log("Cannot create category {$category->name()}");
+ $this->lastStatus = false;
+ return null;
+ }
+ }
+
+ /**
+ * Return the list of category and feed outlines by categories names.
+ *
+ * This method is applied to a list of outlines. It merges the different
+ * list of feeds from several outlines into one array.
+ *
+ * @param array $outlines
+ * The outlines from which to extract the outlines.
+ * @param string $parent_category_name
+ * The name of the parent category of the current outlines.
+ *
+ * @return array[]
+ */
+ private function loadFromOutlines($outlines, $parent_category_name) {
+ $categories_elements = [];
+ $categories_to_feeds = [];
+
+ foreach ($outlines as $outline) {
+ // Get the categories and feeds from the child outline (it may
+ // return several categories and feeds if the outline is a category).
+ list (
+ $outline_categories,
+ $outline_categories_to_feeds,
+ ) = $this->loadFromOutline($outline, $parent_category_name);
+
+ // Then, we merge the initial arrays with the arrays returned by
+ // the outline.
+ $categories_elements = array_merge($categories_elements, $outline_categories);
+
+ foreach ($outline_categories_to_feeds as $category_name => $feeds) {
+ if (!isset($categories_to_feeds[$category_name])) {
+ $categories_to_feeds[$category_name] = [];
}
+
+ $categories_to_feeds[$category_name] = array_merge(
+ $categories_to_feeds[$category_name],
+ $feeds
+ );
}
- if ($error) {
- if (FreshRSS_Context::$isCli) {
- fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n");
- } else {
- Minz_Log::warning('Error during OPML category import from URL: ' . $catName);
- }
+ }
+
+ return [$categories_elements, $categories_to_feeds];
+ }
+
+ /**
+ * Return the list of category and feed outlines by categories names.
+ *
+ * This method is applied to a specific outline. If the outline represents
+ * a category (i.e. @outlines key exists), it will reapply loadFromOutlines()
+ * to its children. If the outline represents a feed (i.e. xmlUrl key
+ * exists), it will add the outline to an array accessible by its category
+ * name.
+ *
+ * @param array $outline
+ * The outline from which to extract the categories and feeds outlines.
+ * @param string $parent_category_name
+ * The name of the parent category of the current outline.
+ *
+ * @return array[]
+ */
+ private function loadFromOutline($outline, $parent_category_name) {
+ $categories_elements = [];
+ $categories_to_feeds = [];
+
+ if ($parent_category_name === '' && isset($outline['category'])) {
+ // The outline has no parent category, but its OPML category
+ // attribute is set, so we use it as the category name.
+ // lib_opml parses this attribute as an array of strings, so we
+ // rebuild a string here.
+ $parent_category_name = implode(', ', $outline['category']);
+ $categories_elements[$parent_category_name] = [
+ 'text' => $parent_category_name,
+ ];
+ }
+
+ if (isset($outline['@outlines'])) {
+ // The outline has children, it's probably a category
+ if (!empty($outline['text'])) {
+ $category_name = $outline['text'];
+ } elseif (!empty($outline['title'])) {
+ $category_name = $outline['title'];
} else {
- $parent_cat = $cat;
+ $category_name = $parent_category_name;
}
+
+ list (
+ $categories_elements,
+ $categories_to_feeds,
+ ) = $this->loadFromOutlines($outline['@outlines'], $category_name);
+
+ unset($outline['@outlines']);
+ $categories_elements[$category_name] = $outline;
}
- if (isset($cat_elt['@outlines'])) {
- // Our cat_elt contains more categories or more feeds, so we
- // add them recursively.
- // Note: FreshRSS does not support yet category arborescence, so always flatten from here
- $this->addOpmlElements($cat_elt['@outlines'], $parent_cat, true, $dryRun);
+ // The xmlUrl means it's a feed URL: add the outline to the array if it
+ // exists.
+ if (isset($outline['xmlUrl'])) {
+ if (!isset($categories_to_feeds[$parent_category_name])) {
+ $categories_to_feeds[$parent_category_name] = [];
+ }
+
+ $categories_to_feeds[$parent_category_name][] = $outline;
}
- return $cat;
+ return [$categories_elements, $categories_to_feeds];
+ }
+
+ private static function log($message) {
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, "FreshRSS error during OPML import: {$message}\n");
+ } else {
+ Minz_Log::warning("Error during OPML import: {$message}");
+ }
}
}
diff --git a/app/Utils/feverUtil.php b/app/Utils/feverUtil.php
index a7d21dacb..0e4b712ce 100644
--- a/app/Utils/feverUtil.php
+++ b/app/Utils/feverUtil.php
@@ -1,19 +1,19 @@
<?php
class FreshRSS_fever_Util {
- const FEVER_PATH = DATA_PATH . '/fever';
+ private const FEVER_PATH = DATA_PATH . '/fever';
/**
* Make sure the fever path exists and is writable.
*
- * @return boolean true if the path is writable, else false.
+ * @return bool true if the path is writable, false otherwise.
*/
- public static function checkFeverPath() {
+ public static function checkFeverPath(): bool {
if (!file_exists(self::FEVER_PATH)) {
@mkdir(self::FEVER_PATH, 0770, true);
}
- $ok = is_writable(self::FEVER_PATH);
+ $ok = touch(self::FEVER_PATH . '/index.html'); // is_writable() is not reliable for a folder on NFS
if (!$ok) {
Minz_Log::error("Could not save Fever API credentials. The directory does not have write access.");
}
@@ -22,25 +22,21 @@ class FreshRSS_fever_Util {
/**
* Return the corresponding path for a fever key.
- *
- * @param string $feverKey
- * @return string
*/
- public static function getKeyPath($feverKey) {
+ public static function getKeyPath(string $feverKey): string {
+ if (FreshRSS_Context::$system_conf === null) {
+ throw new FreshRSS_Context_Exception('System configuration not initialised!');
+ }
$salt = sha1(FreshRSS_Context::$system_conf->salt);
return self::FEVER_PATH . '/.key-' . $salt . '-' . $feverKey . '.txt';
}
/**
* Update the fever key of a user.
- *
- * @param string $username
- * @param string $passwordPlain
* @return string|false the Fever key, or false if the update failed
*/
- public static function updateKey($username, $passwordPlain) {
- $ok = self::checkFeverPath();
- if (!$ok) {
+ public static function updateKey(string $username, string $passwordPlain) {
+ if (!self::checkFeverPath()) {
return false;
}
@@ -48,22 +44,20 @@ class FreshRSS_fever_Util {
$feverKey = strtolower(md5("{$username}:{$passwordPlain}"));
$feverKeyPath = self::getKeyPath($feverKey);
- $res = file_put_contents($feverKeyPath, $username);
- if ($res !== false) {
+ $result = file_put_contents($feverKeyPath, $username);
+ if (is_int($result) && $result > 0) {
return $feverKey;
- } else {
- Minz_Log::warning('Could not save Fever API credentials. Unknown error.', ADMIN_LOG);
- return false;
}
+ Minz_Log::warning('Could not save Fever API credentials. Unknown error.', ADMIN_LOG);
+ return false;
}
/**
* Delete the Fever key of a user.
*
- * @param string $username
- * @return boolean true if the deletion succeeded, else false.
+ * @return bool true if the deletion succeeded, else false.
*/
- public static function deleteKey($username) {
+ public static function deleteKey(string $username) {
$userConfig = get_user_configuration($username);
if ($userConfig === null) {
return false;
diff --git a/app/Utils/passwordUtil.php b/app/Utils/passwordUtil.php
index cff97d2bc..0edead213 100644
--- a/app/Utils/passwordUtil.php
+++ b/app/Utils/passwordUtil.php
@@ -3,26 +3,25 @@
class FreshRSS_password_Util {
// Will also have to be computed client side on mobile devices,
// so do not use a too high cost
- const BCRYPT_COST = 9;
+ public const BCRYPT_COST = 9;
/**
* Return a hash of a plain password, using BCRYPT
- *
- * @param string $passwordPlain
- * @return string
*/
- public static function hash($passwordPlain) {
+ public static function hash(string $passwordPlain): string {
$passwordHash = password_hash(
$passwordPlain,
PASSWORD_BCRYPT,
array('cost' => self::BCRYPT_COST)
);
- $passwordPlain = '';
// Compatibility with bcrypt.js
$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);
- return $passwordHash == '' ? '' : $passwordHash;
+ if ($passwordHash === '' || $passwordHash === null) {
+ return '';
+ }
+ return $passwordHash;
}
/**
@@ -30,11 +29,9 @@ class FreshRSS_password_Util {
*
* A valid password is a string of at least 7 characters.
*
- * @param string $password
- *
- * @return boolean True if the password is valid, false otherwise
+ * @return bool True if the password is valid, false otherwise
*/
- public static function check($password) {
+ public static function check(string $password): bool {
return strlen($password) >= 7;
}
}
diff --git a/app/i18n/cz/conf.php b/app/i18n/cz/conf.php
index 7b603555c..4411b5047 100644
--- a/app/i18n/cz/conf.php
+++ b/app/i18n/cz/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Zobrazení',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Spodní řádek',
'display_authors' => 'Autoři',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Časový limit HTML5 oznámení',
),
'show_nav_buttons' => 'Zobrazit navigační tlačítka',
- 'theme' => 'Motiv',
+ 'theme' => array(
+ '_' => 'Motiv',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Motiv „%s“ již není dostupný. Zvolte jiný motiv, prosím.',
'thumbnail' => array(
'label' => 'Náhled',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Na výšku',
'square' => 'Čtverec',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Zobrazení',
'width' => array(
'content' => 'Šířka obsahu',
diff --git a/app/i18n/cz/gen.php b/app/i18n/cz/gen.php
index 55586b24c..1f75033fa 100644
--- a/app/i18n/cz/gen.php
+++ b/app/i18n/cz/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Uživatelské dotazy',
'reading' => 'Čtení',
'search' => 'Hledat slova nebo #štítky',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Sdílení',
'shortcuts' => 'Zkratky',
'stats' => 'Statistika',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Známé základní stránky',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Schránka',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'E-mail',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php
index a11a9359d..3d08c315b 100644
--- a/app/i18n/cz/sub.php
+++ b/app/i18n/cz/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath pro:',
),
'rss' => 'RSS / Atom (výchozí)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Vymazat mezipaměť',
diff --git a/app/i18n/de/conf.php b/app/i18n/de/conf.php
index 5e9cdb36e..8962123f4 100644
--- a/app/i18n/de/conf.php
+++ b/app/i18n/de/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Anzeige',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Fußzeile',
'display_authors' => 'Autoren',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Zeitüberschreitung für HTML5-Benachrichtigung',
),
'show_nav_buttons' => 'Zeige Navigations-Buttons',
- 'theme' => 'Erscheinungsbild',
+ 'theme' => array(
+ '_' => 'Layout',
+ 'deprecated' => array(
+ '_' => 'Veraltet',
+ 'description' => 'Diese Layout wird nicht mehr länger aktualisiert und wir in einer <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">zukünftigen Version von FreshRSS</a> entfernt sein.',
+ ),
+ ),
'theme_not_available' => 'Das Erscheinungsbild „%s“ ist nicht mehr verfügbar. Bitte ein anderes auswählen.',
'thumbnail' => array(
'label' => 'Vorschaubild',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Hochformat',
'square' => 'Quadrat',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Anzeige',
'width' => array(
'content' => 'Inhaltsbreite',
diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php
index 59f532c74..fb35bc41c 100644
--- a/app/i18n/de/gen.php
+++ b/app/i18n/de/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Benutzerabfragen',
'reading' => 'Lesen',
'search' => 'Suche Worte oder #Tags',
+ 'search_help' => 'Siehe Dokumentation zu den <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">Suchparametern</a>',
'sharing' => 'Teilen',
'shortcuts' => 'Tastaturkürzel',
'stats' => 'Statistiken',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known-Seite (https://withknown.com)',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Zwischenablage',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'E-Mail',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php
index 580f7d348..b265c1b98 100644
--- a/app/i18n/de/sub.php
+++ b/app/i18n/de/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath für:',
),
'rss' => 'RSS / Atom (Standard)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Zwischenspeicher leeren',
diff --git a/app/i18n/el/conf.php b/app/i18n/el/conf.php
index 98f559d18..daacfe684 100644
--- a/app/i18n/el/conf.php
+++ b/app/i18n/el/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Display', // TODO
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Bottom line', // TODO
'display_authors' => 'Authors', // TODO
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 notification timeout', // TODO
),
'show_nav_buttons' => 'Show the navigation buttons', // TODO
- 'theme' => 'Theme', // TODO
+ 'theme' => array(
+ '_' => 'Theme', // TODO
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // TODO
'thumbnail' => array(
'label' => 'Thumbnail', // TODO
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait', // TODO
'square' => 'Square', // TODO
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Display', // TODO
'width' => array(
'content' => 'Content width', // TODO
diff --git a/app/i18n/el/gen.php b/app/i18n/el/gen.php
index a0c95ab39..03852a0c6 100644
--- a/app/i18n/el/gen.php
+++ b/app/i18n/el/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'User queries', // TODO
'reading' => 'Reading', // TODO
'search' => 'Search words or #tags', // TODO
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Sharing', // TODO
'shortcuts' => 'Shortcuts', // TODO
'stats' => 'Statistics', // TODO
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites', // TODO
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // TODO
'blogotext' => 'Blogotext', // TODO
'clipboard' => 'Clipboard', // TODO
'diaspora' => 'Diaspora*', // TODO
'email' => 'Email', // TODO
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // TODO
'gnusocial' => 'GNU social', // TODO
'jdh' => 'Journal du hacker', // TODO
diff --git a/app/i18n/el/sub.php b/app/i18n/el/sub.php
index 424fafc7b..aae9ae412 100644
--- a/app/i18n/el/sub.php
+++ b/app/i18n/el/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Clear cache', // TODO
diff --git a/app/i18n/en-us/conf.php b/app/i18n/en-us/conf.php
index 8330e4970..afea0299a 100644
--- a/app/i18n/en-us/conf.php
+++ b/app/i18n/en-us/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Display', // IGNORE
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Bottom line', // IGNORE
'display_authors' => 'Authors', // IGNORE
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 notification timeout', // IGNORE
),
'show_nav_buttons' => 'Show the navigation buttons', // IGNORE
- 'theme' => 'Theme', // IGNORE
+ 'theme' => array(
+ '_' => 'Theme', // IGNORE
+ 'deprecated' => array(
+ '_' => 'Deprecated', // IGNORE
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // IGNORE
+ ),
+ ),
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // IGNORE
'thumbnail' => array(
'label' => 'Thumbnail', // IGNORE
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait', // IGNORE
'square' => 'Square', // IGNORE
),
+ 'timezone' => 'Time zone', // IGNORE
'title' => 'Display', // IGNORE
'width' => array(
'content' => 'Content width', // IGNORE
diff --git a/app/i18n/en-us/gen.php b/app/i18n/en-us/gen.php
index c5f92ad40..ca08ed27f 100644
--- a/app/i18n/en-us/gen.php
+++ b/app/i18n/en-us/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'User queries', // IGNORE
'reading' => 'Reading', // IGNORE
'search' => 'Search words or #tags', // IGNORE
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // IGNORE
'sharing' => 'Sharing', // IGNORE
'shortcuts' => 'Shortcuts', // IGNORE
'stats' => 'Statistics', // IGNORE
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites', // IGNORE
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Clipboard', // IGNORE
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/en-us/sub.php b/app/i18n/en-us/sub.php
index a6b311084..92d75b81e 100644
--- a/app/i18n/en-us/sub.php
+++ b/app/i18n/en-us/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath for:', // IGNORE
),
'rss' => 'RSS / Atom (default)', // IGNORE
+ 'xml_xpath' => 'XML + XPath', // IGNORE
),
'maintenance' => array(
'clear_cache' => 'Clear cache', // IGNORE
diff --git a/app/i18n/en/conf.php b/app/i18n/en/conf.php
index fe03499ea..9899cf897 100644
--- a/app/i18n/en/conf.php
+++ b/app/i18n/en/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Display',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Bottom line',
'display_authors' => 'Authors',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 notification timeout',
),
'show_nav_buttons' => 'Show the navigation buttons',
- 'theme' => 'Theme',
+ 'theme' => array(
+ '_' => 'Theme',
+ 'deprecated' => array(
+ '_' => 'Deprecated',
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>',
+ ),
+ ),
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.',
'thumbnail' => array(
'label' => 'Thumbnail',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait',
'square' => 'Square',
),
+ 'timezone' => 'Time zone',
'title' => 'Display',
'width' => array(
'content' => 'Content width',
diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php
index 8f7065a83..d3a36995f 100644
--- a/app/i18n/en/gen.php
+++ b/app/i18n/en/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'User queries',
'reading' => 'Reading',
'search' => 'Search words or #tags',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Sharing',
'shortcuts' => 'Shortcuts',
'stats' => 'Statistics',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites',
+ 'archiveORG' => 'archive.org',
'archivePH' => 'archive.ph',
'blogotext' => 'Blogotext',
'clipboard' => 'Clipboard',
'diaspora' => 'Diaspora*',
'email' => 'Email',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook',
'gnusocial' => 'GNU social',
'jdh' => 'Journal du hacker',
diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php
index c7e100c25..04caaff05 100644
--- a/app/i18n/en/sub.php
+++ b/app/i18n/en/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath for:',
),
'rss' => 'RSS / Atom (default)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Clear cache',
diff --git a/app/i18n/es/admin.php b/app/i18n/es/admin.php
index 868cac45d..868cac45d 100755..100644
--- a/app/i18n/es/admin.php
+++ b/app/i18n/es/admin.php
diff --git a/app/i18n/es/conf.php b/app/i18n/es/conf.php
index c91b0205c..5137ff987 100755..100644
--- a/app/i18n/es/conf.php
+++ b/app/i18n/es/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Visualización',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Línea inferior',
'display_authors' => 'Autores/Autoras',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Notificación de fin de espera HTML5',
),
'show_nav_buttons' => 'Mostrar los botones de navegación',
- 'theme' => 'Tema',
+ 'theme' => array(
+ '_' => 'Tema',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'El tema “%s” ya no está disponible. Por favor, elija otro tema.',
'thumbnail' => array(
'label' => 'Miniatura',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Retrato',
'square' => 'Cuadrado',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Visualización',
'width' => array(
'content' => 'Ancho de contenido',
diff --git a/app/i18n/es/feedback.php b/app/i18n/es/feedback.php
index b7305e6d9..b7305e6d9 100755..100644
--- a/app/i18n/es/feedback.php
+++ b/app/i18n/es/feedback.php
diff --git a/app/i18n/es/gen.php b/app/i18n/es/gen.php
index 209a40dac..5ea2fce23 100755..100644
--- a/app/i18n/es/gen.php
+++ b/app/i18n/es/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Peticiones de usuario',
'reading' => 'Lectura',
'search' => 'Buscar palabras o #etiquetas',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Compartir',
'shortcuts' => 'Atajos',
'stats' => 'Estadísticas',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Sitios basados en conocidos',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Portapapeles',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/es/index.php b/app/i18n/es/index.php
index 610f677ed..610f677ed 100755..100644
--- a/app/i18n/es/index.php
+++ b/app/i18n/es/index.php
diff --git a/app/i18n/es/install.php b/app/i18n/es/install.php
index 3c46d6e58..3c46d6e58 100755..100644
--- a/app/i18n/es/install.php
+++ b/app/i18n/es/install.php
diff --git a/app/i18n/es/sub.php b/app/i18n/es/sub.php
index 52d681067..4fd2fa393 100755..100644
--- a/app/i18n/es/sub.php
+++ b/app/i18n/es/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath para:',
),
'rss' => 'RSS / Atom (por defecto)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Borrar caché',
diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php
index 61306289c..3122e3be5 100644
--- a/app/i18n/fr/conf.php
+++ b/app/i18n/fr/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Affichage',
+ 'darkMode' => 'Mode sombre automatique (bêta)',
'icon' => array(
'bottom_line' => 'Ligne du bas',
'display_authors' => 'Auteurs',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Temps d’affichage de la notification HTML5',
),
'show_nav_buttons' => 'Afficher les boutons de navigation',
- 'theme' => 'Thème',
+ 'theme' => array(
+ '_' => 'Thème',
+ 'deprecated' => array(
+ '_' => 'Obsolète',
+ 'description' => 'Ce thème est obsolète et sera supprimé dans une <a href="https://freshrss.github.io/FreshRSS/fr/users/05_Configuration.html#th%C3%A8me" target="_blank">future version de FreshRSS</a>',
+ ),
+ ),
'theme_not_available' => 'Le thème <em>%s</em> n’est plus disponible. Veuillez choisir un autre thème.',
'thumbnail' => array(
'label' => 'Miniature',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait', // IGNORE
'square' => 'Carrée',
),
+ 'timezone' => 'Fuseau horaire',
'title' => 'Affichage',
'width' => array(
'content' => 'Largeur du contenu',
diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php
index 69d260063..53e7160a2 100644
--- a/app/i18n/fr/gen.php
+++ b/app/i18n/fr/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Filtres utilisateurs',
'reading' => 'Lecture',
'search' => 'Rechercher des mots ou des #tags',
+ 'search_help' => 'Voir <a href="https://freshrss.github.io/FreshRSS/fr/users/03_Main_view.html#gr%C3%A2ce-au-champ-de-recherche" target="_blank">la documentation pour la syntaxe des recherches avancées</a>',
'sharing' => 'Partage',
'shortcuts' => 'Raccourcis',
'stats' => 'Statistiques',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Sites basés sur Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Presse-papier',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Courriel',
+ 'email-webmail-firefox-fix' => 'Courriel (pour Webmail avec Firefox)',
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php
index f9df0dbcc..be6dc094d 100644
--- a/app/i18n/fr/sub.php
+++ b/app/i18n/fr/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath pour :',
),
'rss' => 'RSS / Atom (par défaut)',
+ 'xml_xpath' => 'XML + XPath', // IGNORE
),
'maintenance' => array(
'clear_cache' => 'Vider le cache',
diff --git a/app/i18n/he/conf.php b/app/i18n/he/conf.php
index ad479db44..c4a490a2d 100644
--- a/app/i18n/he/conf.php
+++ b/app/i18n/he/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'תצוגה',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'שורה תחתונה',
'display_authors' => 'Authors', // TODO
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 התראה פג תוקף',
),
'show_nav_buttons' => 'Show the navigation buttons', // TODO
- 'theme' => 'ערכת נושא',
+ 'theme' => array(
+ '_' => 'ערכת נושא',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // TODO
'thumbnail' => array(
'label' => 'Thumbnail', // TODO
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait', // TODO
'square' => 'Square', // TODO
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'תצוגה',
'width' => array(
'content' => 'רוחב התוכן',
diff --git a/app/i18n/he/gen.php b/app/i18n/he/gen.php
index a8df3db6b..6345e66e9 100644
--- a/app/i18n/he/gen.php
+++ b/app/i18n/he/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'שאילתות',
'reading' => 'קריאה',
'search' => 'חיפוש מילים או #תגים',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'שיתוף',
'shortcuts' => 'קיצורי דרך',
'stats' => 'סטטיסטיקות',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites', // TODO
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Clipboard', // TODO
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'דואר אלקטרוני',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/he/sub.php b/app/i18n/he/sub.php
index 25552ffa1..bae5f5177 100644
--- a/app/i18n/he/sub.php
+++ b/app/i18n/he/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Clear cache', // TODO
diff --git a/app/i18n/id/conf.php b/app/i18n/id/conf.php
index b8a5b4fc1..8b1fa8dc6 100644
--- a/app/i18n/id/conf.php
+++ b/app/i18n/id/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Display', // TODO
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Bottom line', // TODO
'display_authors' => 'Authors', // TODO
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 notification timeout', // TODO
),
'show_nav_buttons' => 'Show the navigation buttons', // TODO
- 'theme' => 'Theme', // TODO
+ 'theme' => array(
+ '_' => 'Theme', // TODO
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // TODO
'thumbnail' => array(
'label' => 'Thumbnail', // TODO
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait', // TODO
'square' => 'Square', // TODO
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Display', // TODO
'width' => array(
'content' => 'Content width', // TODO
diff --git a/app/i18n/id/gen.php b/app/i18n/id/gen.php
index 93f8b0afe..1fc2fa155 100644
--- a/app/i18n/id/gen.php
+++ b/app/i18n/id/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'User queries', // TODO
'reading' => 'Reading', // TODO
'search' => 'Search words or #tags', // TODO
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Sharing', // TODO
'shortcuts' => 'Shortcuts', // TODO
'stats' => 'Statistics', // TODO
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites', // TODO
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // TODO
'blogotext' => 'Blogotext', // TODO
'clipboard' => 'Clipboard', // TODO
'diaspora' => 'Diaspora*', // TODO
'email' => 'Email', // TODO
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // TODO
'gnusocial' => 'GNU social', // TODO
'jdh' => 'Journal du hacker', // TODO
diff --git a/app/i18n/id/sub.php b/app/i18n/id/sub.php
index 7fdf5c024..3f9a4916a 100644
--- a/app/i18n/id/sub.php
+++ b/app/i18n/id/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Clear cache', // TODO
diff --git a/app/i18n/it/conf.php b/app/i18n/it/conf.php
index 4597687cc..6f3540322 100644
--- a/app/i18n/it/conf.php
+++ b/app/i18n/it/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Visualizzazione',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Barra in fondo',
'display_authors' => 'Autori',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Notifica timeout HTML5',
),
'show_nav_buttons' => 'Mostra i pulsanti di navigazione',
- 'theme' => 'Tema',
+ 'theme' => array(
+ '_' => 'Tema',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Il tema “%s” non è più disponibile. Si prega di selezionarne un altro.',
'thumbnail' => array(
'label' => 'Miniatura',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Ritratto',
'square' => 'Squadrata',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Visualizzazione',
'width' => array(
'content' => 'Larghezza contenuto',
diff --git a/app/i18n/it/gen.php b/app/i18n/it/gen.php
index e5458866c..f3edb57aa 100644
--- a/app/i18n/it/gen.php
+++ b/app/i18n/it/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Ricerche personali',
'reading' => 'Lettura',
'search' => 'Ricerca parole o #tags',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Condivisione',
'shortcuts' => 'Comandi tastiera',
'stats' => 'Statistiche',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Siti basati su Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Appunti',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/it/sub.php b/app/i18n/it/sub.php
index 8614caca7..7ab83cf07 100644
--- a/app/i18n/it/sub.php
+++ b/app/i18n/it/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath per:',
),
'rss' => 'RSS / Atom (predefinito)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Svuota cache',
diff --git a/app/i18n/ja/conf.php b/app/i18n/ja/conf.php
index 5e9aabfa2..4dd939760 100644
--- a/app/i18n/ja/conf.php
+++ b/app/i18n/ja/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => '表示',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => '行の下部',
'display_authors' => '著者',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 の通知タイムアウト時間',
),
'show_nav_buttons' => 'ナビゲーションボタンを表示する',
- 'theme' => 'テーマ',
+ 'theme' => array(
+ '_' => 'テーマ',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => '“%s”テーマはご利用いただけません。他のテーマをお選びください。',
'thumbnail' => array(
'label' => 'サムネイル',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'ポートレート',
'square' => '四角',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'ディスプレイ',
'width' => array(
'content' => 'コンテンツ幅',
diff --git a/app/i18n/ja/gen.php b/app/i18n/ja/gen.php
index 69fc8f9c9..f6cf2dcdf 100644
--- a/app/i18n/ja/gen.php
+++ b/app/i18n/ja/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'ユーザークエリ',
'reading' => 'リーディング',
'search' => '単語で検索するかハッシュタグで検索する',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => '共有',
'shortcuts' => 'ショートカット',
'stats' => '統計',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'よく使われるサイト',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'クリップボード',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Eメール',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/ja/sub.php b/app/i18n/ja/sub.php
index 80548c025..2425b21f3 100644
--- a/app/i18n/ja/sub.php
+++ b/app/i18n/ja/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPathは:',
),
'rss' => 'RSS / Atom (標準)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'キャッシュのクリア',
diff --git a/app/i18n/ko/conf.php b/app/i18n/ko/conf.php
index 279f2f4ad..a88fcf9e0 100644
--- a/app/i18n/ko/conf.php
+++ b/app/i18n/ko/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => '표시',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => '하단',
'display_authors' => '저자',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 알림 타임아웃',
),
'show_nav_buttons' => '내비게이션 버튼 보이기',
- 'theme' => '테마',
+ 'theme' => array(
+ '_' => '테마',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => '“%s” 테마는 더이상 사용할 수 없습니다. 다른 테마를 선택해 주세요.',
'thumbnail' => array(
'label' => '섬네일',
@@ -57,6 +64,7 @@ return array(
'portrait' => '세로 방향',
'square' => '정사각형',
),
+ 'timezone' => 'Time zone', // TODO
'title' => '표시',
'width' => array(
'content' => '내용 표시 너비',
diff --git a/app/i18n/ko/gen.php b/app/i18n/ko/gen.php
index 4f6b6a228..da1f57e9c 100644
--- a/app/i18n/ko/gen.php
+++ b/app/i18n/ko/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => '사용자 쿼리',
'reading' => '읽기',
'search' => '단어 또는 #태그 검색',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => '공유',
'shortcuts' => '단축키',
'stats' => '통계',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites', // IGNORE
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => '클립보드',
'diaspora' => 'Diaspora*', // IGNORE
'email' => '메일',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/ko/sub.php b/app/i18n/ko/sub.php
index e0ef5990b..f376247d5 100644
--- a/app/i18n/ko/sub.php
+++ b/app/i18n/ko/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => '다음의 XPath:',
),
'rss' => 'RSS / Atom (기본값)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => '캐쉬 지우기',
diff --git a/app/i18n/nl/conf.php b/app/i18n/nl/conf.php
index 8b3d597b1..e02ca81cc 100644
--- a/app/i18n/nl/conf.php
+++ b/app/i18n/nl/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Opmaak',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Onderaan',
'display_authors' => 'Auteurs',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 notificatie stop',
),
'show_nav_buttons' => 'Toon navigatieknoppen',
- 'theme' => 'Thema',
+ 'theme' => array(
+ '_' => 'Thema',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Het „%s” thema is niet meer beschikbaar. Kies een ander thema.',
'thumbnail' => array(
'label' => 'Miniatuur',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Staand',
'square' => 'Vierkant',
),
+ 'timezone' => 'Tijdzone',
'title' => 'Opmaak',
'width' => array(
'content' => 'Inhoud breedte',
diff --git a/app/i18n/nl/gen.php b/app/i18n/nl/gen.php
index abd21f460..ad3379ece 100644
--- a/app/i18n/nl/gen.php
+++ b/app/i18n/nl/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Gebruikers informatie',
'reading' => 'Lezen',
'search' => 'Zoek woorden of #labels',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Delen',
'shortcuts' => 'Snelle toegang',
'stats' => 'Statistieken',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known-gebaseerde sites',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Klembord',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php
index 0fa767171..631da9477 100644
--- a/app/i18n/nl/sub.php
+++ b/app/i18n/nl/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath voor:',
),
'rss' => 'RSS / Atom (standaard)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Cache leegmaken',
diff --git a/app/i18n/oc/conf.php b/app/i18n/oc/conf.php
index c1834e9aa..4a3b483e7 100644
--- a/app/i18n/oc/conf.php
+++ b/app/i18n/oc/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Afichatge',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Linha enbàs',
'display_authors' => 'Autors',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Temps d’afichatge de las notificacions HTML5',
),
'show_nav_buttons' => 'Mostrar los botons de navigacion',
- 'theme' => 'Tèma',
+ 'theme' => array(
+ '_' => 'Tèma',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Lo tèma « %s » es pas pus disponible. Causissètz un autre tèma.',
'thumbnail' => array(
'label' => 'Vinheta',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Retrach',
'square' => 'Carrat',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Afichatge',
'width' => array(
'content' => 'Largor del contengut',
diff --git a/app/i18n/oc/gen.php b/app/i18n/oc/gen.php
index 41f2c1499..8e852a810 100644
--- a/app/i18n/oc/gen.php
+++ b/app/i18n/oc/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Filtres utilizaire',
'reading' => 'Lectura',
'search' => 'Recercar de mots o d’#etiquetas',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Partatge',
'shortcuts' => 'Acorchis',
'stats' => 'Estatisticas',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Sites basats sus Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Quicha-papiers.',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Corrièl',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/oc/sub.php b/app/i18n/oc/sub.php
index 92a73057c..008b4964d 100644
--- a/app/i18n/oc/sub.php
+++ b/app/i18n/oc/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath per :',
),
'rss' => 'RSS / Atom (defaut)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Escafar lo cache',
diff --git a/app/i18n/pl/conf.php b/app/i18n/pl/conf.php
index 31b0d238c..8700a1c13 100644
--- a/app/i18n/pl/conf.php
+++ b/app/i18n/pl/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Wyświetlanie',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Dolny margines',
'display_authors' => 'Autorzy',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Czas wyświetlania powiadomienia HTML5',
),
'show_nav_buttons' => 'Pokaż przyciski nawigacyjne',
- 'theme' => 'Motyw',
+ 'theme' => array(
+ '_' => 'Motyw',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Motyw “%s” nie jest już dostępny. Wybierz inny motyw.',
'thumbnail' => array(
'label' => 'Miniaturka',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portret',
'square' => 'Kwadrat',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Wyświetlanie',
'width' => array(
'content' => 'Rozmiar treści',
diff --git a/app/i18n/pl/gen.php b/app/i18n/pl/gen.php
index 1a7bd69a5..fc91d8bca 100644
--- a/app/i18n/pl/gen.php
+++ b/app/i18n/pl/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Zapisane zapytania',
'reading' => 'Czytanie',
'search' => 'Wyszukaj wyrazy lub #tagi',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Podawanie dalej',
'shortcuts' => 'Skróty klawiszowe',
'stats' => 'Statystyki',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Strony bazujące na usłudze Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Schowek',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'E-mail',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/pl/sub.php b/app/i18n/pl/sub.php
index b6121fcb7..565401982 100644
--- a/app/i18n/pl/sub.php
+++ b/app/i18n/pl/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath dla:',
),
'rss' => 'RSS / Atom (domyślne)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Wyczyść pamięć podręczną',
diff --git a/app/i18n/pt-br/conf.php b/app/i18n/pt-br/conf.php
index b925aee21..f8ad55f14 100644
--- a/app/i18n/pt-br/conf.php
+++ b/app/i18n/pt-br/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Exibição',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Linha inferior',
'display_authors' => 'Autores',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Notificação em HTML5 de timeout',
),
'show_nav_buttons' => 'Mostrar botões de navegação',
- 'theme' => 'Tema',
+ 'theme' => array(
+ '_' => 'Tema',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'O tema “%s” não está mais disponível. Por favor escolha outro tema.',
'thumbnail' => array(
'label' => 'Miniatura',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Modo retrato',
'square' => 'Modo quadrado',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Exibição',
'width' => array(
'content' => 'Largura do conteúdo',
diff --git a/app/i18n/pt-br/gen.php b/app/i18n/pt-br/gen.php
index 969056969..51c1eb327 100644
--- a/app/i18n/pt-br/gen.php
+++ b/app/i18n/pt-br/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Queries de usuário',
'reading' => 'Leitura',
'search' => 'Procurar por palavras ou #tags',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Compartilhamento',
'shortcuts' => 'Atalhos',
'stats' => 'Estatísticas',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Sites no Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Área de transferência',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'E-mail',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/pt-br/sub.php b/app/i18n/pt-br/sub.php
index c9755755e..4cdee8681 100644
--- a/app/i18n/pt-br/sub.php
+++ b/app/i18n/pt-br/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath para:',
),
'rss' => 'RSS / Atom (padrão)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Limpar o cache',
diff --git a/app/i18n/ru/conf.php b/app/i18n/ru/conf.php
index c0d25aec1..2c5dda544 100644
--- a/app/i18n/ru/conf.php
+++ b/app/i18n/ru/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Отображение',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Нижняя линия',
'display_authors' => 'Авторы',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Таймаут уведомлений HTML5',
),
'show_nav_buttons' => 'Показать кнопки навигации',
- 'theme' => 'Тема',
+ 'theme' => array(
+ '_' => 'Тема',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Тема “%s” больше не доступна. Пожалуйста выберите другю тему.',
'thumbnail' => array(
'label' => 'Эскиз',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Вертикальный',
'square' => 'Квадратный',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Отображение',
'width' => array(
'content' => 'Ширина содержимого',
diff --git a/app/i18n/ru/gen.php b/app/i18n/ru/gen.php
index 3ed1ab1ac..ddfea7ca4 100644
--- a/app/i18n/ru/gen.php
+++ b/app/i18n/ru/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Пользовательские запросы',
'reading' => 'Чтение',
'search' => 'Искать слова или #теги',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Обмен',
'shortcuts' => 'Горячие клавиши',
'stats' => 'Статистика',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Сайты на Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Буфер обмена',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Электронная почта',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/ru/sub.php b/app/i18n/ru/sub.php
index 5704b53b1..d13c4c4f0 100644
--- a/app/i18n/ru/sub.php
+++ b/app/i18n/ru/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath для:',
),
'rss' => 'RSS / Atom (по умолчанию)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Очистить кэш',
diff --git a/app/i18n/sk/conf.php b/app/i18n/sk/conf.php
index 7efc3a75d..d4714b506 100644
--- a/app/i18n/sk/conf.php
+++ b/app/i18n/sk/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Zobrazenie',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Spodný riadok',
'display_authors' => 'Autori',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Limit HTML5 oznámenia',
),
'show_nav_buttons' => 'Zobraziť tlačidlá oznámenia',
- 'theme' => 'Vzhľad',
+ 'theme' => array(
+ '_' => 'Vzhľad',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Vzhľad “%s” už nie je dostupný. Prosím, vyberte si iný vzhľad.',
'thumbnail' => array(
'label' => 'Miniatúra',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Nastojato',
'square' => 'Štvorec',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Zobraziť',
'width' => array(
'content' => 'Šírka obsahu',
diff --git a/app/i18n/sk/gen.php b/app/i18n/sk/gen.php
index 6bb5e4161..d591266e4 100644
--- a/app/i18n/sk/gen.php
+++ b/app/i18n/sk/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Používateľské dopyty',
'reading' => 'Čítanie',
'search' => 'Hľadajte slová alebo #značky',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Zdieľanie',
'shortcuts' => 'Skratky',
'stats' => 'Štatistiky',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Stránky založené na Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Schránka',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'E-mail', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/sk/sub.php b/app/i18n/sk/sub.php
index f583f6ca0..3c980d202 100644
--- a/app/i18n/sk/sub.php
+++ b/app/i18n/sk/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath pre:',
),
'rss' => 'RSS / Atom (prednastavené)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Vymazať vyrovnáciu pamäť',
diff --git a/app/i18n/tr/conf.php b/app/i18n/tr/conf.php
index 7220d6670..41f658879 100644
--- a/app/i18n/tr/conf.php
+++ b/app/i18n/tr/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Görünüm',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Alt çizgi',
'display_authors' => 'Yazarlar',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 bildirim zaman aşımı',
),
'show_nav_buttons' => 'Gezinti düğmelerini göster',
- 'theme' => 'Tema',
+ 'theme' => array(
+ '_' => 'Tema',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => '“%s” teması şuan uygun değilç Lütfen başka bir tema seçin.',
'thumbnail' => array(
'label' => 'Önizleme',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portre',
'square' => 'Kare',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Görünüm',
'width' => array(
'content' => 'İçerik genişliği',
diff --git a/app/i18n/tr/gen.php b/app/i18n/tr/gen.php
index 8839023e6..4b84d6c40 100644
--- a/app/i18n/tr/gen.php
+++ b/app/i18n/tr/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Kullanıcı sorguları',
'reading' => 'Okuma',
'search' => 'Kelime veya #etiket ara',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Paylaşım',
'shortcuts' => 'Kısayollar',
'stats' => 'İstatistikler',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Bilinen siteler',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Kopyala',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/tr/sub.php b/app/i18n/tr/sub.php
index 056c059ac..3e03f667c 100644
--- a/app/i18n/tr/sub.php
+++ b/app/i18n/tr/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath:',
),
'rss' => 'RSS / Atom (varsayılan)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Önbelleği temizle',
diff --git a/app/i18n/zh-cn/admin.php b/app/i18n/zh-cn/admin.php
index 46b4d190a..79bcde2e1 100644
--- a/app/i18n/zh-cn/admin.php
+++ b/app/i18n/zh-cn/admin.php
@@ -17,7 +17,7 @@ return array(
'api_enabled' => '允许 <abbr>API</abbr> 访问 <small>(用于手机应用)</small>',
'form' => '网页表单(传统方式, 需要 JavaScript)',
'http' => 'HTTP(面向启用 HTTPS 的高级用户)',
- 'none' => '无认证(危险)',
+ 'none' => '无(危险)',
'title' => '认证',
'token' => '认证口令',
'token_help' => '用于不经认证访问默认用户的 RSS 输出:',
@@ -26,7 +26,7 @@ return array(
),
'check_install' => array(
'cache' => array(
- 'nok' => '请检查 <em>./data/cache</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data/cache</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'cache 目录权限正常',
),
'categories' => array(
@@ -39,27 +39,27 @@ return array(
),
'ctype' => array(
'nok' => '找不到字符类型检测库(php-ctype)',
- 'ok' => '已找到字符类型检测库 (php-ctype)',
+ 'ok' => '已找到字符类型检测库(ctype)',
),
'curl' => array(
- 'nok' => '找不到 cURL 库(php-cURL)',
- 'ok' => '已找到 cURL 库(php-cURL)',
+ 'nok' => '找不到 cURL 库(php-curl 包)',
+ 'ok' => '已找到 cURL 库',
),
'data' => array(
- 'nok' => '请检查 <em>./data</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'data 目录权限正常',
),
- 'database' => '数据库相关',
+ 'database' => '数据库安装',
'dom' => array(
- 'nok' => '找不到用于浏览 DOM 的库(php-xml)',
- 'ok' => '已找到用于浏览 DOM 的库(php-xml)',
+ 'nok' => '找不到用于浏览 DOM 的库(php-xml 包)',
+ 'ok' => '已找到用于浏览 DOM 的库',
),
'entries' => array(
'nok' => 'Entry 表配置错误',
- 'ok' => 'Entry 表正常',
+ 'ok' => 'Entry 表配置正常',
),
'favicons' => array(
- 'nok' => '请检查 <em>./data/favicons</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data/favicons</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'favicons 目录权限正常',
),
'feeds' => array(
@@ -67,46 +67,46 @@ return array(
'ok' => 'Feed 表正常',
),
'fileinfo' => array(
- 'nok' => '找不到 fileinfo 库(php-fileinfo)',
- 'ok' => '已找到 fileinfo 库(php-fileinfo)',
+ 'nok' => '找不到 PHP fileinfo 库(php-fileinfo 包)',
+ 'ok' => '已找到 fileinfo 库',
),
'files' => '文件相关',
'json' => array(
- 'nok' => '找不到 JSON 扩展(php-json )',
- 'ok' => '已找到 JSON 扩展(php-json)',
+ 'nok' => '找不到 JSON 扩展(php-json 包)',
+ 'ok' => '已找到 JSON 扩展',
),
'mbstring' => array(
- 'nok' => '找不到推荐的 Unicode 解析库(mbstring)',
- 'ok' => '已找到推荐的 Unicode 解析库(mbstring)',
+ 'nok' => '找不到推荐用于 Unicode 的 mbstring 库',
+ 'ok' => '已找到推荐用于 Unicode 的 mbstring 库',
),
'pcre' => array(
'nok' => '找不到正则表达式解析库(php-pcre)',
- 'ok' => '已找到正则表达式解析库(php-pcre)',
+ 'ok' => '已找到正则表达式解析库(PCRE)',
),
'pdo' => array(
- 'nok' => '找不到 PDO 或支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
- 'ok' => '已找到 PDO 和支持的至少一种驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
+ 'nok' => '找不到 PDO 或其中一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
+ 'ok' => '已找到 PDO 和至少一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
),
'php' => array(
- '_' => 'PHP 相关',
+ '_' => 'PHP 安装',
'nok' => '你的 PHP 版本为 %s,但 FreshRSS 最低需要 %s',
'ok' => '你的 PHP 版本为 %s,与 FreshRSS 兼容',
),
'tables' => array(
'nok' => '数据库中缺少一个或多个表',
- 'ok' => '数据库中相关表存在',
+ 'ok' => '数据库中存在正确的表',
),
'title' => '环境检查',
'tokens' => array(
- 'nok' => '请检查 <em>./data/tokens</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data/tokens</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'tokens 目录权限正常',
),
'users' => array(
- 'nok' => '请检查 <em>./data/users</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data/users</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'users 目录权限正常',
),
'zip' => array(
- 'nok' => '找不到 ZIP 扩展(php-zip)',
+ 'nok' => '找不到 ZIP 扩展(php-zip 包)',
'ok' => '已找到 ZIP 扩展',
),
),
@@ -119,10 +119,10 @@ return array(
'enabled' => '已启用',
'latest' => '已安装',
'name' => '名称',
- 'no_configure_view' => '此扩展不能配置。',
+ 'no_configure_view' => '此扩展无法配置。',
'system' => array(
'_' => '系统扩展',
- 'no_rights' => '系统扩展(你无权修改)',
+ 'no_rights' => '系统扩展(你没有所需权限)',
),
'title' => '扩展',
'update' => '更新可用',
@@ -130,20 +130,20 @@ return array(
'version' => '版本',
),
'stats' => array(
- '_' => '统计',
+ '_' => '统计数据',
'all_feeds' => '所有订阅源',
'category' => '分类',
'entry_count' => '文章数',
'entry_per_category' => '各分类文章数',
- 'entry_per_day' => '近三十日每日文章数',
- 'entry_per_day_of_week' => '一周各日(平均:%.2f 条消息)',
- 'entry_per_hour' => '各小时(平均:%.2f 条消息)',
- 'entry_per_month' => '各月(平均:%.2f 条消息)',
+ 'entry_per_day' => '每日文章数(近三十日)',
+ 'entry_per_day_of_week' => '一周中(平均:%.2f 条消息)',
+ 'entry_per_hour' => '各小时(平均:%.2f 条消息)',
+ 'entry_per_month' => '各月(平均:%.2f 条消息)',
'entry_repartition' => '文章分布',
'feed' => '订阅源',
'feed_per_category' => '各分类订阅源数',
'idle' => '长期无更新订阅源',
- 'main' => '主要统计',
+ 'main' => '主要统计数据',
'main_stream' => '首页',
'no_idle' => '订阅源近期皆有更新!',
'number_entries' => '%d 篇文章',
@@ -158,9 +158,9 @@ return array(
),
'system' => array(
'_' => '系统配置',
- 'auto-update-url' => '自动升级服务器地址',
+ 'auto-update-url' => '自动更新服务器 URL',
'cookie-duration' => array(
- 'help' => '单位(秒)',
+ 'help' => '单位:秒',
'number' => '保持登录的时长',
),
'force_email_validation' => '强制验证邮箱地址',
@@ -178,8 +178,8 @@ return array(
),
),
'status' => array(
- 'disabled' => '注册表单禁用',
- 'enabled' => '注册表单启用',
+ 'disabled' => '注册表单已禁用',
+ 'enabled' => '注册表单已启用',
),
'title' => '用户注册表单',
),
@@ -191,7 +191,7 @@ return array(
'current_version' => '当前 FreshRSS 版本为 %s。',
'last' => '上次检查:%s',
'none' => '没有可用更新',
- 'title' => '系统更新',
+ 'title' => '更新系统',
),
'user' => array(
'admin' => '管理员',
diff --git a/app/i18n/zh-cn/conf.php b/app/i18n/zh-cn/conf.php
index 8f8ef09ad..0be182cfb 100644
--- a/app/i18n/zh-cn/conf.php
+++ b/app/i18n/zh-cn/conf.php
@@ -13,17 +13,17 @@
return array(
'archiving' => array(
'_' => '归档',
- 'exception' => '高级清理策略',
- 'help' => '具体选项位于各订阅源的设置',
- 'keep_favourites' => '不清理已收藏的文章',
- 'keep_labels' => '不清理标签',
+ 'exception' => '清理例外',
+ 'help' => '更多可用选项位于各订阅源的设置',
+ 'keep_favourites' => '永不删除已收藏的文章',
+ 'keep_labels' => '永不删除标签',
'keep_max' => '最多保留的文章数',
'keep_min_by_feed' => '至少保留的文章数',
'keep_period' => '文章最多保留',
- 'keep_unreads' => '不清理未读文章',
- 'maintenance' => '优化',
+ 'keep_unreads' => '永不删除未读文章',
+ 'maintenance' => '维护',
'optimize' => '优化数据库',
- 'optimize_help' => '偶尔执行优化可以减少数据库大小',
+ 'optimize_help' => '偶尔执行可以减少数据库大小',
'policy' => '清理策略',
'policy_warning' => '如果未选择清理策略,则将保留全部文章。',
'purge_now' => '立即清除',
@@ -32,12 +32,13 @@ return array(
),
'display' => array(
'_' => '显示',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => '底栏',
'display_authors' => '作者',
'entry' => '文章图标',
'publication_date' => '更新日期',
- 'related_tags' => '相关标签',
+ 'related_tags' => '文章标签',
'sharing' => '分享',
'summary' => '摘要',
'top_line' => '顶栏',
@@ -48,15 +49,22 @@ return array(
'timeout' => 'HTML5 通知超时时间',
),
'show_nav_buttons' => '显示导航按钮',
- 'theme' => '主题',
+ 'theme' => array(
+ '_' => '主题',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => '“%s” 主题不再可用,请选择其他主题。',
'thumbnail' => array(
'label' => '缩略图',
- 'landscape' => '风景',
+ 'landscape' => '横向',
'none' => '无',
- 'portrait' => '肖像',
- 'square' => '方块',
+ 'portrait' => '纵向',
+ 'square' => '方形',
),
+ 'timezone' => 'Time zone', // TODO
'title' => '显示',
'width' => array(
'content' => '内容宽度',
@@ -80,17 +88,17 @@ return array(
),
),
'profile' => array(
- '_' => '用户管理',
+ '_' => '账户管理',
'api' => 'API 管理',
'delete' => array(
'_' => '账户删除',
- 'warn' => '将删除你的帐户以及所有相关数据!',
+ 'warn' => '你的帐户以及所有相关数据将被删除。',
),
'email' => '邮箱地址',
'password_api' => 'API 密码<br /><small>(例如用于手机应用)</small>',
'password_form' => '密码<br /><small>(用于 Web-form 登录方式)</small>',
'password_format' => '至少 7 个字符',
- 'title' => '用户帐户',
+ 'title' => '账户',
),
'query' => array(
'_' => '自定义查询',
@@ -135,67 +143,67 @@ return array(
),
'reading' => array(
'_' => '阅读',
- 'after_onread' => '「全部标记为已读」后',
+ 'after_onread' => '“全部标记为已读”后',
'always_show_favorites' => '默认显示收藏夹中所有的文章',
'article' => array(
'authors_date' => array(
'_' => '作者和日期',
- 'both' => '两者都显示',
- 'footer' => '仅页脚显示',
- 'header' => '仅页眉显示',
+ 'both' => '页脚与页眉',
+ 'footer' => '页脚',
+ 'header' => '页眉',
'none' => '不显示',
),
'feed_name' => array(
- 'above_title' => '在文章标题和标签上方',
+ 'above_title' => '在标题/标签上方',
'none' => '不显示',
'with_authors' => '与作者和日期一行',
),
'feed_title' => '订阅源标题',
'tags' => array(
'_' => '文章标签',
- 'both' => '两者都显示',
- 'footer' => '仅页脚显示',
- 'header' => '仅页眉显示',
+ 'both' => '页脚与页眉',
+ 'footer' => '页脚',
+ 'header' => '页眉',
'none' => '不显示',
),
'tags_max' => array(
'_' => '标签最多显示个数',
- 'help' => '0 标识显示所有标签',
+ 'help' => '0 表示:显示所有标签且不折叠',
),
),
'articles_per_page' => '每页文章数',
'auto_load_more' => '在页面底部载入更多文章',
'auto_remove_article' => '阅读后隐藏文章',
- 'confirm_enabled' => '「全部标记为已读」时显示确认对话框',
+ 'confirm_enabled' => '“全部标记为已读”时显示确认对话框',
'display_articles_unfolded' => '默认展开显示文章',
'display_categories_unfolded' => '展开的分类',
'headline' => array(
'articles' => '文章:打开/关闭',
'articles_header_footer' => '文章: 页眉/页脚',
- 'categories' => '左侧导航:分类',
+ 'categories' => '左侧导航栏:分类',
'mark_as_read' => '标为已读选项',
'misc' => '其它',
'view' => '浏览',
),
- 'hide_read_feeds' => '隐藏没有未读文章的分类和订阅源 (启用「显示所有文章」后不生效)',
+ 'hide_read_feeds' => '隐藏没有未读文章的分类和订阅源(启用“显示所有文章”后不生效)',
'img_with_lazyload' => '延迟加载图片',
'jump_next' => '跳转到下一未读项(订阅源或分类)',
- 'mark_updated_article_unread' => '将更新的文章设为未读',
+ 'mark_updated_article_unread' => '将有更新的文章设为未读',
'number_divided_when_reader' => '阅读视图中显示一半',
'read' => array(
'article_open_on_website' => '在打开原文章后',
'article_viewed' => '在文章被浏览后',
'keep_max_n_unread' => '未读最多保留 n 条',
'scroll' => '在滚动浏览后',
- 'upon_gone' => '在被原订阅源移除后',
+ 'upon_gone' => '在被原订阅源被移除后',
'upon_reception' => '在接收文章后',
'when' => '何时将文章标记为已读',
'when_same_title' => '已存在 n 条相同标题文章',
),
'show' => array(
'_' => '文章显示',
- 'active_category' => '激活的分类',
- 'adaptive' => '智能显示',
+ 'active_category' => '活跃的分类',
+ 'adaptive' => '自适应显示',
'all_articles' => '显示所有',
'all_categories' => '所有分类',
'no_category' => '无分类',
@@ -203,13 +211,13 @@ return array(
'unread' => '只显示未读',
),
'show_fav_unread_help' => '同样适用于标签',
- 'sides_close_article' => '点击文章区域外以关闭',
+ 'sides_close_article' => '点击文章文本区域外关闭文章',
'sort' => array(
'_' => '排列顺序',
'newer_first' => '由新至旧',
'older_first' => '由旧至新',
),
- 'sticky_post' => '打开文章时将其置于页首',
+ 'sticky_post' => '打开文章时将其置顶',
'title' => '阅读',
'view' => array(
'default' => '默认视图',
@@ -222,20 +230,20 @@ return array(
'_' => '分享',
'add' => '添加分享方式',
'blogotext' => 'Blogotext', // IGNORE
- 'deprecated' => '这项功能已废弃并在将来版本的 FreshRSS 中移除,详情请见 <a href="https://freshrss.github.io/FreshRSS/en/users/08_sharing_services.html" title="Open documentation for more information" target="_blank">说明文档</a>.',
+ 'deprecated' => '此功能已被废弃并会在未来的 FreshRSS 版本中移除,详情见 <a href="https://freshrss.github.io/FreshRSS/en/users/08_sharing_services.html" title="打开文档获更多信息" target="_blank">说明文档</a>.',
'diaspora' => 'Diaspora*', // IGNORE
- 'email' => '邮箱', // IGNORE
- 'facebook' => '脸书', // IGNORE
+ 'email' => 'Email', // IGNORE
+ 'facebook' => 'Facebook', // IGNORE
'more_information' => '更多信息',
'print' => '打印',
'raindrop' => 'Raindrop.io', // IGNORE
'remove' => '删除分享方式',
'shaarli' => 'Shaarli', // IGNORE
- 'share_name' => '名称',
- 'share_url' => '地址',
+ 'share_name' => '显示名称',
+ 'share_url' => '用于分享的 URL',
'title' => '分享',
- 'twitter' => '推特', // IGNORE
- 'wallabag' => 'Wallabag', // IGNORE
+ 'twitter' => 'Twitter', // IGNORE
+ 'wallabag' => 'wallabag', // IGNORE
),
'shortcut' => array(
'_' => '快捷键',
@@ -243,9 +251,9 @@ return array(
'auto_share' => '分享',
'auto_share_help' => '如果有多种分享方式,则会按照它们的序号依次访问。',
'close_dropdown' => '关闭菜单',
- 'collapse_article' => '收起文章',
+ 'collapse_article' => '折叠文章',
'first_article' => '打开第一篇文章',
- 'focus_search' => '聚焦到搜索框',
+ 'focus_search' => '访问搜索框',
'global_view' => '切换到全屏视图',
'help' => '显示帮助文档',
'javascript' => '若要使用快捷键,必须启用 JavaScript',
@@ -254,18 +262,18 @@ return array(
'mark_favorite' => '加入收藏',
'mark_read' => '设为已读',
'navigation' => '浏览',
- 'navigation_help' => '组合 <kbd>⇧ Shift</kbd> 键,浏览快捷键将生效于订阅源。<br/>组合 <kbd>Alt ⎇</kbd> 键,浏览快捷键将生效于分类。',
+ 'navigation_help' => '组合 <kbd>⇧ Shift</kbd> 键,导航快捷键将应用于订阅源。<br/>组合 <kbd>Alt ⎇</kbd> 键,导航快捷键将应用于分类。',
'navigation_no_mod_help' => '以下快捷键不支持组合键(Shift 或 Alt)',
'next_article' => '打开下一篇文章',
'next_unread_article' => '打开下一篇未读文章',
- 'non_standard' => '这些键 (<kbd>%s</kbd>) 可能不能作为快捷键',
+ 'non_standard' => '这些键(<kbd>%s</kbd>)可能不能作为快捷键',
'normal_view' => '切换到普通视图',
'other_action' => '其他操作',
'previous_article' => '打开上一篇文章',
'reading_view' => '切换到阅读视图',
'rss_view' => '切换到 RSS 视图',
'see_on_website' => '在原网站中查看',
- 'shift_for_all_read' => '组合 <kbd>Alt ⎇</kbd>键 将上方的文章标记为已读<br />组合 <kbd>⇧ Shift</kbd>按键 可以将全部文章设为已读',
+ 'shift_for_all_read' => '+ <kbd>Alt ⎇</kbd> 键将上方的文章标记为已读<br />+ <kbd>⇧ Shift</kbd> 键将所有文章设为已读',
'skip_next_article' => '跳转到下一篇文章而不打开',
'skip_previous_article' => '跳转到上一篇文章而不打开',
'title' => '快捷键',
@@ -275,7 +283,7 @@ return array(
'views' => '视图',
),
'user' => array(
- 'articles_and_size' => '%s 篇文章 (%s)',
+ 'articles_and_size' => '%s 篇文章(%s)',
'current' => '当前用户',
'is_admin' => '该用户为管理员',
'users' => '用户',
diff --git a/app/i18n/zh-cn/feedback.php b/app/i18n/zh-cn/feedback.php
index 020e70918..701471f4e 100644
--- a/app/i18n/zh-cn/feedback.php
+++ b/app/i18n/zh-cn/feedback.php
@@ -20,8 +20,8 @@ return array(
),
'api' => array(
'password' => array(
- 'failed' => '您的密码无法修改',
- 'updated' => '您的密码已修改',
+ 'failed' => '你的密码无法修改',
+ 'updated' => '你的密码已修改',
),
),
'auth' => array(
@@ -43,7 +43,7 @@ return array(
'already_enabled' => '%s 已启用',
'cannot_remove' => '无法删除 %s',
'disable' => array(
- 'ko' => '禁用 %s 失败。<a href="%s">检查 FreshRSS 日志</a> 查看详情。',
+ 'ko' => '无法禁用 %s。<a href="%s">检查 FreshRSS 日志</a> 查看详情。',
'ok' => '%s 现已禁用',
),
'enable' => array(
@@ -56,15 +56,15 @@ return array(
'removed' => '%s 已删除',
),
'import_export' => array(
- 'export_no_zip_extension' => '服务器未启用 ZIP 扩展。请尝试逐个导出文件。',
- 'feeds_imported' => '你的订阅已导入,即将刷新',
+ 'export_no_zip_extension' => '服务器未启用 ZIP 扩展,请尝试逐个导出文件。',
+ 'feeds_imported' => '你的订阅源已导入,即将刷新',
'feeds_imported_with_errors' => '你的订阅源已导入,但发生错误',
'file_cannot_be_uploaded' => '文件未能上传!',
'no_zip_extension' => '服务器未启用 ZIP 扩展。',
'zip_error' => '导入 ZIP 文件时出错',
),
'profile' => array(
- 'error' => '你的帐户修改失败',
+ 'error' => '你的帐户无法修改',
'updated' => '你的帐户已修改',
),
'sub' => array(
@@ -79,7 +79,7 @@ return array(
'emptied' => '已清空分类',
'error' => '更新分类失败',
'name_exists' => '分类名已存在',
- 'no_id' => '你必须明确分类编号',
+ 'no_id' => '你必须指定分类 ID',
'no_name' => '分类名不能为空',
'not_delete_default' => '你不能删除默认分类!',
'not_exist' => '分类不存在!',
@@ -94,21 +94,21 @@ return array(
'cache_cleared' => '<em>%s</em> 缓存已清理',
'deleted' => '已删除订阅源',
'error' => '订阅源更新失败',
- 'internal_problem' => '订阅源添加失败。<a href="%s">检查 FreshRSS 日志</a> 查看详情。你可以在地址链接后附加 <code>#force_feed</code> 从而尝试强制添加。',
- 'invalid_url' => '地址链接 <em>%s</em> 无效',
+ 'internal_problem' => '订阅源添加失败,<a href="%s">检查 FreshRSS 日志</a> 查看详情。你可以在 URL 后添加 <code>#force_feed</code> 尝试强制添加。',
+ 'invalid_url' => 'URL <em>%s</em> 无效',
'n_actualized' => '已更新 %d 个订阅源',
'n_entries_deleted' => '已删除 %d 篇文章',
- 'no_refresh' => '没有可刷新的订阅源…',
+ 'no_refresh' => '没有可刷新的订阅源',
'not_added' => '<em>%s</em> 添加失败',
'not_found' => '无法找到订阅',
'over_max' => '你已达到订阅源数上限(%d)',
- 'reloaded' => '<em>%s</em> 已重置',
+ 'reloaded' => '<em>%s</em> 已重新加载',
'selector_preview' => array(
'http_error' => '无法加载网站内容。',
- 'no_entries' => '您的订阅中没有任何条目。您至少需要一个条目来创建一个预览。',
+ 'no_entries' => '你的订阅中没有任何条目,你至少需要一个条目来创建一个预览。',
'no_feed' => '网络错误(订阅源不存在)',
- 'no_result' => '选择器没有匹配到任何东西。作为备用,原始的feed文本将被显示出来。',
- 'selector_empty' => '选择器是空的。你需要一个来创建预览。',
+ 'no_result' => '选择器没有匹配到任何东西,回退显示原始的订阅源文本。',
+ 'selector_empty' => '选择器是空的,你需要一个来创建预览。',
),
'updated' => '已更新订阅源',
),
@@ -122,10 +122,10 @@ return array(
'update' => array(
'can_apply' => 'FreshRSS 将更新到 <strong>版本 %s</strong>。',
'error' => '更新出错:%s',
- 'file_is_nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'file_is_nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须拥有写入权限。',
'finished' => '更新完成!',
'none' => '没有可用更新',
- 'server_not_found' => '找不到更新服务器 [%s]',
+ 'server_not_found' => '找不到更新服务器。 [%s]',
),
'user' => array(
'created' => array(
diff --git a/app/i18n/zh-cn/gen.php b/app/i18n/zh-cn/gen.php
index 2b2249db5..d4999e5b0 100644
--- a/app/i18n/zh-cn/gen.php
+++ b/app/i18n/zh-cn/gen.php
@@ -12,7 +12,7 @@
return array(
'action' => array(
- 'actualize' => '更新提要',
+ 'actualize' => '更新订阅源',
'add' => '添加',
'back' => '← 返回',
'back_to_rss_feeds' => '← 返回订阅源',
@@ -26,7 +26,7 @@ return array(
'export' => '导出',
'filter' => '过滤',
'import' => '导入',
- 'load_default_shortcuts' => '重置快捷键',
+ 'load_default_shortcuts' => '加载默认快捷键',
'manage' => '管理',
'mark_read' => '标记已读',
'open_url' => '打开链接',
@@ -38,7 +38,7 @@ return array(
'see_website' => '网站中查看',
'submit' => '提交',
'truncate' => '删除所有文章',
- 'update' => '更新订阅',
+ 'update' => '更新',
),
'auth' => array(
'accept_tos' => '我接受 <a href="%s">服务条款</a>',
@@ -127,7 +127,7 @@ return array(
'js' => array(
'category_empty' => '清空分类',
'confirm_action' => '你确定要执行此操作吗?这将不可撤销!',
- 'confirm_action_feed_cat' => '你确定要执行此操作吗?你将丢失相关的收藏和自定义查询。这将不可撤销!',
+ 'confirm_action_feed_cat' => '你确定要执行此操作吗?你将丢失相关的收藏和自定义查询,这将不可撤销!',
'feedback' => array(
'body_new_articles' => 'FreshRSS 中有 %%d 篇文章等待阅读。',
'body_unread_articles' => '(未读: %%d)',
@@ -174,13 +174,14 @@ return array(
'queries' => '自定义查询',
'reading' => '阅读',
'search' => '搜索内容或#标签',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => '分享',
'shortcuts' => '快捷键',
'stats' => '统计',
'system' => '系统配置',
'update' => '更新',
'user_management' => '用户管理',
- 'user_profile' => '用户帐户',
+ 'user_profile' => '帐户',
),
'period' => array(
'days' => '天',
@@ -191,12 +192,14 @@ return array(
),
'share' => array(
'Known' => '基于 Known 的站点',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => '剪贴板',
'diaspora' => 'Diaspora*', // IGNORE
- 'email' => '邮箱', // IGNORE
- 'facebook' => '脸书', // IGNORE
+ 'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - 兼容 Firefox)',
+ 'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
'lemmy' => 'Lemmy', // IGNORE
@@ -211,10 +214,10 @@ return array(
'raindrop' => 'Raindrop.io', // IGNORE
'reddit' => 'Reddit', // IGNORE
'shaarli' => 'Shaarli', // IGNORE
- 'twitter' => '推特', // IGNORE
+ 'twitter' => 'Twitter', // IGNORE
'wallabag' => 'Wallabag v1', // IGNORE
'wallabagv2' => 'Wallabag v2', // IGNORE
- 'web-sharing-api' => 'Web分享',
+ 'web-sharing-api' => '系统分享',
'whatsapp' => 'Whatsapp', // IGNORE
'xing' => 'Xing', // IGNORE
),
diff --git a/app/i18n/zh-cn/index.php b/app/i18n/zh-cn/index.php
index 916140107..59d9ffb87 100644
--- a/app/i18n/zh-cn/index.php
+++ b/app/i18n/zh-cn/index.php
@@ -17,7 +17,7 @@ return array(
'bugs_reports' => '报告错误',
'credits' => '致谢',
'credits_content' => '某些设计元素来自于 <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> ,尽管 FreshRSS 并没有使用此框架。<a href="https://gitlab.gnome.org/Archive/gnome-icon-theme-symbolic">图标</a> 来自于 <a href="https://www.gnome.org/">GNOME 项目</a>。<em>Open Sans</em> 字体出自 <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> 之手。FreshRSS 基于 PHP 框架 <a href="https://framagit.org/marienfressinaud/MINZ">Minz</a>。',
- 'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 或 <a href="https://github.com/LeedRSS/Leed">Leed</a>。 它不仅轻快又易用,而且强大又易于配置。',
+ 'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 或 <a href="https://github.com/LeedRSS/Leed">Leed</a>。 它不仅轻快易用,并且强大又易于配置。',
'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github Issues</a>',
'license' => '授权',
'project_website' => '项目网站',
@@ -25,8 +25,8 @@ return array(
'version' => '版本',
),
'feed' => array(
- 'add' => '你可以添加一些订阅源。',
- 'empty' => '暂时没有文章可显示。',
+ 'add' => '请添加一些订阅源。',
+ 'empty' => '没有文章可以显示。',
'rss_of' => '%s 的订阅源',
'title' => '首页',
'title_fav' => '收藏',
diff --git a/app/i18n/zh-cn/install.php b/app/i18n/zh-cn/install.php
index 8927674d2..1d9d61e38 100644
--- a/app/i18n/zh-cn/install.php
+++ b/app/i18n/zh-cn/install.php
@@ -21,7 +21,7 @@ return array(
'auth' => array(
'form' => '网页表单(传统方式, 依赖 JavaScript)',
'http' => 'HTTP(面向启用 HTTPS 的高级用户)',
- 'none' => '无认证(危险)',
+ 'none' => '无(危险)',
'password_form' => '密码<br /><small>(用于网页表单登录方式)</small>',
'password_format' => '至少 7 个字符',
'type' => '认证方式',
@@ -30,61 +30,61 @@ return array(
'_' => '数据库',
'conf' => array(
'_' => '数据库配置',
- 'ko' => '请验证你的数据库信息',
+ 'ko' => '验证你的数据库信息',
'ok' => '数据库配置已保存',
),
'host' => '主机',
- 'password' => '密码',
+ 'password' => '数据库密码',
'prefix' => '表前缀',
'type' => '数据库类型',
- 'username' => '用户名',
+ 'username' => '数据库用户名',
),
'check' => array(
'_' => '检查',
'already_installed' => '我们检测到 FreshRSS 已经安装!',
'cache' => array(
- 'nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>%s</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'cache 目录权限正常',
),
'ctype' => array(
'nok' => '找不到字符类型检测库(php-ctype)',
- 'ok' => '已找到字符类型检测库',
+ 'ok' => '已找到字符类型检测库(ctype)',
),
'curl' => array(
- 'nok' => '找不到 cURL 库(php-curl)',
+ 'nok' => '找不到 cURL 库(php-curl 包)',
'ok' => '已找到 cURL 库',
),
'data' => array(
- 'nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>%s</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'data 目录权限正常',
),
'dom' => array(
- 'nok' => '找不到用于浏览 DOM 的库(php-xml)',
+ 'nok' => '找不到用于浏览 DOM 的库(php-xml 包)',
'ok' => '已找到用于浏览 DOM 的库',
),
'favicons' => array(
- 'nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data/favicons</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'favicons 目录权限正常',
),
'fileinfo' => array(
- 'nok' => '找不到 PHP fileinfo 库(php-fileinfo)',
+ 'nok' => '找不到 PHP fileinfo 库(fileinfo 包)',
'ok' => '已找到 fileinfo 库',
),
'json' => array(
- 'nok' => '找不到推荐的 JSON 解析库',
- 'ok' => '已找到推荐的 JSON 解析库',
+ 'nok' => '找不到 JSON 扩展(php-json 包)',
+ 'ok' => '已找到 JSON 扩展',
),
'mbstring' => array(
- 'nok' => '找不到推荐的 Unicode 解析库(mbstring)',
- 'ok' => '已找到推荐的 Unicode 解析库',
+ 'nok' => '找不到推荐用于 Unicode 的 mbstring 库',
+ 'ok' => '已找到推荐用于 Unicode 的 mbstring 库',
),
'pcre' => array(
'nok' => '找不到正则表达式解析库(php-pcre)',
- 'ok' => '已找到正则表达式解析库',
+ 'ok' => '已找到正则表达式解析库(PCRE)',
),
'pdo' => array(
- 'nok' => '找不到 PDO 或支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
- 'ok' => '已找到 PDO 和支持的至少一种驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
+ 'nok' => '找不到 PDO 或其中一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
+ 'ok' => '已找到 PDO 和至少一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
),
'php' => array(
'nok' => '你的 PHP 版本为 %s,但 FreshRSS 最低需要 %s',
@@ -92,12 +92,12 @@ return array(
),
'reload' => '再检查一遍',
'tmp' => array(
- 'nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>%s</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => '缓存目录权限正常。',
),
'unknown_process_username' => '未知',
'users' => array(
- 'nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>%s</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'users 目录权限正常',
),
'xml' => array(
@@ -114,7 +114,7 @@ return array(
'fix_errors_before' => '请在继续下一步前修复错误',
'javascript_is_better' => '启用 JavaScript 会使 FreshRSS 工作得更好',
'js' => array(
- 'confirm_reinstall' => '重新安装 FreshRSS 将会重置之前的配置。你确定要继续吗?',
+ 'confirm_reinstall' => '重新安装 FreshRSS 将会重置之前的配置,你确定要继续吗?',
),
'language' => array(
'_' => '语言',
diff --git a/app/i18n/zh-cn/sub.php b/app/i18n/zh-cn/sub.php
index 4ad401329..5e6e570a9 100644
--- a/app/i18n/zh-cn/sub.php
+++ b/app/i18n/zh-cn/sub.php
@@ -16,9 +16,9 @@ return array(
'title' => 'API', // IGNORE
),
'bookmarklet' => array(
- 'documentation' => '拖动此书签到你的书签栏或者右键选择「收藏此链接」,然后在你想要订阅的页面上点击「订阅」按钮',
+ 'documentation' => '拖动此书签到你的书签栏或者右键选择「收藏此链接」,然后在你想要订阅的页面上点击「订阅」按钮。',
'label' => '订阅',
- 'title' => '书签应用',
+ 'title' => '书签',
),
'category' => array(
'_' => '分类',
@@ -26,18 +26,18 @@ return array(
'archiving' => '归档',
'dynamic_opml' => array(
'_' => '动态订阅',
- 'help' => '使用地址上的 <a href="http://opml.org/" target="_blank">OPML 文件</a> 中的订阅源填充这一分类',
+ 'help' => '使用 URL 上的 <a href="http://opml.org/" target="_blank">OPML 文件</a> 中的订阅源填充这一分类',
),
'empty' => '空分类',
'information' => '信息',
- 'opml_url' => 'OPML 地址',
+ 'opml_url' => 'OPML URL', // IGNORE
'position' => '显示位置',
'position_help' => '控制分类排列顺序',
'title' => '标题',
),
'feed' => array(
'accept_cookies' => '接受 Cookies',
- 'accept_cookies_help' => '允许提要服务器设置 Cookies(仅在请求期间存储在内存中)',
+ 'accept_cookies_help' => '允许订阅源服务器设置 Cookies(仅在请求期间存储在内存中)',
'add' => '添加订阅源',
'advanced' => '高级',
'archiving' => '归档',
@@ -77,8 +77,8 @@ return array(
'html_xpath' => array(
'_' => 'HTML + XPath (Web 抓取)',
'feed_title' => array(
- '_' => '提要标题',
- 'help' => '如 <code>//title</code> 或是静态字符串如 <code>"My custom feed"</code>',
+ '_' => '订阅源标题',
+ 'help' => '如 <code>//title</code> 或是静态字符串如: <code>"My custom feed"</code>',
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> 是为资深用户准备的标准查询语言,FreshRSS 用以实现 Web 抓取.',
'item' => array(
@@ -99,8 +99,8 @@ return array(
'help' => '例如 <code>descendant::img/@src</code>',
),
'item_timeFormat' => array(
- '_' => 'Custom date/time format', // TODO
- 'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>', // TODO
+ '_' => '自定义日期/时间格式',
+ 'help' => '可选项, 格式参见 <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> 例如 <code>d-m-Y H:i:s</code>',
),
'item_timestamp' => array(
'_' => '文章日期:',
@@ -111,7 +111,7 @@ return array(
'help' => '注意使用 <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath 轴</a> <code>descendant::</code>,例如 <code>descendant::h2</code>',
),
'item_uid' => array(
- '_' => '文章唯一标识',
+ '_' => '文章唯一 ID',
'help' => '可选,例如: <code>descendant::div/@data-uri</code>',
),
'item_uri' => array(
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath 定位:',
),
'rss' => 'RSS / Atom (默认)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => '清理缓存',
@@ -175,8 +176,8 @@ return array(
'export_opml' => '导出订阅源列表(OPML)',
'export_starred' => '导出你的收藏',
'feed_list' => '%s 文章列表',
- 'file_to_import' => '需要导入的文件<br />(OPML、JSON 或 ZIP)',
- 'file_to_import_no_zip' => '需要导入的文件<br />(OPML 或 JSON)',
+ 'file_to_import' => '需要导入的文件 <br />(OPML、JSON 或 ZIP)',
+ 'file_to_import_no_zip' => '需要导入的文件 <br />(OPML 或 JSON)',
'import' => '导入',
'starred_list' => '收藏文章列表',
'title' => '导入/导出',
diff --git a/app/i18n/zh-cn/user.php b/app/i18n/zh-cn/user.php
index 8b4d35a7f..8a096b985 100644
--- a/app/i18n/zh-cn/user.php
+++ b/app/i18n/zh-cn/user.php
@@ -13,21 +13,21 @@
return array(
'email' => array(
'feedback' => array(
- 'invalid' => '电子邮箱地址无效',
+ 'invalid' => '邮箱地址无效',
'required' => '必须填写邮箱地址',
),
'validation' => array(
- 'change_email' => '您可以在 <a href="%s">用户管理</a> 中变更您的邮箱地址',
- 'email_sent_to' => '我们已通过 <strong>%s</strong> 发送验证邮件给您,请按其中指示来验证邮箱地址。',
+ 'change_email' => '你可以在 <a href="%s">用户管理</a> 中变更你的邮箱地址',
+ 'email_sent_to' => '我们已通过 <strong>%s</strong> 发送验证邮件给你,请按其中指示来验证邮箱地址。',
'feedback' => array(
- 'email_failed' => '由于服务器配置错误,我们无法向您发送邮件。',
- 'email_sent' => '邮件已发送到您的邮箱中',
+ 'email_failed' => '由于服务器配置错误,我们无法向你发送邮件。',
+ 'email_sent' => '邮件已发送到你的邮箱中',
'error' => '邮箱地址无法通过验证',
'ok' => '邮箱地址已成功通过验证',
'unnecessary' => '该邮箱地址已被验证',
'wrong_token' => '由于令牌错误,邮箱地址无法通过验证。',
),
- 'need_to' => '您需要先验证邮箱地址才能使用 %s',
+ 'need_to' => '你需要先验证邮箱地址才能使用 %s',
'resend_email' => '重发邮件',
'title' => '验证邮箱地址',
),
@@ -35,8 +35,8 @@ return array(
'mailer' => array(
'email_need_validation' => array(
'body' => '%s,欢迎',
- 'title' => '您需要验证您的帐户',
- 'welcome' => '您已注册 %s 现在只需点击下方链接通过邮箱验证即可完成注册:',
+ 'title' => '你需要验证你的帐户',
+ 'welcome' => '你已注册 %s 现在只需点击下方链接通过邮箱验证即可完成注册:',
),
),
'password' => array(
@@ -44,7 +44,7 @@ return array(
),
'tos' => array(
'feedback' => array(
- 'invalid' => '您必须接受服务条款才能注册',
+ 'invalid' => '你必须接受服务条款才能注册',
),
),
'username' => array(
diff --git a/app/i18n/zh-tw/conf.php b/app/i18n/zh-tw/conf.php
index 15fabaa40..34439c01b 100644
--- a/app/i18n/zh-tw/conf.php
+++ b/app/i18n/zh-tw/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => '顯示',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => '底欄',
'display_authors' => '作者',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 通知超時時間',
),
'show_nav_buttons' => '顯示導航按鈕',
- 'theme' => '主題',
+ 'theme' => array(
+ '_' => '主題',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => '“%s” 主題不再可用,請選擇其他主題。',
'thumbnail' => array(
'label' => '縮圖',
@@ -57,6 +64,7 @@ return array(
'portrait' => '肖像',
'square' => '方塊',
),
+ 'timezone' => 'Time zone', // TODO
'title' => '顯示',
'width' => array(
'content' => '內容寬度',
diff --git a/app/i18n/zh-tw/gen.php b/app/i18n/zh-tw/gen.php
index 1dcd94eeb..3ef8bca44 100644
--- a/app/i18n/zh-tw/gen.php
+++ b/app/i18n/zh-tw/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => '自定義查詢',
'reading' => '閱讀',
'search' => '搜尋內容或#標簽',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => '分享',
'shortcuts' => '快捷鍵',
'stats' => '統計',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => '基於 Known 的站點',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => '剪貼板',
'diaspora' => 'Diaspora*', // IGNORE
'email' => '郵箱', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => '臉書', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/zh-tw/sub.php b/app/i18n/zh-tw/sub.php
index dddcb2661..8a255645d 100644
--- a/app/i18n/zh-tw/sub.php
+++ b/app/i18n/zh-tw/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath 定位:',
),
'rss' => 'RSS / Atom (默認)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => '清理暫存',
diff --git a/app/install.php b/app/install.php
index 9d0d855b8..3163367f4 100644
--- a/app/install.php
+++ b/app/install.php
@@ -283,11 +283,7 @@ function freshrss_already_installed() {
// A configuration file already exists, we try to load it.
$system_conf = null;
try {
- Minz_Configuration::register('system', $conf_path);
- /**
- * @var FreshRSS_SystemConfiguration $system_conf
- */
- $system_conf = Minz_Configuration::get('system');
+ $system_conf = FreshRSS_SystemConfiguration::init($conf_path);
} catch (Minz_FileNotExistException $e) {
return false;
}
@@ -295,7 +291,7 @@ function freshrss_already_installed() {
// ok, the global conf exists… but what about default user conf?
$current_user = $system_conf->default_user;
try {
- Minz_Configuration::register('user', join_path(USERS_PATH, $current_user, 'config.php'));
+ FreshRSS_UserConfiguration::init(USERS_PATH . '/' . $current_user . '/config.php');
} catch (Minz_FileNotExistException $e) {
return false;
}
@@ -449,7 +445,7 @@ function printStep1() {
<?php } else { ?>
<p class="alert alert-error"><?= _t('install.action.fix_errors_before') ?></p>
<a id="actualize" class="btn" href="./index.php?step=1" title="<?= _t('install.check.reload') ?>">
- <img class="icon" src="../themes/icons/refresh.svg" alt="🔃" />
+ <img class="icon" src="../themes/icons/refresh.svg" alt="🔃" loading="lazy" />
</a>
<?php } ?>
<?php
@@ -680,7 +676,7 @@ if (_t('gen.dir') === 'rtl') {
<div class="item title">
<div id="logo-wrapper">
<a href="./">
- <img class="logo" src="../themes/icons/FreshRSS-logo.svg" alt="">
+ <img class="logo" src="../themes/icons/FreshRSS-logo.svg" alt="" loading="lazy">
</a>
</div>
</div>
diff --git a/app/layout/aside_configure.phtml b/app/layout/aside_configure.phtml
index 5f1762834..03b8108f7 100644
--- a/app/layout/aside_configure.phtml
+++ b/app/layout/aside_configure.phtml
@@ -1,72 +1,90 @@
<nav class="nav nav-list aside" id="aside_feed">
<a class="toggle_aside" href="#close"><?= _i('close') ?></a>
-
+
<ul>
- <li class="nav-header"><?= _t('gen.menu.account') ?>: <?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8')?></li>
- <li class="item<?= Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'profile' ? ' active' : '' ?>">
- <a href="<?= _url('user', 'profile') ?>"><?= _t('gen.menu.user_profile') ?></a>
- </li>
- <li class="item">
- <a class="signout" href="<?= _url('auth', 'logout') ?>">
- <?php
- echo _t('gen.auth.logout'); ?> <?= _i('logout') ?></a>
- </li>
- <li class="nav-header"><?= _t('gen.menu.configuration') ?></li>
- <li class="item<?= Minz_Request::actionName() === 'display' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'display') ?>"><?= _t('gen.menu.display') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'reading' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'reading') ?>"><?= _t('gen.menu.reading') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'archiving' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'archiving') ?>"><?= _t('gen.menu.archiving') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'integration' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'integration') ?>"><?= _t('gen.menu.sharing') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'shortcut' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'shortcut') ?>"><?= _t('gen.menu.shortcuts') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'queries' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'queries') ?>"><?= _t('gen.menu.queries') ?></a>
- </li>
- <li class="item<?= Minz_Request::controllerName() === 'extension' ? ' active' : '' ?>">
- <a href="<?= _url('extension', 'index') ?>"><?= _t('gen.menu.extensions') ?></a>
+ <li class="item nav-section">
+ <div class="item nav-header"><?= _t('gen.menu.account') ?>: <?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8')?></div>
+ <ul>
+ <li class="item<?= Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'profile' ? ' active' : '' ?>">
+ <a href="<?= _url('user', 'profile') ?>"><?= _t('gen.menu.user_profile') ?></a>
+ </li>
+ <li class="item">
+ <a class="signout" href="<?= _url('auth', 'logout') ?>">
+ <?php
+ echo _t('gen.auth.logout'); ?> <?= _i('logout') ?></a>
+ </li>
+ </ul>
</li>
- <?php if (!FreshRSS_Auth::hasAccess('admin')) { ?>
- <li class="item<?= Minz_Request::actionName() === 'logs' ? ' active' : '' ?>">
- <a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a>
+
+ <li class="item nav-section">
+ <div class="item nav-header"><?= _t('gen.menu.configuration') ?></div>
+ <ul>
+ <li class="item<?= Minz_Request::actionName() === 'display' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'display') ?>"><?= _t('gen.menu.display') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'reading' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'reading') ?>"><?= _t('gen.menu.reading') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'archiving' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'archiving') ?>"><?= _t('gen.menu.archiving') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'integration' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'integration') ?>"><?= _t('gen.menu.sharing') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'shortcut' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'shortcut') ?>"><?= _t('gen.menu.shortcuts') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'queries' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'queries') ?>"><?= _t('gen.menu.queries') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::controllerName() === 'extension' ? ' active' : '' ?>">
+ <a href="<?= _url('extension', 'index') ?>"><?= _t('gen.menu.extensions') ?></a>
+ </li>
+ <?php if (!FreshRSS_Auth::hasAccess('admin')) { ?>
+ <li class="item<?= Minz_Request::actionName() === 'logs' ? ' active' : '' ?>">
+ <a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a>
+ </li>
+ <?php } ?>
+ <?= Minz_ExtensionManager::callHook('menu_configuration_entry') ?>
+ </ul>
</li>
- <?php } ?>
- <?= Minz_ExtensionManager::callHook('menu_configuration_entry') ?>
<?php if (FreshRSS_Auth::hasAccess('admin')) { ?>
- <li class="nav-header"><?= _t('gen.menu.admin') ?></li>
- <li class="item<?= Minz_Request::actionName() === 'system' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'system') ?>"><?= _t('gen.menu.system') ?></a>
- </li>
- <li class="item<?= Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'manage' ? ' active' : '' ?>">
- <a href="<?= _url('user', 'manage') ?>"><?= _t('gen.menu.user_management') ?></a>
- </li>
- <li class="item<?= Minz_Request::controllerName() === 'auth' ? ' active' : '' ?>">
- <a href="<?= _url('auth', 'index') ?>"><?= _t('gen.menu.authentication') ?></a>
- </li>
- <li class="item<?= Minz_Request::controllerName() === 'update' && Minz_Request::actionName() === 'checkInstall' ? ' active' : '' ?>">
- <a href="<?= _url('update', 'checkInstall') ?>"><?= _t('gen.menu.check_install') ?></a>
- </li>
- <?php if (!Minz_Configuration::get('system')->disable_update) { ?>
- <li class="item<?= Minz_Request::controllerName() === 'update' && Minz_Request::actionName() === 'index' ? ' active' : '' ?>">
- <a href="<?= _url('update', 'index') ?>"><?= _t('gen.menu.update') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'logs' ? ' active' : '' ?>">
- <a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a>
+ <li class="item nav-section">
+ <div class="item nav-header"><?= _t('gen.menu.admin') ?></div>
+ <ul>
+ <li class="item<?= Minz_Request::actionName() === 'system' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'system') ?>"><?= _t('gen.menu.system') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'manage' ? ' active' : '' ?>">
+ <a href="<?= _url('user', 'manage') ?>"><?= _t('gen.menu.user_management') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::controllerName() === 'auth' ? ' active' : '' ?>">
+ <a href="<?= _url('auth', 'index') ?>"><?= _t('gen.menu.authentication') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::controllerName() === 'update' && Minz_Request::actionName() === 'checkInstall' ? ' active' : '' ?>">
+ <a href="<?= _url('update', 'checkInstall') ?>"><?= _t('gen.menu.check_install') ?></a>
+ </li>
+ <?php if (!FreshRSS_Context::$system_conf->disable_update) { ?>
+ <li class="item<?= Minz_Request::controllerName() === 'update' && Minz_Request::actionName() === 'index' ? ' active' : '' ?>">
+ <a href="<?= _url('update', 'index') ?>"><?= _t('gen.menu.update') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'logs' ? ' active' : '' ?>">
+ <a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a>
+ </li>
+ <?php } ?>
+ <?= Minz_ExtensionManager::callHook('menu_admin_entry') ?>
+ </ul>
</li>
<?php } ?>
- <?= Minz_ExtensionManager::callHook('menu_admin_entry') ?>
- <?php } ?>
- <li class="nav-header"><!-- empty headline --></li>
- <li class="item<?= Minz_Request::actionName() === 'about' ? ' active' : '' ?>">
- <a href="<?= _url('index', 'about') ?>"><?= _t('gen.menu.about') ?></a>
+
+ <li class="item nav-section">
+ <div class="item nav-header"><!-- empty headline --></div>
+ <ul>
+ <li class="item<?= Minz_Request::actionName() === 'about' ? ' active' : '' ?>">
+ <a href="<?= _url('index', 'about') ?>"><?= _t('gen.menu.about') ?></a>
+ </li>
+ </ul>
</li>
</ul>
</nav>
diff --git a/app/layout/aside_feed.phtml b/app/layout/aside_feed.phtml
index 3c4f1ec2e..bb9d678dc 100644
--- a/app/layout/aside_feed.phtml
+++ b/app/layout/aside_feed.phtml
@@ -37,7 +37,7 @@
<li class="tree-folder category favorites<?= FreshRSS_Context::isCurrentGet('s') ? ' active' : '' ?>">
<div class="tree-folder-title">
- <?= _i('bookmark') ?>
+ <?= _i('starred') ?>
<a class="title" data-unread="<?= format_number(FreshRSS_Context::$total_starred['unread']) ?>" href="<?= _url('index', $actual_view, 'get', 's') . $state_filter_manual ?>">
<?= _t('index.menu.favorites', format_number(FreshRSS_Context::$total_starred['all'])) ?>
</a>
diff --git a/app/layout/aside_subscription.phtml b/app/layout/aside_subscription.phtml
index aa7857f74..e1f520f34 100644
--- a/app/layout/aside_subscription.phtml
+++ b/app/layout/aside_subscription.phtml
@@ -1,38 +1,45 @@
<nav class="nav nav-list aside" id="aside_feed">
<a class="toggle_aside" href="#close"><?= _i('close') ?></a>
<ul>
- <li class="nav-header"><?= _t('sub.menu.subscription_management') ?></li>
+ <li class="item nav-section">
+ <div class="nav-header"><?= _t('sub.menu.subscription_management') ?></div>
+ <ul>
+ <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'add' ? ' active' : '' ?>">
+ <a href="<?= _url('subscription', 'add') ?>"><?= _t('sub.menu.add') ?></a>
+ </li>
- <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'add' ? ' active' : '' ?>">
- <a href="<?= _url('subscription', 'add') ?>"><?= _t('sub.menu.add') ?></a>
- </li>
+ <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'index' ? ' active' : '' ?>">
+ <a href="<?= _url('subscription', 'index') ?>"><?= _t('sub.menu.subscription_management') ?></a>
+ </li>
- <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'index' ? ' active' : '' ?>">
- <a href="<?= _url('subscription', 'index') ?>"><?= _t('sub.menu.subscription_management') ?></a>
- </li>
+ <li class="item<?= Minz_Request::controllerName() === 'tag' ? ' active' : '' ?>">
+ <a href="<?= _url('tag', 'index') ?>"><?= _t('sub.menu.label_management') ?></a>
+ </li>
- <li class="item<?= Minz_Request::controllerName() === 'tag' ? ' active' : '' ?>">
- <a href="<?= _url('tag', 'index') ?>"><?= _t('sub.menu.label_management') ?></a>
- </li>
+ <li class="item<?= Minz_Request::controllerName() === 'importExport' ? ' active' : '' ?>">
+ <a href="<?= _url('importExport', 'index') ?>"><?= _t('sub.menu.import_export') ?></a>
+ </li>
- <li class="item<?= Minz_Request::controllerName() === 'importExport' ? ' active' : '' ?>">
- <a href="<?= _url('importExport', 'index') ?>"><?= _t('sub.menu.import_export') ?></a>
+ <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'bookmarklet' ? ' active' : '' ?>">
+ <a href="<?= _url('subscription', 'bookmarklet') ?>"><?= _t('sub.menu.subscription_tools') ?></a>
+ </li>
+ </ul>
</li>
- <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'bookmarklet' ? ' active' : '' ?>">
- <a href="<?= _url('subscription', 'bookmarklet') ?>"><?= _t('sub.menu.subscription_tools') ?></a>
- </li>
- <li class="nav-header"><?= _t('admin.stats') ?></li>
- <li class="item<?= Minz_Request::controllerName() == 'stats' && Minz_Request::actionName() == 'index' ? ' active' : '' ?>">
- <a href="<?= _url('stats', 'index') ?>"><?= _t('sub.menu.stats.main') ?></a>
+ <li class="item nav-section">
+ <div class="nav-header"><?= _t('admin.stats') ?></div>
+ <ul>
+ <li class="item<?= Minz_Request::controllerName() == 'stats' && Minz_Request::actionName() == 'index' ? ' active' : '' ?>">
+ <a href="<?= _url('stats', 'index') ?>"><?= _t('sub.menu.stats.main') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() == 'idle' ? ' active' : '' ?>">
+ <a href="<?= _url('stats', 'idle') ?>"><?= _t('sub.menu.stats.idle') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() == 'repartition' ? ' active' : '' ?>">
+ <a href="<?= _url('stats', 'repartition') ?>"><?= _t('sub.menu.stats.repartition') ?></a>
+ </li>
+ </ul>
</li>
- <li class="item<?= Minz_Request::actionName() == 'idle' ? ' active' : '' ?>">
- <a href="<?= _url('stats', 'idle') ?>"><?= _t('sub.menu.stats.idle') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() == 'repartition' ? ' active' : '' ?>">
- <a href="<?= _url('stats', 'repartition') ?>"><?= _t('sub.menu.stats.repartition') ?></a>
- </li>
-
</ul>
</nav>
<a class="close-aside" href="#close">❌</a>
diff --git a/app/layout/header.phtml b/app/layout/header.phtml
index f8e54c7ce..0a49d5992 100644
--- a/app/layout/header.phtml
+++ b/app/layout/header.phtml
@@ -2,7 +2,7 @@
<div class="item title">
<a href="<?= _url('index', 'index') ?>">
<?php if (FreshRSS_Context::$system_conf->logo_html == '') { ?>
- <img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" />
+ <img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" loading="lazy" />
<?php
} else {
echo FreshRSS_Context::$system_conf->logo_html;
@@ -15,10 +15,15 @@
<?php if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::$system_conf->allow_anonymous) { ?>
<form action="<?= _url('index', 'index') ?>" method="get">
<div class="stick">
- <input type="search" name="search" id="search" class="extend"
+ <input type="search" name="search" id="search"
value="<?= htmlspecialchars(htmlspecialchars_decode(FreshRSS_Context::$search, ENT_QUOTES), ENT_COMPAT, 'UTF-8') ?>"
placeholder="<?= _t('gen.menu.search') ?>" />
+ <?php $param_a = Minz_Request::actionName(); ?>
+ <?php if (in_array($param_a, ['normal', 'global', 'reader'])) { ?>
+ <input type="hidden" name="a" value="<?= $param_a ?>" />
+ <?php } ?>
+
<?php $get = Minz_Request::param('get', ''); ?>
<?php if ($get != '') { ?>
<input type="hidden" name="get" value="<?= $get ?>" />
@@ -48,38 +53,59 @@
<ul class="dropdown-menu scrollbar-thin">
<li class="dropdown-header-close"><a class="toggle_aside" href="#close"><?= _i('close') ?></a></li>
- <li class="dropdown-header"><?= _t('gen.menu.account') ?>: <?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8') ?></li>
- <li class="item"><a href="<?= _url('user', 'profile') ?>"><?= _t('gen.menu.user_profile') ?></a></li>
- <?php if (FreshRSS_Auth::accessNeedsAction()): ?>
- <li class="item"><a class="signout" href="<?= _url('auth', 'logout') ?>"><?= _t('gen.auth.logout'); ?><?= _i('logout') ?></a></li>
- <?php else: ?>
- <li class="item"><span class="signout">(<?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8') ?>)</span></li>
- <?php endif; ?>
- <li class="dropdown-header"><?= _t('gen.menu.configuration') ?></li>
- <li class="item"><a href="<?= _url('configure', 'display') ?>"><?= _t('gen.menu.display') ?></a></li>
- <li class="item"><a href="<?= _url('configure', 'reading') ?>"><?= _t('gen.menu.reading') ?></a></li>
- <li class="item"><a href="<?= _url('configure', 'archiving') ?>"><?= _t('gen.menu.archiving') ?></a></li>
- <li class="item"><a href="<?= _url('configure', 'integration') ?>"><?= _t('gen.menu.sharing') ?></a></li>
- <li class="item"><a href="<?= _url('configure', 'shortcut') ?>"><?= _t('gen.menu.shortcuts') ?></a></li>
- <li class="item"><a href="<?= _url('configure', 'queries') ?>"><?= _t('gen.menu.queries') ?></a></li>
- <li class="item"><a href="<?= _url('extension', 'index') ?>"><?= _t('gen.menu.extensions') ?></a></li>
- <?= Minz_ExtensionManager::callHook('menu_configuration_entry') ?>
-
+ <li class="item dropdown-section">
+ <div class="dropdown-section-title">
+ <?= _t('gen.menu.account') ?>: <?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8') ?>
+ </div>
+ <ul>
+ <li class="item"><a href="<?= _url('user', 'profile') ?>"><?= _t('gen.menu.user_profile') ?></a></li>
+ <?php if (FreshRSS_Auth::accessNeedsAction()): ?>
+ <li class="item"><a class="signout" href="<?= _url('auth', 'logout') ?>"><?= _t('gen.auth.logout'); ?><?= _i('logout') ?></a></li>
+ <?php else: ?>
+ <li class="item"><span class="signout">(<?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8') ?>)</span></li>
+ <?php endif; ?>
+ </ul>
+ </li>
+ <li class="item dropdown-section">
+ <div class="dropdown-section-title">
+ <?= _t('gen.menu.configuration') ?>
+ </div>
+ <ul>
+ <li class="item"><a href="<?= _url('configure', 'display') ?>"><?= _t('gen.menu.display') ?></a></li>
+ <li class="item"><a href="<?= _url('configure', 'reading') ?>"><?= _t('gen.menu.reading') ?></a></li>
+ <li class="item"><a href="<?= _url('configure', 'archiving') ?>"><?= _t('gen.menu.archiving') ?></a></li>
+ <li class="item"><a href="<?= _url('configure', 'integration') ?>"><?= _t('gen.menu.sharing') ?></a></li>
+ <li class="item"><a href="<?= _url('configure', 'shortcut') ?>"><?= _t('gen.menu.shortcuts') ?></a></li>
+ <li class="item"><a href="<?= _url('configure', 'queries') ?>"><?= _t('gen.menu.queries') ?></a></li>
+ <li class="item"><a href="<?= _url('extension', 'index') ?>"><?= _t('gen.menu.extensions') ?></a></li>
+ <?= Minz_ExtensionManager::callHook('menu_configuration_entry') ?>
+ </ul>
+ </li>
<?php if (FreshRSS_Auth::hasAccess('admin')) { ?>
- <li class="dropdown-header"><?= _t('gen.menu.admin') ?></li>
- <li class="item"><a href="<?= _url('configure', 'system') ?>"><?= _t('gen.menu.system') ?></a></li>
- <li class="item"><a href="<?= _url('user', 'manage') ?>"><?= _t('gen.menu.user_management') ?></a></li>
- <li class="item"><a href="<?= _url('auth', 'index') ?>"><?= _t('gen.menu.authentication') ?></a></li>
- <li class="item"><a href="<?= _url('update', 'checkInstall') ?>"><?= _t('gen.menu.check_install') ?></a></li>
- <?php if (!Minz_Configuration::get('system')->disable_update) { ?>
- <li class="item"><a href="<?= _url('update', 'index') ?>"><?= _t('gen.menu.update') ?></a></li>
- <?php } ?>
- <?= Minz_ExtensionManager::callHook('menu_admin_entry') ?>
+ <li class="item dropdown-section">
+ <div class="dropdown-section-title">
+ <?= _t('gen.menu.admin') ?>
+ </div>
+ <ul>
+ <li class="item"><a href="<?= _url('configure', 'system') ?>"><?= _t('gen.menu.system') ?></a></li>
+ <li class="item"><a href="<?= _url('user', 'manage') ?>"><?= _t('gen.menu.user_management') ?></a></li>
+ <li class="item"><a href="<?= _url('auth', 'index') ?>"><?= _t('gen.menu.authentication') ?></a></li>
+ <li class="item"><a href="<?= _url('update', 'checkInstall') ?>"><?= _t('gen.menu.check_install') ?></a></li>
+ <?php if (!FreshRSS_Context::$system_conf->disable_update) { ?>
+ <li class="item"><a href="<?= _url('update', 'index') ?>"><?= _t('gen.menu.update') ?></a></li>
+ <?php } ?>
+ <?= Minz_ExtensionManager::callHook('menu_admin_entry') ?>
+ </ul>
+ </li>
<?php } ?>
- <li class="item"><a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a></li>
- <li class="item"><a href="<?= _url('index', 'about') ?>"><?= _t('gen.menu.about') ?></a></li>
- <?= Minz_ExtensionManager::callHook('menu_other_entry') ?>
+ <li class="item dropdown-section">
+ <ul>
+ <li class="item"><a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a></li>
+ <li class="item"><a href="<?= _url('index', 'about') ?>"><?= _t('gen.menu.about') ?></a></li>
+ <?= Minz_ExtensionManager::callHook('menu_other_entry') ?>
+ </ul>
+ </li>
</ul>
<a class="dropdown-close" href="#close">❌</a>
</div>
diff --git a/app/layout/layout.phtml b/app/layout/layout.phtml
index 48ada7183..1e9ce6905 100644
--- a/app/layout/layout.phtml
+++ b/app/layout/layout.phtml
@@ -52,7 +52,7 @@ if (_t('gen.dir') === 'rtl') {
<meta name="robots" content="noindex,nofollow" />
<?php } ?>
</head>
- <body class="<?= Minz_Request::actionName() ?>">
+ <body class="<?= Minz_Request::actionName() ?><?= (FreshRSS_Context::$user_conf->darkMode === 'no') ? '' : ' darkMode_' . FreshRSS_Context::$user_conf->darkMode ?>">
<?php
if (!Minz_Request::param('ajax')) {
flush();
diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml
index e8c4170c9..848144568 100644
--- a/app/layout/nav_menu.phtml
+++ b/app/layout/nav_menu.phtml
@@ -29,10 +29,53 @@
href="<?= Minz_Url::display($url_state) ?>"><?= _i($state_str) ?></a>
<?php } ?>
+ <div class="dropdown only-mobile" id="dropdown-search-wrapper">
+ <input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
+ <div id="dropdown-search" class="dropdown-target"></div>
+
+ <a id="toggle-search" class="dropdown-toggle btn<?= (strlen(FreshRSS_Context::$search) > 0) ? ' active' : ''; ?>" title="<?= _t('gen.menu.search') ?>"
+ href="#dropdown-search"><?= _i('search') ?></a>
+ <ul class="dropdown-menu">
+ <li class="item">
+ <span>
+ <form action="<?= _url('index', 'index') ?>" method="get">
+ <?php $param_a = Minz_Request::actionName(); ?>
+ <?php if (in_array($param_a, ['normal', 'global', 'reader'])) { ?>
+ <input type="hidden" name="a" value="<?= $param_a ?>" />
+ <?php } ?>
+
+ <?php $get = Minz_Request::param('get', ''); ?>
+ <?php if ($get != '') { ?>
+ <input type="hidden" name="get" value="<?= $get ?>" />
+ <?php } ?>
+
+ <?php $order = Minz_Request::param('order', ''); ?>
+ <?php if ($order != '') { ?>
+ <input type="hidden" name="order" value="<?= $order ?>" />
+ <?php } ?>
+
+ <?php $state = Minz_Request::param('state', ''); ?>
+ <?php if ($state != '') { ?>
+ <input type="hidden" name="state" value="<?= $state ?>" />
+ <?php } ?>
+
+ <div class="stick search">
+ <input type="search" name="search"
+ value="<?= htmlspecialchars(htmlspecialchars_decode(FreshRSS_Context::$search, ENT_QUOTES), ENT_COMPAT, 'UTF-8'); ?>"
+ placeholder="<?= _t('gen.menu.search') ?>" title="<?= _t('gen.menu.search') ?>" /><button class="btn" type="submit" title="<?= _t('index.menu.search_short') ?>"><?= _i('search') ?></button>
+ </div>
+ <p class="help"><?= _i('help') ?> <?= _t('gen.menu.search_help') ?></a></p>
+ </form>
+ </span>
+ </li>
+ </ul>
+ <a class="dropdown-close" href="#close">❌</a>
+ </div>
+
<div class="dropdown">
<div id="dropdown-query" class="dropdown-target"></div>
- <a class="dropdown-toggle btn" href="#dropdown-query" title="<?= _t('index.menu.queries') ?>"><?= _i('bookmark-tag') ?></a>
+ <a id="toggle-userqueries" class="dropdown-toggle btn" href="#dropdown-query" title="<?= _t('index.menu.queries') ?>"><?= _i('bookmark-tag') ?></a>
<ul class="dropdown-menu">
<li class="dropdown-header">
<?= _t('index.menu.queries') ?>
@@ -185,28 +228,6 @@
</div>
<?php } ?>
- <div class="item search">
- <form action="<?= _url('index', 'index') ?>" method="get">
- <input type="search" name="search" class="extend" value="<?php
- echo htmlspecialchars(htmlspecialchars_decode(FreshRSS_Context::$search, ENT_QUOTES), ENT_COMPAT, 'UTF-8'); ?>" placeholder="<?= _t('index.menu.search_short') ?>" />
-
- <?php $get = Minz_Request::param('get', ''); ?>
- <?php if($get != '') { ?>
- <input type="hidden" name="get" value="<?= $get ?>" />
- <?php } ?>
-
- <?php $order = Minz_Request::param('order', ''); ?>
- <?php if($order != '') { ?>
- <input type="hidden" name="order" value="<?= $order ?>" />
- <?php } ?>
-
- <?php $state = Minz_Request::param('state', ''); ?>
- <?php if($state != '') { ?>
- <input type="hidden" name="state" value="<?= $state ?>" />
- <?php } ?>
- </form>
- </div>
-
<?php
if (FreshRSS_Context::$order === 'DESC') {
$order = 'ASC';
diff --git a/app/layout/simple.phtml b/app/layout/simple.phtml
index 8a2ee14bb..c9d209999 100644
--- a/app/layout/simple.phtml
+++ b/app/layout/simple.phtml
@@ -31,7 +31,7 @@
<div class="item title">
<a href="<?= _url('index', 'index') ?>">
<?php if (FreshRSS_Context::$system_conf->logo_html == '') { ?>
- <img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" />
+ <img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" loading="lazy" />
<?php
} else {
echo FreshRSS_Context::$system_conf->logo_html;
diff --git a/app/shares.php b/app/shares.php
index 8685dba5d..117cd4dce 100644
--- a/app/shares.php
+++ b/app/shares.php
@@ -26,6 +26,13 @@
*/
return array(
+ 'archiveORG' => array(
+ 'url' => 'https://web.archive.org/save/~LINK~',
+ 'transform' => array(),
+ 'help' => 'https://web.archive.org',
+ 'form' => 'simple',
+ 'method' => 'GET',
+ ),
'archivePH' => array(
'url' => 'https://archive.ph/submit/?url=~LINK~',
'transform' => array(),
@@ -61,6 +68,12 @@ return array(
'form' => 'simple',
'method' => 'GET',
),
+ 'email-webmail-firefox-fix' => array( // see https://github.com/FreshRSS/FreshRSS/issues/2666
+ 'url' => 'mailto:?subject=~TITLE~&amp;body=~LINK~',
+ 'transform' => array('rawurlencode'),
+ 'form' => 'simple',
+ 'method' => 'GET',
+ ),
'facebook' => array(
'url' => 'https://www.facebook.com/sharer.php?u=~LINK~&amp;t=~TITLE~',
'transform' => array('rawurlencode'),
@@ -88,7 +101,7 @@ return array(
'method' => 'GET',
),
'lemmy' => array(
- 'url' => '~URL~/create_post?url=~LINK~&name=~TITLE~',
+ 'url' => '~URL~/create_post?url=~LINK~&title=~TITLE~',
'transform' => array('rawurlencode'),
'help' => 'https://join-lemmy.org/',
'form' => 'advanced',
diff --git a/app/views/auth/register.phtml b/app/views/auth/register.phtml
index a56eff3ee..999b2406a 100644
--- a/app/views/auth/register.phtml
+++ b/app/views/auth/register.phtml
@@ -16,6 +16,18 @@
</div>
<div class="form-group">
+ <label for="new_user_timezone"><?= _t('conf.display.timezone') ?></label>
+ <select name="new_user_timezone" id="new_user_timezone">
+ <?php $timezones = array_merge([''], DateTimeZone::listIdentifiers()); ?>
+ <?php foreach ($timezones as $timezone): ?>
+ <option value="<?= $timezone ?>"<?= $timezone === '' ? ' selected="selected"' : '' ?>>
+ <?= $timezone == '' ? _t('gen.short.by_default') . ' (' . FreshRSS_Context::defaultTimeZone() . ')' : $timezone ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </div>
+
+ <div class="form-group">
<label for="new_user_name"><?= _t('gen.auth.username') ?></label>
<input id="new_user_name" name="new_user_name" type="text" size="16" required="required" autocomplete="off"
pattern="<?= FreshRSS_user_Controller::USERNAME_PATTERN ?>" autocapitalize="off" />
diff --git a/app/views/configure/display.phtml b/app/views/configure/display.phtml
index e3e18f5a0..44ca242ad 100644
--- a/app/views/configure/display.phtml
+++ b/app/views/configure/display.phtml
@@ -26,6 +26,25 @@
</div>
<div class="form-group">
+ <label class="group-name" for="language"><?= _t('conf.display.timezone') ?></label>
+ <div class="group-controls">
+ <select name="timezone" id="timezone" data-leave-validation="<?= FreshRSS_Context::$user_conf->timezone ?>">
+ <?php
+ $timezones = array_merge([''], DateTimeZone::listIdentifiers());
+ if (!in_array(FreshRSS_Context::$user_conf->timezone, $timezones, true)) {
+ FreshRSS_Context::$user_conf->timezone = '';
+ }
+ ?>
+ <?php foreach ($timezones as $timezone): ?>
+ <option value="<?= $timezone ?>"<?= FreshRSS_Context::$user_conf->timezone === $timezone ? ' selected="selected"' : '' ?>>
+ <?= $timezone == '' ? _t('gen.short.by_default') . ' (' . FreshRSS_Context::defaultTimeZone() . ')' : $timezone ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group">
<label class="group-name" for="theme"><?= _t('conf.display.theme') ?></label>
<div class="group-controls">
<ul class="slides">
@@ -41,7 +60,7 @@
data-leave-validation="<?= (FreshRSS_Context::$user_conf->theme === $theme['id']) ? 1 : 0 ?>" />
<li class="slide-container">
<div class="slide">
- <img src="<?= Minz_Url::display('/themes/' . $theme['id'] . '/thumbs/original.png') ?>" />
+ <img src="<?= Minz_Url::display('/themes/' . $theme['id'] . '/thumbs/original.png') ?>" loading="lazy" />
</div>
<div class="nav">
<?php if ($i !== 1) {?>
@@ -52,8 +71,18 @@
<?php } ?>
</div>
<div class="properties">
- <div><?= sprintf('%s — %s %s', $theme['name'], _t('gen.short.by_author'), $theme['author']) ?></div>
- <div><?= $theme['description'] ?></div>
+ <div>
+ <?php if (!empty($theme['deprecated'])) { ?>
+ <span class="deprecated error"><?= _t('conf.display.theme.deprecated') ?>:</span>
+ <?php } ?>
+ <?= sprintf('%s — %s %s', $theme['name'], _t('gen.short.by_author'), $theme['author']) ?>
+ </div>
+ <div>
+ <?php if (!empty($theme['deprecated'])) { ?>
+ <span class="deprecated"><?= _t('conf.display.theme.deprecated.description') ?></span><br />
+ <?php } ?>
+ <?= $theme['description'] ?>
+ </div>
<div class="page-number"><?= sprintf('%d/%d', $i, $slides) ?></div>
</div>
</li>
@@ -76,6 +105,16 @@
</div>
</div>
+ <div class="form-group">
+ <label class="group-name" for="darkMode"><?= _t('conf.display.darkMode') ?></label>
+ <div class="group-controls">
+ <select name="darkMode" id="darkMode" data-leave-validation="<?= FreshRSS_Context::$user_conf->darkMode ?>">
+ <option value="no"<?= FreshRSS_Context::$user_conf->darkMode === 'no' ? ' selected' : '' ?>>No</option>
+ <option value="auto"<?= FreshRSS_Context::$user_conf->darkMode === 'auto' ? ' selected' : '' ?>>Auto</option>
+ </select>
+ </div>
+ </div>
+
<?php $width = FreshRSS_Context::$user_conf->content_width; ?>
<div class="form-group">
<label class="group-name" for="content_width"><?= _t('conf.display.width.content') ?></label>
@@ -126,7 +165,7 @@
<tr>
<th> </th>
<th title="<?= _t('conf.shortcut.mark_read') ?>"><?= _i('read') ?></th>
- <th title="<?= _t('conf.shortcut.mark_favorite') ?>"><?= _i('bookmark') ?></th>
+ <th title="<?= _t('conf.shortcut.mark_favorite') ?>"><?= _i('starred') ?></th>
<th><?= _t('conf.display.icon.related_tags') ?></th>
<th><?= _t('conf.display.icon.sharing') ?></th>
<th><?= _t('conf.display.icon.summary') ?></th>
diff --git a/app/views/configure/integration.phtml b/app/views/configure/integration.phtml
index c078ae709..34c10b3c3 100644
--- a/app/views/configure/integration.phtml
+++ b/app/views/configure/integration.phtml
@@ -15,7 +15,7 @@
<template id="simple-share">
<formgroup class="group-share dragbox">
<legend draggable="true">##label##</legend>
- <input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" />
+ <input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" data-leave-validation="" />
<div class="form-group" id="group-share-##key##">
<label class="group-name" for="share_##key##_name"><?= _t('conf.sharing.share_name') ?></label>
<div class="group-controls">
@@ -32,7 +32,7 @@
<template id="advanced-share">
<formgroup class="group-share dragbox">
<legend draggable="true">##label##</legend>
- <input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" />
+ <input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" data-leave-validation="" />
<input type="hidden" id="share_##key##_method" name="share[##key##][method]" value="##method##" />
<input type="hidden" id="share_##key##_field" name="share[##key##][field]" value="##field##" />
<div class="form-group" id="group-share-##key##">
diff --git a/app/views/configure/system.phtml b/app/views/configure/system.phtml
index 94fe34b94..bf14cac5d 100644
--- a/app/views/configure/system.phtml
+++ b/app/views/configure/system.phtml
@@ -16,7 +16,7 @@
<div class="form-group">
<label class="group-name" for="instance-name"><?= _t('admin.system.instance-name') ?></label>
<div class="group-controls">
- <input type="text" class="extend" id="instance-name" name="instance-name" value="<?= FreshRSS_Context::$system_conf->title ?>"
+ <input type="text" id="instance-name" name="instance-name" value="<?= FreshRSS_Context::$system_conf->title ?>"
data-leave-validation="<?= FreshRSS_Context::$system_conf->title ?>"/>
</div>
</div>
@@ -24,7 +24,7 @@
<div class="form-group">
<label class="group-name" for="auto-update-url"><?= _t('admin.system.auto-update-url') ?></label>
<div class="group-controls">
- <input type="text" class="extend" id="auto-update-url" name="auto-update-url" value="<?= FreshRSS_Context::$system_conf->auto_update_url ?>"
+ <input type="text" id="auto-update-url" name="auto-update-url" value="<?= FreshRSS_Context::$system_conf->auto_update_url ?>"
data-leave-validation="<?= FreshRSS_Context::$system_conf->auto_update_url ?>"/>
</div>
</div>
diff --git a/app/views/feed/add.phtml b/app/views/feed/add.phtml
index 8f5594526..3461e6e61 100644
--- a/app/views/feed/add.phtml
+++ b/app/views/feed/add.phtml
@@ -67,7 +67,7 @@
<div class="form-group">
<label class="group-name" for="http_user"><?= _t('sub.feed.auth.username') ?></label>
<div class="group-controls">
- <input type="text" name="http_user" id="http_user" class="extend" value="<?= $auth['username'] ?>" autocomplete="off" />
+ <input type="text" name="http_user" id="http_user" value="<?= $auth['username'] ?>" autocomplete="off" />
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.auth.help') ?></p>
</div>
diff --git a/app/views/helpers/category/update.phtml b/app/views/helpers/category/update.phtml
index 171ee20a4..a0986ff04 100644
--- a/app/views/helpers/category/update.phtml
+++ b/app/views/helpers/category/update.phtml
@@ -16,7 +16,7 @@
<div class="form-group">
<label class="group-name" for="name"><?= _t('sub.category.title') ?></label>
<div class="group-controls">
- <input type="text" name="name" id="name" class="extend" value="<?= $this->category->name() ?>" <?php
+ <input type="text" name="name" id="name" value="<?= $this->category->name() ?>" <?php
//Disallow changing the name of the default category
echo $this->category->id() == FreshRSS_CategoryDAO::DEFAULTCATEGORYID ? 'disabled="disabled"' : '';
?> />
diff --git a/app/views/helpers/configure/query.phtml b/app/views/helpers/configure/query.phtml
index a75333cfc..f8d51c193 100644
--- a/app/views/helpers/configure/query.phtml
+++ b/app/views/helpers/configure/query.phtml
@@ -12,7 +12,7 @@
<div class="form-group">
<label class="group-name" for="name"><?= _t('conf.query.name') ?></label>
<div class="group-controls">
- <input type="text" name="name" id="name" class="extend" value="<?= $this->query->getName() ?>" />
+ <input type="text" name="name" id="name" value="<?= $this->query->getName() ?>" />
</div>
</div>
<legend><?= _t('conf.query.filter') ?></legend>
@@ -20,7 +20,7 @@
<div class="form-group">
<label class="group-name" for=""><?= _t('conf.query.filter.search') ?></label>
<div class="group-controls">
- <input type="text" id="query_search" name="query[search]" class="extend" value="<?= htmlspecialchars($this->query->getSearch(), ENT_COMPAT, 'UTF-8') ?>"/>
+ <input type="text" id="query_search" name="query[search]" value="<?= htmlspecialchars($this->query->getSearch(), ENT_COMPAT, 'UTF-8') ?>"/>
</div>
</div>
<div class="form-group">
diff --git a/app/views/helpers/export/opml.phtml b/app/views/helpers/export/opml.phtml
index d97641fd2..64c83c960 100644
--- a/app/views/helpers/export/opml.phtml
+++ b/app/views/helpers/export/opml.phtml
@@ -9,6 +9,7 @@ function feedsToOutlines($feeds, $excludeMutedFeeds = false): array {
if ($feed->mute() && $excludeMutedFeeds) {
continue;
}
+
$outline = [
'text' => htmlspecialchars_decode($feed->name(), ENT_QUOTES),
'type' => FreshRSS_Export_Service::TYPE_RSS_ATOM,
@@ -16,49 +17,65 @@ function feedsToOutlines($feeds, $excludeMutedFeeds = false): array {
'htmlUrl' => htmlspecialchars_decode($feed->website(), ENT_QUOTES),
'description' => htmlspecialchars_decode($feed->description(), ENT_QUOTES),
];
- if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH) {
- $outline['type'] = FreshRSS_Export_Service::TYPE_HTML_XPATH;
+
+ if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH || $feed->kind() === FreshRSS_Feed::KIND_XML_XPATH) {
+ switch ($feed->kind()) {
+ case FreshRSS_Feed::KIND_HTML_XPATH:
+ $outline['type'] = FreshRSS_Export_Service::TYPE_HTML_XPATH;
+ break;
+ case FreshRSS_Feed::KIND_XML_XPATH:
+ $outline['type'] = FreshRSS_Export_Service::TYPE_XML_XPATH;
+ break;
+ }
/** @var array<string,string> */
$xPathSettings = $feed->attributes('xpath');
- $outline['frss:xPathItem'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['item'] ?? null];
- $outline['frss:xPathItemTitle'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemTitle'] ?? null];
- $outline['frss:xPathItemContent'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemContent'] ?? null];
- $outline['frss:xPathItemUri'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemUri'] ?? null];
- $outline['frss:xPathItemAuthor'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemAuthor'] ?? null];
- $outline['frss:xPathItemTimestamp'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemTimestamp'] ?? null];
- $outline['frss:xPathItemTimeformat'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemTimeformat'] ?? null];
- $outline['frss:xPathItemThumbnail'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemThumbnail'] ?? null];
- $outline['frss:xPathItemCategories'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemCategories'] ?? null];
- $outline['frss:xPathItemUid'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemUid'] ?? null];
+ $outline['frss:xPathItem'] = $xPathSettings['item'] ?? null;
+ $outline['frss:xPathItemTitle'] = $xPathSettings['itemTitle'] ?? null;
+ $outline['frss:xPathItemContent'] = $xPathSettings['itemContent'] ?? null;
+ $outline['frss:xPathItemUri'] = $xPathSettings['itemUri'] ?? null;
+ $outline['frss:xPathItemAuthor'] = $xPathSettings['itemAuthor'] ?? null;
+ $outline['frss:xPathItemTimestamp'] = $xPathSettings['itemTimestamp'] ?? null;
+ $outline['frss:xPathItemTimeformat'] = $xPathSettings['itemTimeformat'] ?? null;
+ $outline['frss:xPathItemThumbnail'] = $xPathSettings['itemThumbnail'] ?? null;
+ $outline['frss:xPathItemCategories'] = $xPathSettings['itemCategories'] ?? null;
+ $outline['frss:xPathItemUid'] = $xPathSettings['itemUid'] ?? null;
}
+
if (!empty($feed->filtersAction('read'))) {
$filters = '';
foreach ($feed->filtersAction('read') as $filterRead) {
$filters .= $filterRead->getRawInput() . "\n";
}
$filters = trim($filters);
- $outline['frss:filtersActionRead'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $filters];
+ $outline['frss:filtersActionRead'] = $filters;
}
+
if ($feed->pathEntries() != '') {
- $outline['frss:cssFullContent'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => htmlspecialchars_decode($feed->pathEntries(), ENT_QUOTES)];
+ $outline['frss:cssFullContent'] = htmlspecialchars_decode($feed->pathEntries(), ENT_QUOTES);
}
+
if ($feed->attributes('path_entries_filter') != '') {
- $outline['frss:cssFullContentFilter'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $feed->attributes('path_entries_filter')];
+ $outline['frss:cssFullContentFilter'] = $feed->attributes('path_entries_filter');
}
+
$outlines[] = $outline;
}
+
return $outlines;
}
/** @var FreshRSS_View $this */
-$opml_array = array(
- 'head' => array(
+$opml_array = [
+ 'namespaces' => [
+ 'frss' => FreshRSS_Export_Service::FRSS_NAMESPACE,
+ ],
+ 'head' => [
'title' => FreshRSS_Context::$system_conf->title,
- 'dateCreated' => date('D, d M Y H:i:s')
- ),
- 'body' => array()
-);
+ 'dateCreated' => new DateTime(),
+ ],
+ 'body' => [],
+];
if (!empty($this->categories)) {
foreach ($this->categories as $key => $cat) {
@@ -66,9 +83,11 @@ if (!empty($this->categories)) {
'text' => htmlspecialchars_decode($cat->name(), ENT_QUOTES),
'@outlines' => feedsToOutlines($cat->feeds(), $this->excludeMutedFeeds),
];
+
if ($cat->kind() === FreshRSS_Category::KIND_DYNAMIC_OPML) {
- $outline['frss:opmlUrl'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $cat->attributes('opml_url')];;
+ $outline['frss:opmlUrl'] = $cat->attributes('opml_url');
}
+
$opml_array['body'][$key] = $outline;
}
}
@@ -77,4 +96,5 @@ if (!empty($this->feeds)) {
$opml_array['body'][] = feedsToOutlines($this->feeds, $this->excludeMutedFeeds);
}
-echo libopml_render($opml_array);
+$libopml = new \marienfressinaud\LibOpml\LibOpml(true);
+echo $libopml->render($opml_array);
diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml
index 8dbba0ab0..0cd2ec0c3 100644
--- a/app/views/helpers/feed/update.phtml
+++ b/app/views/helpers/feed/update.phtml
@@ -391,8 +391,9 @@
<label class="group-name" for="feed_kind"><?= _t('sub.feed.kind') ?></label>
<div class="group-controls">
<select name="feed_kind" id="feed_kind" class="select-show w100">
- <option value="<?= FreshRSS_Feed::KIND_RSS ?>" <?= $this->feed->kind() == FreshRSS_Feed::KIND_RSS ? 'selected="selected"' : '' ?>><?= _t('sub.feed.kind.rss') ?></option>
- <option value="<?= FreshRSS_Feed::KIND_HTML_XPATH ?>" <?= $this->feed->kind() == FreshRSS_Feed::KIND_HTML_XPATH ? 'selected="selected"' : '' ?> data-show="html_xpath"><?= _t('sub.feed.kind.html_xpath') ?></option>
+ <option value="<?= FreshRSS_Feed::KIND_RSS ?>" <?= $this->feed->kind() === FreshRSS_Feed::KIND_RSS ? 'selected="selected"' : '' ?>><?= _t('sub.feed.kind.rss') ?></option>
+ <option value="<?= FreshRSS_Feed::KIND_HTML_XPATH ?>" <?= $this->feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH ? 'selected="selected"' : '' ?> data-show="html_xpath"><?= _t('sub.feed.kind.html_xpath') ?></option>
+ <option value="<?= FreshRSS_Feed::KIND_XML_XPATH ?>" <?= $this->feed->kind() === FreshRSS_Feed::KIND_XML_XPATH ? 'selected="selected"' : '' ?> data-show="html_xpath"><?= _t('sub.feed.kind.xml_xpath') ?></option>
</select>
</div>
</div>
@@ -602,7 +603,7 @@
<div class="form-group">
<label class="group-name" for="timeout"><?= _t('sub.feed.timeout') ?></label>
<div class="group-controls">
- <input type="number" name="timeout" id="timeout" class="w50" min="3" max="120" value="<?= $this->feed->attributes('timeout') ?>" placeholder="<?= _t('gen.short.by_default') ?>" />
+ <input type="number" name="timeout" id="timeout" class="w50" min="3" max="900" value="<?= $this->feed->attributes('timeout') ?>" placeholder="<?= _t('gen.short.by_default') ?>" />
</div>
</div>
diff --git a/app/views/helpers/index/normal/entry_header.phtml b/app/views/helpers/index/normal/entry_header.phtml
index 43eeb7f8a..92eacf617 100644
--- a/app/views/helpers/index/normal/entry_header.phtml
+++ b/app/views/helpers/index/normal/entry_header.phtml
@@ -42,8 +42,7 @@
?><li class="item thumbnail <?= $topline_thumbnail ?> <?= $topline_summary ? '' : 'small' ?>"><?php
$thumbnail = $this->entry->thumbnail();
if ($thumbnail != null):
- ?><img src="<?= htmlspecialchars($thumbnail['url'], ENT_COMPAT, 'UTF-8') ?>" class="item-element "<?= $lazyload ? ' loading="lazy"' : '' ?><?=
- empty($thumbnail['alt']) ? '' : ' alt="' . htmlspecialchars(strip_tags($thumbnail['alt']), ENT_COMPAT, 'UTF-8') . '"' ?> /><?php
+ ?><img src="<?= $thumbnail['url'] ?>" class="item-element "<?= $lazyload ? ' loading="lazy"' : '' ?> alt="" /><?php
endif;
?></li><?php
endif; ?>
@@ -62,7 +61,7 @@
?></span><?php
endif;
if ($topline_summary):
- ?><div class="summary"><?= trim(mb_substr(strip_tags($this->entry->content()), 0, 500, 'UTF-8')) ?></div><?php
+ ?><div class="summary"><?= trim(mb_substr(strip_tags($this->entry->content(false)), 0, 500, 'UTF-8')) ?></div><?php
endif;
?></a></li>
<?php if ($topline_date) { ?><li class="item date"><time datetime="<?= $this->entry->machineReadableDate() ?>" class="item-element"><?= $this->entry->date() ?></time>&nbsp;</li><?php } ?>
diff --git a/app/views/helpers/stream-footer.phtml b/app/views/helpers/stream-footer.phtml
index ebce4d852..f37ba59d6 100755
--- a/app/views/helpers/stream-footer.phtml
+++ b/app/views/helpers/stream-footer.phtml
@@ -30,19 +30,20 @@ if ($hasAccess) { ?>
<?php if (FreshRSS_Context::$next_id) { ?>
<button id="load_more" type="submit" class="btn" formaction="<?= Minz_Url::display($url_next) ?>"><?= _t('gen.stream.load_more') ?></button>
<?php } elseif ($hasAccess) { ?>
+ <?= _t('gen.stream.nothing_to_load') ?><br />
<button id="bigMarkAsRead"
class="as-link <?= FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : '' ?>"
form="stream-footer"
formaction="<?= Minz_Url::display($url_mark_read) ?>"
type="submit">
- <?= _t('gen.stream.nothing_to_load') ?><br />
<span class="bigTick">✓</span><br />
- <?= _t('gen.stream.mark_all_read') ?>
+ <span class="markAllRead"><?= _t('gen.stream.mark_all_read') ?></span>
+ <?php if (FreshRSS_Context::$user_conf->onread_jump_next) { ?>
+ <div class="jumpNext"><?= _t('conf.reading.jump_next') ?></div>
+ <?php } ?>
</button>
<?php } else { ?>
- <div id="bigMarkAsRead">
- <?= _t('gen.stream.nothing_to_load') ?><br />
- </div>
+ <?= _t('gen.stream.nothing_to_load') ?><br />
<?php } ?>
</div>
<?php if ($hasAccess) { ?>
diff --git a/app/views/index/logs.phtml b/app/views/index/logs.phtml
index f6a76b922..896a19765 100644
--- a/app/views/index/logs.phtml
+++ b/app/views/index/logs.phtml
@@ -6,7 +6,7 @@
</div>
<h1><?= _t('index.log') ?></h1>
-
+
<?php
/** @var array<FreshRSS_Log> $items */
@@ -29,8 +29,8 @@
<?= _i($log->level()) ?>
</td>
<td class="log-date">
- <time datetime="<?= @date('Y-m-d H:i:s', @strtotime($log->date())) ?>">
- <?= @date('Y-m-d H:i:s', @strtotime($log->date())) ?>
+ <time datetime="<?= date('Y-m-d H:i:s', @strtotime($log->date()) ?: 0) ?>">
+ <?= date('Y-m-d H:i:s', @strtotime($log->date()) ?: 0) ?>
</time>
</td>
<td class="log-message">
@@ -42,7 +42,7 @@
</table>
</div>
<?php $this->logsPaginator->render('logs_pagination.phtml', 'page'); ?>
-
+
<form method="post" action="<?= _url('index', 'logs') ?>">
@@ -58,5 +58,5 @@
<?php } else { ?>
<p class="alert alert-warn"><?= _t('index.log.empty') ?></p>
<?php } ?>
-
+
</main>
diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml
index 6f7c47677..847c307ab 100644
--- a/app/views/index/normal.phtml
+++ b/app/views/index/normal.phtml
@@ -162,7 +162,7 @@ $today = @strtotime('today');
<?php } ?>
</header>
<div class="text"><?php
- echo $lazyload && $hidePosts ? lazyimg($this->entry->content()) : $this->entry->content();
+ echo $lazyload && $hidePosts ? lazyimg($this->entry->content(true)) : $this->entry->content(true);
?></div>
<?php
$display_authors_date = FreshRSS_Context::$user_conf->show_author_date === 'f' || FreshRSS_Context::$user_conf->show_author_date === 'b';
diff --git a/app/views/index/reader.phtml b/app/views/index/reader.phtml
index 5789f229b..9dcd07435 100644
--- a/app/views/index/reader.phtml
+++ b/app/views/index/reader.phtml
@@ -87,8 +87,8 @@ $MAX_TAGS_DISPLAYED = FreshRSS_Context::$user_conf->show_tags_max;
if (!empty($remainingTags)) { // more than 7 tags: show dropdown menu ?>
<li class="item tag">
<div class="dropdown">
- <div id="dropdown-tags2-<?= $this->entry->id() ?>" class="dropdown-target"></div>
- <a class="dropdown-toggle" href="#dropdown-tags2-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
+ <div id="dropdown-tags-<?= $this->entry->id() ?>" class="dropdown-target"></div>
+ <a class="dropdown-toggle" href="#dropdown-tags-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
<ul class="dropdown-menu">
<li class="dropdown-header"><?= _t('index.tag.related') ?></li>
<?php
@@ -136,7 +136,7 @@ $MAX_TAGS_DISPLAYED = FreshRSS_Context::$user_conf->show_tags_max;
</header>
<div class="text">
- <?= $item->content() ?>
+ <?= $item->content(true) ?>
</div>
<?php
$display_authors_date = FreshRSS_Context::$user_conf->show_author_date === 'f' || FreshRSS_Context::$user_conf->show_author_date === 'b';
diff --git a/app/views/index/rss.phtml b/app/views/index/rss.phtml
index 0b07a02f3..0b3dc7955 100755
--- a/app/views/index/rss.phtml
+++ b/app/views/index/rss.phtml
@@ -29,29 +29,41 @@ foreach ($this->entries as $item) {
$authors = $item->authors();
if (is_array($authors)) {
foreach ($authors as $author) {
- echo "\t\t\t" , '<dc:creator>', $author, '</dc:creator>', "\n";
+ echo "\t\t\t", '<dc:creator>', $author, '</dc:creator>', "\n";
}
}
$categories = $item->tags();
if (is_array($categories)) {
foreach ($categories as $category) {
- echo "\t\t\t" , '<category>', $category, '</category>', "\n";
+ echo "\t\t\t", '<category>', $category, '</category>', "\n";
}
}
+ $thumbnail = $item->thumbnail(false);
+ if (!empty($thumbnail['url'])) {
+ // https://www.rssboard.org/media-rss#media-thumbnails
+ echo "\t\t\t", '<media:thumbnail url="' . $thumbnail['url']
+ . (empty($thumbnail['width']) ? '' : '" width="' . $thumbnail['width'])
+ . (empty($thumbnail['height']) ? '' : '" height="' . $thumbnail['height'])
+ . (empty($thumbnail['time']) ? '' : '" time="' . $thumbnail['time'])
+ . '" />', "\n";
+ }
$enclosures = $item->enclosures(false);
if (is_array($enclosures)) {
foreach ($enclosures as $enclosure) {
// https://www.rssboard.org/media-rss
- echo "\t\t\t" , '<media:content url="' . $enclosure['url']
+ echo "\t\t\t", '<media:content url="' . $enclosure['url']
. (empty($enclosure['medium']) ? '' : '" medium="' . $enclosure['medium'])
. (empty($enclosure['type']) ? '' : '" type="' . $enclosure['type'])
. (empty($enclosure['length']) ? '' : '" length="' . $enclosure['length'])
- . '"></media:content>', "\n";
+ . '">'
+ . (empty($enclosure['title']) ? '' : '<media:title type="html">' . $enclosure['title'] . '</media:title>')
+ . (empty($enclosure['credit']) ? '' : '<media:credit>' . $enclosure['credit'] . '</media:credit>')
+ . '</media:content>', "\n";
}
}
?>
<description><![CDATA[<?php
- echo $item->content();
+ echo $item->content(false);
?>]]></description>
<pubDate><?= date('D, d M Y H:i:s O', $item->date(true)) ?></pubDate>
<guid isPermaLink="false"><?= $item->id() > 0 ? $item->id() : $item->guid() ?></guid>
diff --git a/app/views/subscription/add.phtml b/app/views/subscription/add.phtml
index 800093bed..4e9da877f 100644
--- a/app/views/subscription/add.phtml
+++ b/app/views/subscription/add.phtml
@@ -70,6 +70,7 @@
<select name="feed_kind" id="feed_kind" class="select-show">
<option value="<?= FreshRSS_Feed::KIND_RSS ?>" selected="selected"><?= _t('sub.feed.kind.rss') ?></option>
<option value="<?= FreshRSS_Feed::KIND_HTML_XPATH ?>" data-show="html_xpath"><?= _t('sub.feed.kind.html_xpath') ?></option>
+ <option value="<?= FreshRSS_Feed::KIND_XML_XPATH ?>" data-show="html_xpath"><?= _t('sub.feed.kind.xml_xpath') ?></option>
</select>
</div>
</div>
@@ -190,7 +191,7 @@
<div class="form-group">
<label class="group-name" for="curl_params_cookie"><?= _t('sub.feed.css_cookie') ?></label>
<div class="group-controls">
- <input type="text" name="curl_params_cookie" id="curl_params_cookie" class="extend" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
+ <input type="text" name="curl_params_cookie" id="curl_params_cookie" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.css_cookie_help') ?></p>
<label for="curl_params_cookiefile">
<input type="checkbox" name="curl_params_cookiefile" id="curl_params_cookiefile" value="1" />
@@ -203,7 +204,7 @@
<div class="form-group">
<label class="group-name" for="curl_params_redirects"><?= _t('sub.feed.max_http_redir') ?></label>
<div class="group-controls">
- <input type="number" name="curl_params_redirects" id="curl_params_redirects" class="extend" min="-1" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
+ <input type="number" name="curl_params_redirects" id="curl_params_redirects" min="-1" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.max_http_redir_help') ?></p>
</div>
</div>
@@ -212,7 +213,7 @@
<label class="group-name" for="curl_params_useragent"><?= _t('sub.feed.useragent') ?></label>
<div class="group-controls">
<div class="stick">
- <input type="text" name="curl_params_useragent" id="curl_params_useragent" class="extend" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
+ <input type="text" name="curl_params_useragent" id="curl_params_useragent" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
</div>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.useragent_help') ?></p>
</div>
@@ -228,7 +229,7 @@
?>
</select>
<div class="stick">
- <input type="text" name="curl_params" id="curl_params" class="extend" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
+ <input type="text" name="curl_params" id="curl_params" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
</div>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.proxy_help') ?></p>
</div>
@@ -237,7 +238,7 @@
<div class="form-group">
<label class="group-name" for="timeout"><?= _t('sub.feed.timeout') ?></label>
<div class="group-controls">
- <input type="number" name="timeout" id="timeout" min="3" max="120" value="" placeholder="<?= _t('gen.short.by_default') ?>" />
+ <input type="number" name="timeout" id="timeout" min="3" max="900" value="" placeholder="<?= _t('gen.short.by_default') ?>" />
</div>
</div>
diff --git a/app/views/user/manage.phtml b/app/views/user/manage.phtml
index 0d01d33d7..b996cdf2b 100644
--- a/app/views/user/manage.phtml
+++ b/app/views/user/manage.phtml
@@ -15,8 +15,6 @@
<input type="hidden" name="originController" value="<?= Minz_Request::controllerName() ?>" />
<input type="hidden" name="originAction" value="<?= Minz_Request::actionName() ?>" />
-
-
<div class="form-group">
<label class="group-name" for="new_user_language"><?= _t('admin.user.language') ?></label>
<div class="group-controls">
@@ -31,6 +29,20 @@
</div>
<div class="form-group">
+ <label class="group-name" for="new_user_timezone"><?= _t('conf.display.timezone') ?></label>
+ <div class="group-controls">
+ <select name="new_user_timezone" id="new_user_timezone">
+ <?php $timezones = array_merge([''], DateTimeZone::listIdentifiers()); ?>
+ <?php foreach ($timezones as $timezone): ?>
+ <option value="<?= $timezone ?>"<?= $timezone === '' ? ' selected="selected"' : '' ?>>
+ <?= $timezone == '' ? _t('gen.short.by_default') . ' (' . FreshRSS_Context::defaultTimeZone() . ')' : $timezone ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group">
<label class="group-name" for="new_user_name"><?= _t('admin.user.username') ?></label>
<div class="group-controls">
<input id="new_user_name" name="new_user_name" type="text" size="16" required="required" autocomplete="off"
diff --git a/cli/README.md b/cli/README.md
index e290cc267..cb43b7340 100644
--- a/cli/README.md
+++ b/cli/README.md
@@ -18,9 +18,7 @@ In any case, when you are done with a series of commands, you should re-apply th
```sh
cd /usr/share/FreshRSS
-sudo chown -R :www-data .
-sudo chmod -R g+r .
-sudo chmod -R g+w ./data/
+sudo cli/access-permissions.sh
```
diff --git a/cli/_cli.php b/cli/_cli.php
index 10a92385a..0d2c8695f 100644
--- a/cli/_cli.php
+++ b/cli/_cli.php
@@ -44,7 +44,7 @@ function cliInitUser($username) {
function accessRights() {
echo 'ℹ️ Remember to re-apply the appropriate access rights, such as:',
- "\t", 'sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/', "\n";
+ "\t", 'sudo cli/access-permissions.sh', "\n";
}
function done($ok = true) {
diff --git a/cli/access-permissions.sh b/cli/access-permissions.sh
new file mode 100755
index 000000000..c13130a4b
--- /dev/null
+++ b/cli/access-permissions.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+# Apply access permissions
+
+if [ ! -f './constants.php' ] || [ ! -d './cli/' ]; then
+ echo >&2 '⛔ It does not look like a FreshRSS directory; exiting!'
+ exit 2
+fi
+
+if [ "$(id -u)" -ne 0 ]; then
+ echo >&2 '⛔ Applying access permissions require running as root or sudo!'
+ exit 3
+fi
+
+# Based on group access
+chown -R :www-data .
+# Read files, and directory traversal
+chmod -R g+rX .
+# Write access
+chmod -R g+w ./data/
diff --git a/cli/i18n/I18nFile.php b/cli/i18n/I18nFile.php
index fca31d662..12a04c6a2 100644
--- a/cli/i18n/I18nFile.php
+++ b/cli/i18n/I18nFile.php
@@ -27,7 +27,7 @@ class I18nFile {
foreach ($i18n as $language => $file) {
$dir = I18N_PATH . DIRECTORY_SEPARATOR . $language;
if (!file_exists($dir)) {
- mkdir($dir);
+ mkdir($dir, 0770, true);
}
foreach ($file as $name => $content) {
$filename = $dir . DIRECTORY_SEPARATOR . $name;
diff --git a/cli/sensitive-log.sh b/cli/sensitive-log.sh
new file mode 100755
index 000000000..40309b0db
--- /dev/null
+++ b/cli/sensitive-log.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+# Strips sensitive passwords from (Apache) logs
+
+# For e.g. GNU systems such as Debian
+# N.B.: `sed -u` is not available in BusyBox and without it there are buffering delays (even with stdbuf)
+sed -Eu 's/([?&])(Passwd|token)=[^& \t]+/\1\2=redacted/ig' 2>/dev/null ||
+
+ # For systems with gawk (not available by default in Docker of Debian or Alpine) or with BuzyBox such as Alpine
+ $(which gawk || which awk) -v IGNORECASE=1 '{ print gensub(/([?&])(Passwd|token)=[^& \t]+/, "\\1\\2=redacted", "g") }'
diff --git a/composer.json b/composer.json
index a8d1b64ad..2234a0e13 100644
--- a/composer.json
+++ b/composer.json
@@ -17,7 +17,7 @@
"WebSub"
],
"require": {
- "php": ">=7.0",
+ "php": ">=7.2",
"ext-ctype": "*",
"ext-curl": "*",
"ext-dom": "*",
@@ -47,7 +47,7 @@
"ext-phar": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
- "phpstan/phpstan": "~1.7.14",
+ "phpstan/phpstan": "~1.9.17",
"phpunit/phpunit": "^9",
"squizlabs/php_codesniffer": "^3.7"
},
diff --git a/composer.lock b/composer.lock
index d9bc8ed02..74a7155d8 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,35 +4,35 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "b52a52e72b9449a7d252d92ccc62c826",
+ "content-hash": "4dcbcd3ba9c1dbed63612651a725dab8",
"packages": [],
"packages-dev": [
{
"name": "doctrine/instantiator",
- "version": "1.4.1",
+ "version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
- "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc"
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc",
- "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
- "doctrine/coding-standard": "^9",
+ "doctrine/coding-standard": "^9 || ^11",
"ext-pdo": "*",
"ext-phar": "*",
"phpbench/phpbench": "^0.16 || ^1",
"phpstan/phpstan": "^1.4",
"phpstan/phpstan-phpunit": "^1",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
- "vimeo/psalm": "^4.22"
+ "vimeo/psalm": "^4.30 || ^5.4"
},
"type": "library",
"autoload": {
@@ -59,7 +59,7 @@
],
"support": {
"issues": "https://github.com/doctrine/instantiator/issues",
- "source": "https://github.com/doctrine/instantiator/tree/1.4.1"
+ "source": "https://github.com/doctrine/instantiator/tree/1.5.0"
},
"funding": [
{
@@ -75,7 +75,7 @@
"type": "tidelift"
}
],
- "time": "2022-03-03T08:28:38+00:00"
+ "time": "2022-12-30T00:15:36+00:00"
},
{
"name": "myclabs/deep-copy",
@@ -138,16 +138,16 @@
},
{
"name": "nikic/php-parser",
- "version": "v4.14.0",
+ "version": "v4.15.2",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1"
+ "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1",
- "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
+ "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
"shasum": ""
},
"require": {
@@ -188,9 +188,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2"
},
- "time": "2022-05-31T20:59:12+00:00"
+ "time": "2022-11-12T15:38:23+00:00"
},
{
"name": "phar-io/manifest",
@@ -304,244 +304,17 @@
"time": "2022-02-21T01:04:05+00:00"
},
{
- "name": "phpdocumentor/reflection-common",
- "version": "2.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
- "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
- "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
- "shasum": ""
- },
- "require": {
- "php": "^7.2 || ^8.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-2.x": "2.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "phpDocumentor\\Reflection\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Jaap van Otterdijk",
- "email": "opensource@ijaap.nl"
- }
- ],
- "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
- "homepage": "http://www.phpdoc.org",
- "keywords": [
- "FQSEN",
- "phpDocumentor",
- "phpdoc",
- "reflection",
- "static analysis"
- ],
- "support": {
- "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
- "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
- },
- "time": "2020-06-27T09:03:43+00:00"
- },
- {
- "name": "phpdocumentor/reflection-docblock",
- "version": "5.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "622548b623e81ca6d78b721c5e029f4ce664f170"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170",
- "reference": "622548b623e81ca6d78b721c5e029f4ce664f170",
- "shasum": ""
- },
- "require": {
- "ext-filter": "*",
- "php": "^7.2 || ^8.0",
- "phpdocumentor/reflection-common": "^2.2",
- "phpdocumentor/type-resolver": "^1.3",
- "webmozart/assert": "^1.9.1"
- },
- "require-dev": {
- "mockery/mockery": "~1.3.2",
- "psalm/phar": "^4.8"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "5.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "phpDocumentor\\Reflection\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Mike van Riel",
- "email": "me@mikevanriel.com"
- },
- {
- "name": "Jaap van Otterdijk",
- "email": "account@ijaap.nl"
- }
- ],
- "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
- "support": {
- "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
- "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0"
- },
- "time": "2021-10-19T17:43:47+00:00"
- },
- {
- "name": "phpdocumentor/type-resolver",
- "version": "1.6.1",
- "source": {
- "type": "git",
- "url": "https://github.com/phpDocumentor/TypeResolver.git",
- "reference": "77a32518733312af16a44300404e945338981de3"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3",
- "reference": "77a32518733312af16a44300404e945338981de3",
- "shasum": ""
- },
- "require": {
- "php": "^7.2 || ^8.0",
- "phpdocumentor/reflection-common": "^2.0"
- },
- "require-dev": {
- "ext-tokenizer": "*",
- "psalm/phar": "^4.8"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-1.x": "1.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "phpDocumentor\\Reflection\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Mike van Riel",
- "email": "me@mikevanriel.com"
- }
- ],
- "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
- "support": {
- "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
- "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1"
- },
- "time": "2022-03-15T21:29:03+00:00"
- },
- {
- "name": "phpspec/prophecy",
- "version": "v1.15.0",
- "source": {
- "type": "git",
- "url": "https://github.com/phpspec/prophecy.git",
- "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
- "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
- "shasum": ""
- },
- "require": {
- "doctrine/instantiator": "^1.2",
- "php": "^7.2 || ~8.0, <8.2",
- "phpdocumentor/reflection-docblock": "^5.2",
- "sebastian/comparator": "^3.0 || ^4.0",
- "sebastian/recursion-context": "^3.0 || ^4.0"
- },
- "require-dev": {
- "phpspec/phpspec": "^6.0 || ^7.0",
- "phpunit/phpunit": "^8.0 || ^9.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Prophecy\\": "src/Prophecy"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Konstantin Kudryashov",
- "email": "ever.zet@gmail.com",
- "homepage": "http://everzet.com"
- },
- {
- "name": "Marcello Duarte",
- "email": "marcello.duarte@gmail.com"
- }
- ],
- "description": "Highly opinionated mocking framework for PHP 5.3+",
- "homepage": "https://github.com/phpspec/prophecy",
- "keywords": [
- "Double",
- "Dummy",
- "fake",
- "mock",
- "spy",
- "stub"
- ],
- "support": {
- "issues": "https://github.com/phpspec/prophecy/issues",
- "source": "https://github.com/phpspec/prophecy/tree/v1.15.0"
- },
- "time": "2021-12-08T12:19:24+00:00"
- },
- {
"name": "phpstan/phpstan",
- "version": "1.7.15",
+ "version": "1.9.17",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "cd0202ea1b1fc6d1bbe156c6e2e18a03e0ff160a"
+ "reference": "204e459e7822f2c586463029f5ecec31bb45a1f2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd0202ea1b1fc6d1bbe156c6e2e18a03e0ff160a",
- "reference": "cd0202ea1b1fc6d1bbe156c6e2e18a03e0ff160a",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/204e459e7822f2c586463029f5ecec31bb45a1f2",
+ "reference": "204e459e7822f2c586463029f5ecec31bb45a1f2",
"shasum": ""
},
"require": {
@@ -565,9 +338,13 @@
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
- "source": "https://github.com/phpstan/phpstan/tree/1.7.15"
+ "source": "https://github.com/phpstan/phpstan/tree/1.9.17"
},
"funding": [
{
@@ -579,35 +356,31 @@
"type": "github"
},
{
- "url": "https://www.patreon.com/phpstan",
- "type": "patreon"
- },
- {
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
"type": "tidelift"
}
],
- "time": "2022-06-20T08:29:01+00:00"
+ "time": "2023-02-08T12:25:00+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "9.2.15",
+ "version": "9.2.23",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f"
+ "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
- "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c",
+ "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
- "nikic/php-parser": "^4.13.0",
+ "nikic/php-parser": "^4.14",
"php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
@@ -656,7 +429,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.23"
},
"funding": [
{
@@ -664,7 +437,7 @@
"type": "github"
}
],
- "time": "2022-03-07T09:28:20+00:00"
+ "time": "2022-12-28T12:41:10+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -909,16 +682,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "9.5.21",
+ "version": "9.5.27",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1"
+ "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1",
- "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a2bc7ffdca99f92d959b3f2270529334030bba38",
+ "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38",
"shasum": ""
},
"require": {
@@ -933,7 +706,6 @@
"phar-io/manifest": "^2.0.3",
"phar-io/version": "^3.0.2",
"php": ">=7.3",
- "phpspec/prophecy": "^1.12.1",
"phpunit/php-code-coverage": "^9.2.13",
"phpunit/php-file-iterator": "^3.0.5",
"phpunit/php-invoker": "^3.1.1",
@@ -941,19 +713,16 @@
"phpunit/php-timer": "^5.0.2",
"sebastian/cli-parser": "^1.0.1",
"sebastian/code-unit": "^1.0.6",
- "sebastian/comparator": "^4.0.5",
+ "sebastian/comparator": "^4.0.8",
"sebastian/diff": "^4.0.3",
"sebastian/environment": "^5.1.3",
- "sebastian/exporter": "^4.0.3",
+ "sebastian/exporter": "^4.0.5",
"sebastian/global-state": "^5.0.1",
"sebastian/object-enumerator": "^4.0.3",
"sebastian/resource-operations": "^3.0.3",
- "sebastian/type": "^3.0",
+ "sebastian/type": "^3.2",
"sebastian/version": "^3.0.2"
},
- "require-dev": {
- "phpspec/prophecy-phpunit": "^2.0.1"
- },
"suggest": {
"ext-soap": "*",
"ext-xdebug": "*"
@@ -995,7 +764,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.21"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.27"
},
"funding": [
{
@@ -1005,9 +774,13 @@
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+ "type": "tidelift"
}
],
- "time": "2022-06-19T12:14:25+00:00"
+ "time": "2022-12-09T07:31:23+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -1178,16 +951,16 @@
},
{
"name": "sebastian/comparator",
- "version": "4.0.6",
+ "version": "4.0.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "55f4261989e546dc112258c7a75935a81a7ce382"
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382",
- "reference": "55f4261989e546dc112258c7a75935a81a7ce382",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
"shasum": ""
},
"require": {
@@ -1240,7 +1013,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
- "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6"
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
},
"funding": [
{
@@ -1248,7 +1021,7 @@
"type": "github"
}
],
- "time": "2020-10-26T15:49:45+00:00"
+ "time": "2022-09-14T12:41:17+00:00"
},
{
"name": "sebastian/complexity",
@@ -1438,16 +1211,16 @@
},
{
"name": "sebastian/exporter",
- "version": "4.0.4",
+ "version": "4.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9"
+ "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9",
- "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
+ "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
"shasum": ""
},
"require": {
@@ -1503,7 +1276,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
- "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4"
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5"
},
"funding": [
{
@@ -1511,7 +1284,7 @@
"type": "github"
}
],
- "time": "2021-11-11T14:18:36+00:00"
+ "time": "2022-09-14T06:03:37+00:00"
},
{
"name": "sebastian/global-state",
@@ -1866,16 +1639,16 @@
},
{
"name": "sebastian/type",
- "version": "3.0.0",
+ "version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
- "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad"
+ "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad",
- "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e",
+ "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e",
"shasum": ""
},
"require": {
@@ -1887,7 +1660,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-master": "3.2-dev"
}
},
"autoload": {
@@ -1910,7 +1683,7 @@
"homepage": "https://github.com/sebastianbergmann/type",
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
- "source": "https://github.com/sebastianbergmann/type/tree/3.0.0"
+ "source": "https://github.com/sebastianbergmann/type/tree/3.2.0"
},
"funding": [
{
@@ -1918,7 +1691,7 @@
"type": "github"
}
],
- "time": "2022-03-15T09:54:48+00:00"
+ "time": "2022-09-12T14:47:03+00:00"
},
{
"name": "sebastian/version",
@@ -2078,64 +1851,6 @@
}
],
"time": "2021-07-28T10:34:58+00:00"
- },
- {
- "name": "webmozart/assert",
- "version": "1.11.0",
- "source": {
- "type": "git",
- "url": "https://github.com/webmozarts/assert.git",
- "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991",
- "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991",
- "shasum": ""
- },
- "require": {
- "ext-ctype": "*",
- "php": "^7.2 || ^8.0"
- },
- "conflict": {
- "phpstan/phpstan": "<0.12.20",
- "vimeo/psalm": "<4.6.1 || 4.6.2"
- },
- "require-dev": {
- "phpunit/phpunit": "^8.5.13"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.10-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Webmozart\\Assert\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Bernhard Schussek",
- "email": "bschussek@gmail.com"
- }
- ],
- "description": "Assertions to validate method input/output with nice error messages.",
- "keywords": [
- "assert",
- "check",
- "validate"
- ],
- "support": {
- "issues": "https://github.com/webmozarts/assert/issues",
- "source": "https://github.com/webmozarts/assert/tree/1.11.0"
- },
- "time": "2022-06-03T18:03:27+00:00"
}
],
"aliases": [],
@@ -2144,7 +1859,7 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
- "php": ">=7.0",
+ "php": ">=7.2",
"ext-ctype": "*",
"ext-curl": "*",
"ext-dom": "*",
@@ -2170,5 +1885,5 @@
"ext-tokenizer": "*",
"ext-xmlwriter": "*"
},
- "plugin-api-version": "2.2.0"
+ "plugin-api-version": "2.3.0"
}
diff --git a/config-user.default.php b/config-user.default.php
index f7759396a..5a26841ac 100644
--- a/config-user.default.php
+++ b/config-user.default.php
@@ -7,6 +7,8 @@ return array (
'enabled' => true,
'is_admin' => false,
'language' => 'en',
+ // A timezone identifier such as 'Europe/Paris' https://php.net/timezones or blank for server default
+ 'timezone' => '',
'archiving' => [
'keep_period' => 'P3M',
'keep_max' => 200,
@@ -62,6 +64,7 @@ return array (
'site' => true,
),
'theme' => 'Origine',
+ 'darkMode' => 'no',
'content_width' => 'thin',
'shortcuts' => array (
'actualize' => 'q',
diff --git a/config.default.php b/config.default.php
index 37eb4d6d5..d0873d55d 100644
--- a/config.default.php
+++ b/config.default.php
@@ -103,7 +103,7 @@ return array(
'cache_duration' => 800,
# SimplePie HTTP request timeout in seconds.
- 'timeout' => 15,
+ 'timeout' => 20,
# If a user has not used FreshRSS for more than x seconds,
# then its feeds are not refreshed anymore.
diff --git a/constants.php b/constants.php
index 8d3b52f2f..396e39785 100644
--- a/constants.php
+++ b/constants.php
@@ -2,8 +2,8 @@
//NB: Do not edit; use ./constants.local.php instead.
//<Not customisable>
-define('FRESHRSS_MIN_PHP_VERSION', '7.0.0');
-define('FRESHRSS_VERSION', '1.20.2');
+define('FRESHRSS_MIN_PHP_VERSION', '7.2.0');
+define('FRESHRSS_VERSION', '1.21.0');
define('FRESHRSS_WEBSITE', 'https://freshrss.org');
define('FRESHRSS_WIKI', 'https://freshrss.github.io/FreshRSS/');
diff --git a/docs/en/admins/02_Prerequisites.md b/docs/en/admins/02_Prerequisites.md
index 41dabc651..c2a2b3fa7 100644
--- a/docs/en/admins/02_Prerequisites.md
+++ b/docs/en/admins/02_Prerequisites.md
@@ -7,7 +7,7 @@ You need to verify that your server can run FreshRSS before installing it. If yo
| Software | Recommended | Also Works With |
| ------------- | ----------------------- | ----------------------- |
| Web server | **Apache 2** | Nginx, lighttpd |
-| PHP | **PHP 7+** | |
+| PHP | **PHP 7.2+** | |
| PHP modules | Required: libxml, cURL, JSON, PDO_MySQL, PCRE and ctype.<br />Required (32-bit only): GMP <br />Recommended: Zlib, mbstring, iconv, ZipArchive<br />*For the whole modules list see [Dockerfile](https://github.com/FreshRSS/FreshRSS/blob/edge/Docker/Dockerfile-Alpine#L7-L9)* | |
| Database | **MySQL 5.5.3+** | SQLite 3.7.4+, PostgreSQL 9.5+ |
| Browser | **Firefox** | Chrome, Opera, Safari, or Edge |
diff --git a/docs/en/admins/05_Configuring_email_validation.md b/docs/en/admins/05_Configuring_email_validation.md
index 69a06b53f..aec6b8b75 100644
--- a/docs/en/admins/05_Configuring_email_validation.md
+++ b/docs/en/admins/05_Configuring_email_validation.md
@@ -68,7 +68,7 @@ Once you’re done, don’t forget to reconfigure your environment to `productio
## Access the validation URL during development
-You might find painful to configure a SMTP server when you’re developping and
+You might find painful to configure a SMTP server when you’re developing and
`mail` function will not work on your local machine. For the moment, there is
no easy way to access the validation URL unless forging it. You’ll need to
information:
diff --git a/docs/en/admins/06_LinuxInstall.md b/docs/en/admins/06_LinuxInstall.md
index e92fc3247..1af041efe 100644
--- a/docs/en/admins/06_LinuxInstall.md
+++ b/docs/en/admins/06_LinuxInstall.md
@@ -81,14 +81,7 @@ Change to the new FreshRSS directory, and set the permissions so that your Web s
```sh
cd FreshRSS
-chown -R :www-data .
-sudo chmod -R g+r .
-```
-
-We’ll also need to allow the data folder to be written to, like so:
-
-```sh
-chmod -R g+w ./data/
+sudo cli/access-permissions.sh
```
Optional: If you would like to allow updates from the Web interface, set write permissions
diff --git a/docs/en/admins/07_LinuxUpdate.md b/docs/en/admins/07_LinuxUpdate.md
index 834dfaaef..47b84e5ef 100644
--- a/docs/en/admins/07_LinuxUpdate.md
+++ b/docs/en/admins/07_LinuxUpdate.md
@@ -49,22 +49,22 @@ If your local user doesn’t have write access to the FreshRSS folder, use a sud
4. Update FreshRSS
```sh
git checkout edge
- git pull
- git checkout $(git describe --tags --abbrev=0)
+ git pull --ff-only
```
- Note: If you want to use the rolling release, the last command is optional.
+ > ℹ️ Use `edge` for the rolling release or `latest` for the latest stable release.
5. (optional) Make sure you use the correct version
```sh
git status
```
- The command should tell you the tag that you’re using. It must be the same as the one associated with [the latest release on GitHub](https://github.com/FreshRSS/FreshRSS/releases/latest). If you use the rolling release, it should tell you that your `edge` branch is up to date with `origin`.
+ The command should tell you the branch that you’re using. It must be the same as the one associated with [the latest release on GitHub](https://github.com/FreshRSS/FreshRSS/releases/latest).
+ If you use the rolling release, it should tell you that your `edge` branch is up to date with `origin`.
6. Re-set correct permissions so that your web server can access the files
```sh
- chown -R :www-data . && chmod -R g+r . && chmod -R g+w ./data/
+ cli/access-permissions.sh
```
## Using the Zip archive
@@ -91,7 +91,7 @@ If your local user doesn’t have write access to the FreshRSS folder, use a sud
5. Re-set permissions
```sh
- chown -R :www-data . && chmod -R g+r . && chmod -R g+w ./data/
+ cli/access-permissions.sh
```
6. Clean up the FreshRSS directory by deleting the downloaded zip and the temporary directory
diff --git a/docs/en/admins/08_FeedUpdates.md b/docs/en/admins/08_FeedUpdates.md
index 2072188bd..79c2bbea6 100644
--- a/docs/en/admins/08_FeedUpdates.md
+++ b/docs/en/admins/08_FeedUpdates.md
@@ -6,7 +6,7 @@ FreshRSS is updated by the `./app/actualize_script.php` script. Knowing this, we
**Note:** the following examples assume that FreshRSS is installed to `/usr/share/FreshRSS`. You’ll need to modify the FreshRSS path to reflect your own system.
-**Note:** If you cannot configure a local Cronjob, [see an alternative using online cron](../users/03_Main_view.md#online-cron).
+**Note:** If you cannot configure a local Cronjob, [see an alternative using online cron](../users/09_refreshing_feeds.md#online-cron).
## Cron as a trigger
diff --git a/docs/en/admins/10_ServerConfig.md b/docs/en/admins/10_ServerConfig.md
index 6c5823a2d..87bd74d09 100644
--- a/docs/en/admins/10_ServerConfig.md
+++ b/docs/en/admins/10_ServerConfig.md
@@ -1,8 +1,13 @@
# Apache/Nginx Configuration Files
+> ℹ️ For improved security, remove sensitive information in the Web server logs by using our [`sensitive-log.sh` script](https://github.com/FreshRSS/FreshRSS/blob/edge/cli/sensitive-log.sh),
+on the model of our [reference Apache configuration](https://github.com/FreshRSS/FreshRSS/blob/edge/Docker/FreshRSS.Apache.conf) used for our official Docker images
+(see [`CustomLog`](https://httpd.apache.org/docs/current/mod/mod_log_config.html#customlog)).
+
## Apache configuration
This is an example Apache virtual hosts configuration file. It covers HTTP and HTTPS configuration.
+For more details, check our [reference Apache configuration](https://github.com/FreshRSS/FreshRSS/blob/edge/Docker/FreshRSS.Apache.conf) used for our official Docker images.
```apache
<VirtualHost *:80>
@@ -24,6 +29,7 @@ This is an example Apache virtual hosts configuration file. It covers HTTP and H
</Directory>
ErrorLog ${APACHE_LOG_DIR}/freshrss_error.log
+ # Consider piping the logs for cleaning passwords; cf. comment higher up.
CustomLog ${APACHE_LOG_DIR}/freshrss_access.log combined
AllowEncodedSlashes On
diff --git a/docs/en/developers/02_Github.md b/docs/en/developers/02_Github.md
index 4e6a84ab3..066d6ffb0 100644
--- a/docs/en/developers/02_Github.md
+++ b/docs/en/developers/02_Github.md
@@ -53,7 +53,7 @@ Now you can create a PR based on your branch.
## How to write a commit message
-A commit message should succintly describe the changes on the first line. For example:
+A commit message should succinctly describe the changes on the first line. For example:
> Fix broken icon
diff --git a/docs/en/developers/03_Backend/05_Extensions.md b/docs/en/developers/03_Backend/05_Extensions.md
index aa707d2e4..e86d73bfa 100644
--- a/docs/en/developers/03_Backend/05_Extensions.md
+++ b/docs/en/developers/03_Backend/05_Extensions.md
@@ -22,195 +22,9 @@ Another solution consists of an extension system. By allowing users to write the
Note: it is quite conceivable that the functionalities of an extension can later be officially integrated into the FreshRSS code. Extensions make it easy to propose a proof of concept.
-## Understanding basic mechanics (Minz and MVC)
+## Minz Framework
-**TODO** : move to 02_Minz.md
-
-This data sheet should refer to the official FreshRSS and Minz documentation (the PHP framework on which FreshRSS is based). Unfortunately, this documentation does not yet exist. In a few words, here are the main things you should know. It is not necessary to read all the chapters in this section if you don’t need to use a feature in your extension (if you don’t need to translate your extension, no need to know more about the `Minz_Translate` module for example).
-
-### MVC Architecture
-
-Minz relies on and imposes an MVC architecture on projects using it. This architecture consists of three main components:
-
-* The model: this is the base object that we will manipulate. In FreshRSS, categories, flows and articles are templates. The part of the code that makes it possible to manipulate them in a database is also part of the model but is separated from the base model: we speak of DAO (for "Data Access Object"). The templates are stored in a `Models` folder.
-* The view: this is what the user sees. The view is therefore simply HTML code mixed with PHP to display dynamic information. The views are stored in a `views` folder.
-* The controller: this is what makes it possible to link models and views. Typically, a controller will load templates from the database (like a list of items) to "pass" them to a view for display. Controllers are stored in a `Controllers` directory.
-
-### Routing
-
-In order to link a URL to a controller, first you have to go through a "routing" phase. In FreshRSS, this is particularly simple because it suffices to specify the name of the controller to load into the URL using a `c` parameter.
-For example, the address <http://example.com?c=hello> will execute the code contained in the `hello` controller.
-
-One concept that has not yet been discussed is the "actions" system. An action is executed *on* a controller. Concretely, a controller is represented by a class and its actions by methods. To execute an action, it is necessary to specify an `a` parameter in the URL.
-
-Code example:
-
-```php
-<?php
-
-class FreshRSS_hello_Controller extends FreshRSS_ActionController {
- public function indexAction() {
- $this->view->a_variable = 'FooBar';
- }
-
- public function worldAction() {
- $this->view->a_variable = 'Hello World!';
- }
-}
-
-?>
-```
-
-When loading the address <http://example.com?c=hello&a=world>, the `world` action is executed on the `hello` controller.
-
-Note: if `c` or `a` is not specified, the default value for each of these variables is `index`.
-So the address <http://example.com?c=hello> will execute the `index` action of the `hello` controller.
-
-From now on, the `hello/world` naming convention will be used to refer to a controller/action pair.
-
-### Views
-
-Each view is associated with a controller and an action. The view associated with `hello/world` will be stored in a very specific file: `views/hello/world. phtml`. This convention is imposed by Minz.
-
-As explained above, the views consist of HTML mixed with PHP. Code example:
-
-```html
-<p>
- This is a parameter passed from the controller: <?= $this->a_variable ?>
-</p>
-```
-
-The variable `$this->a_variable` is passed by the controller (see previous example). The difference is that in the controller it is necessary to pass `$this->view`, while in the view `$this` suffices.
-
-### Working with GET / POST
-
-It is often necessary to take advantage of parameters passed by GET or POST. In Minz, these parameters are accessible using the `Minz_Request` class.
-Code example:
-
-```php
-<?php
-
-$default_value = 'foo';
-$param = Minz_Request::param('bar', $default_value);
-
-// Display the value of the parameter `bar` (passed via GET or POST)
-// or "foo" if the parameter does not exist.
-echo $param;
-
-// Sets the value of the `bar` parameter
-Minz_Request::_param('bar', 'baz');
-
-// Will necessarily display "baz" since we have just forced its value.
-// Note that the second parameter (default) is optional.
-echo Minz_Request::param('bar');
-
-?>
-```
-
-The `Minz_Request::isPost()` method can be used to execute a piece of code only if it is a POST request.
-
-Note: it is preferable to use `Minz_Request` only in controllers. It is likely that you will encounter this method in FreshRSS views, or even in templates, but be aware that this is **not** good practice.
-
-### Access session settings
-
-The access to session parameters is strangely similar to the GET / POST parameters but passes through the `Minz_Session` class this time! There is no example here because you can repeat the previous example by changing all `Minz_Request` to `Minz_Session`.
-
-### Working with URLs
-
-To take full advantage of the Minz routing system, it is strongly discouraged to write hard URLs in your code. For example, the following view should be avoided:
-
-```html
-<p>
- Go to page <a href="http://example.com?c=hello&amp;a=world">Hello world</a>!
-</p>
-```
-
-If one day it was decided to use a "url rewriting" system to have addresses in a <http://example.com/controller/action> format, all previous addresses would become ineffective!
-
-So use the `Minz_Url` class and its `display()` method instead. `Minz_Url::display()` takes an array of the following form as its argument:
-
-```php
-<?php
-
-$url_array = [
- 'c' => 'hello',
- 'a' => 'world',
- 'params' => [
- 'foo' => 'bar',
- ],
-];
-
-// Show something like .?c=hello&amp;a=world&amp;foo=bar
-echo Minz_Url::display($url_array);
-
-?>
-```
-
-Since this can become a bit tedious to use in the long run, especially in views, it is preferable to use the `_url()` shortcut:
-
-```php
-<?php
-
-// Displays the same as above
-echo _url('hello', 'world', 'foo', 'bar');
-
-?>
-```
-
-Note: as a general rule, the shortened form (`_url()`) should be used in views, while the long form (`Minz_Url::display()`) should be used in controllers.
-
-### Redirections
-
-It is often necessary to redirect a user to another page. To do so, the `Minz_Request` class offers another useful method: `forward()`. This method takes the same URL format as the one seen just before as its argument.
-
-Code example:
-
-```php
-<?php
-
-$url_array = [
- 'c' => 'hello',
- 'a' => 'world',
-];
-
-// Tells Minz to redirect the user to the hello / world page.
-// Note that this is a redirection in the Minz sense of the term, not a redirection that the browser will have to manage (HTTP code 301 or 302)
-// The code that follows forward() will thus be executed!
-Minz_Request::forward($url_array);
-
-// To perform a type 302 redirect, add "true".
-// The code that follows will never be executed.
-Minz_Request::forward($url_array, true);
-
-?>
-```
-
-It is very common to want display a message to the user while performing a redirect, to tell the user how the action was carried out (validation of a form for example). Such a message is passed through a `notification` session variable (note: we will talk about feedback from now on to avoid confusion with a notification that can occur at any time). To facilitate this kind of very frequent action, there are two shortcuts that both perform a 302 redirect by assigning a feedback message:
-
-```php
-<?php
-
-$url_array = [
- 'c' => 'hello',
- 'a' => 'world',
-];
-$feedback_good = 'All went well!';
-$feedback_bad = 'Oops, something went wrong.';
-
-Minz_Request::good($feedback_good, $url_array);
-
-// or
-
-Minz_Request::bad($feedback_bad, $url_array);
-
-?>
-```
-
-### Translation Management
-
-This part [is explained here](/docs/en/internationalization.md).
-
-### Configuration management
+see [Minz documentation](/docs/en/developers/Minz/index.md)
## Write an extension for FreshRSS
diff --git a/docs/en/developers/Minz/index.md b/docs/en/developers/Minz/index.md
index 9b6d46f17..ed5bc0482 100644
--- a/docs/en/developers/Minz/index.md
+++ b/docs/en/developers/Minz/index.md
@@ -1,19 +1,193 @@
-# Minz
+# Minz Framework
Minz is the homemade PHP framework used by FreshRSS.
-The documentation is still incomplete and it would be great to explain:
+This data sheet should refer to the official FreshRSS and Minz documentation (the PHP framework on which FreshRSS is based). Unfortunately, this documentation does not yet exist. In a few words, here are the main things you should know. It is not necessary to read all the chapters in this section if you don’t need to use a feature in your extension (if you don’t need to translate your extension, no need to know more about the `Minz_Translate` module for example).
-- routing, controllers and actions
-- configuration
-- models and database
-- views
-- URLs management
-- sessions
-- internationalisation
-- extensions
-- mailer
+## MVC Architecture
+
+Minz relies on and imposes an MVC architecture on projects using it. This architecture consists of three main components:
+
+* The model: this is the base object that we will manipulate. In FreshRSS, categories, flows and articles are templates. The part of the code that makes it possible to manipulate them in a database is also part of the model but is separated from the base model: we speak of DAO (for "Data Access Object"). The templates are stored in a `Models` folder.
+* The view: this is what the user sees. The view is therefore simply HTML code mixed with PHP to display dynamic information. The views are stored in a `views` folder.
+* The controller: this is what makes it possible to link models and views. Typically, a controller will load templates from the database (like a list of items) to "pass" them to a view for display. Controllers are stored in a `Controllers` directory.
+
+## Routing
+
+In order to link a URL to a controller, first you have to go through a "routing" phase. In FreshRSS, this is particularly simple because it suffices to specify the name of the controller to load into the URL using a `c` parameter.
+For example, the address <http://example.com?c=hello> will execute the code contained in the `hello` controller.
+
+One concept that has not yet been discussed is the "actions" system. An action is executed *on* a controller. Concretely, a controller is represented by a class and its actions by methods. To execute an action, it is necessary to specify an `a` parameter in the URL.
+
+Code example:
+
+```php
+<?php
+
+class FreshRSS_hello_Controller extends FreshRSS_ActionController {
+ public function indexAction() {
+ $this->view->a_variable = 'FooBar';
+ }
+
+ public function worldAction() {
+ $this->view->a_variable = 'Hello World!';
+ }
+}
+
+?>
+```
+
+When loading the address <http://example.com?c=hello&a=world>, the `world` action is executed on the `hello` controller.
+
+Note: if `c` or `a` is not specified, the default value for each of these variables is `index`.
+So the address <http://example.com?c=hello> will execute the `index` action of the `hello` controller.
+
+From now on, the `hello/world` naming convention will be used to refer to a controller/action pair.
+
+## Views
+
+Each view is associated with a controller and an action. The view associated with `hello/world` will be stored in a very specific file: `views/hello/world. phtml`. This convention is imposed by Minz.
+
+As explained above, the views consist of HTML mixed with PHP. Code example:
+
+```html
+<p>
+ This is a parameter passed from the controller: <?= $this->a_variable ?>
+</p>
+```
+
+The variable `$this->a_variable` is passed by the controller (see previous example). The difference is that in the controller it is necessary to pass `$this->view`, while in the view `$this` suffices.
+
+## Working with GET / POST
+
+It is often necessary to take advantage of parameters passed by GET or POST. In Minz, these parameters are accessible using the `Minz_Request` class.
+Code example:
+
+```php
+<?php
+
+$default_value = 'foo';
+$param = Minz_Request::param('bar', $default_value);
+
+// Display the value of the parameter `bar` (passed via GET or POST)
+// or "foo" if the parameter does not exist.
+echo $param;
+
+// Sets the value of the `bar` parameter
+Minz_Request::_param('bar', 'baz');
+
+// Will necessarily display "baz" since we have just forced its value.
+// Note that the second parameter (default) is optional.
+echo Minz_Request::param('bar');
+
+?>
+```
+
+The `Minz_Request::isPost()` method can be used to execute a piece of code only if it is a POST request.
+
+Note: it is preferable to use `Minz_Request` only in controllers. It is likely that you will encounter this method in FreshRSS views, or even in templates, but be aware that this is **not** good practice.
+
+## Access session settings
+
+The access to session parameters is strangely similar to the GET / POST parameters but passes through the `Minz_Session` class this time! There is no example here because you can repeat the previous example by changing all `Minz_Request` to `Minz_Session`.
+
+## Working with URLs
+
+To take full advantage of the Minz routing system, it is strongly discouraged to write hard URLs in your code. For example, the following view should be avoided:
+
+```html
+<p>
+ Go to page <a href="http://example.com?c=hello&amp;a=world">Hello world</a>!
+</p>
+```
+
+If one day it was decided to use a "url rewriting" system to have addresses in a <http://example.com/controller/action> format, all previous addresses would become ineffective!
+
+So use the `Minz_Url` class and its `display()` method instead. `Minz_Url::display()` takes an array of the following form as its argument:
+
+```php
+<?php
+
+$url_array = [
+ 'c' => 'hello',
+ 'a' => 'world',
+ 'params' => [
+ 'foo' => 'bar',
+ ],
+];
+
+// Show something like .?c=hello&amp;a=world&amp;foo=bar
+echo Minz_Url::display($url_array);
+
+?>
+```
+
+Since this can become a bit tedious to use in the long run, especially in views, it is preferable to use the `_url()` shortcut:
+
+```php
+<?php
+
+// Displays the same as above
+echo _url('hello', 'world', 'foo', 'bar');
+
+?>
+```
+
+Note: as a general rule, the shortened form (`_url()`) should be used in views, while the long form (`Minz_Url::display()`) should be used in controllers.
+
+## Redirections
+
+It is often necessary to redirect a user to another page. To do so, the `Minz_Request` class offers another useful method: `forward()`. This method takes the same URL format as the one seen just before as its argument.
+
+Code example:
+
+```php
+<?php
+
+$url_array = [
+ 'c' => 'hello',
+ 'a' => 'world',
+];
+
+// Tells Minz to redirect the user to the hello / world page.
+// Note that this is a redirection in the Minz sense of the term, not a redirection that the browser will have to manage (HTTP code 301 or 302)
+// The code that follows forward() will thus be executed!
+Minz_Request::forward($url_array);
+
+// To perform a type 302 redirect, add "true".
+// The code that follows will never be executed.
+Minz_Request::forward($url_array, true);
+
+?>
+```
+
+It is very common to want display a message to the user while performing a redirect, to tell the user how the action was carried out (validation of a form for example). Such a message is passed through a `notification` session variable (note: we will talk about feedback from now on to avoid confusion with a notification that can occur at any time). To facilitate this kind of very frequent action, there are two shortcuts that both perform a 302 redirect by assigning a feedback message:
+
+```php
+<?php
+
+$url_array = [
+ 'c' => 'hello',
+ 'a' => 'world',
+];
+$feedback_good = 'All went well!';
+$feedback_bad = 'Oops, something went wrong.';
+
+Minz_Request::good($feedback_good, $url_array);
+
+// or
+
+Minz_Request::bad($feedback_bad, $url_array);
+
+?>
+```
+
+## Translation Management
+
+This part [is explained here](/docs/en/internationalization.md).
+
+## Migration
Existing documentation includes:
-- [How to manage migrations](migrations.md)
+* [How to manage migrations](migrations.md)
diff --git a/docs/en/developers/OPML.md b/docs/en/developers/OPML.md
index 2190a1de3..5191592a8 100644
--- a/docs/en/developers/OPML.md
+++ b/docs/en/developers/OPML.md
@@ -17,17 +17,19 @@ FreshRSS uses the XML namespace <https://freshrss.org/opml> to export/import ext
The list of the custom FreshRSS attributes can be seen in [the source code](https://github.com/FreshRSS/FreshRSS/blob/edge/app/views/helpers/export/opml.phtml), and here is an overview:
-### HTML+XPath
+### HTML+XPath or XML+XPath
* `<outline type="HTML+XPath" ...`: Additional type of source, which is not RSS/Atom, but HTML Web Scraping using [XPath](https://www.w3.org/TR/xpath-10/) 1.0.
> ℹ️ [XPath 1.0](https://en.wikipedia.org/wiki/XPath) is a standard query language, which FreshRSS supports to enable [Web scraping](https://en.wikipedia.org/wiki/Web_scraping).
+* `<outline type="XML+XPath" ...`: Same than `HTML+XPath` but using an XML parser.
+
The following attributes are using similar naming conventions than [RSS-Bridge](https://rss-bridge.github.io/rss-bridge/Bridge_API/XPathAbstract.html).
* `frss:xPathItem`: XPath expression for extracting the feed items from the source page.
* Example: `//div[@class="news-item"]`
-* `frss:xPathItemTitle`: XPath expression for extracting the feed title from the source page.
+* `frss:xPathItemTitle`: XPath expression for extracting the item’s title from the item context.
* Example: `descendant::h2`
* `frss:xPathItemContent`: XPath expression for extracting an item’s content from the item context.
* Example: `.`
diff --git a/docs/en/internationalization.md b/docs/en/internationalization.md
index 3feb4f57c..741a642c7 100644
--- a/docs/en/internationalization.md
+++ b/docs/en/internationalization.md
@@ -86,7 +86,7 @@ This command adds an IGNORE comment on the translation so the key can be conside
## Add/remove/update a key
-If you’re developping a new part of the application, you might want to declare a new translation key. Your first impulse would be to add the key to each file manually: don’t do that, it’s very painful. We provide another command:
+If you’re developing a new part of the application, you might want to declare a new translation key. Your first impulse would be to add the key to each file manually: don’t do that, it’s very painful. We provide another command:
```sh
make i18n-add-key key=the.key.to.add value='Your string in English'
diff --git a/docs/en/users/02_First_steps.md b/docs/en/users/02_First_steps.md
index 3bcdaab08..7f176af77 100644
--- a/docs/en/users/02_First_steps.md
+++ b/docs/en/users/02_First_steps.md
@@ -19,9 +19,9 @@ Now that you’ve mastered basic use, it’s time to configure FreshRSS to impro
* [Organize your feeds in categories](04_Subscriptions.md#feed-management)
* [Change the home page](05_Configuration.md#changing-the-view)
* [Choose the reading options](05_Configuration.md#reading-options)
-* [Refresh feeds](03_Main_view.md#refreshing-feeds)
-* [Filter articles](03_Main_view.md#filtering-articles) for a fast access to a selection
-* [search for an article](03_Main_view.md#with-the-search-field) published some time ago
+* [Refresh feeds](09_refreshing_feeds.md)
+* [Filter articles](10_filter.md) for a fast access to a selection
+* [search for an article](10_filter.md#with-the-search-field) published some time ago
* [Access your feeds on a mobile device](06_Mobile_access.md)
* [Add some extensions](https://github.com/FreshRSS/Extensions)
* [Frequently asked questions](07_Frequently_Asked_Questions.md)
diff --git a/docs/en/users/03_Main_view.md b/docs/en/users/03_Main_view.md
index eb8fe0f01..7a0320cb6 100644
--- a/docs/en/users/03_Main_view.md
+++ b/docs/en/users/03_Main_view.md
@@ -1,10 +1,12 @@
+# Views
+
FreshRSS has three primary viewing modes: Normal, Global, and Reader view.
-# Normal view
+## Normal view
Normal view will allow you to view articles in a compressed view. They can be separated by category or individual feed, or viewed in the "main stream" containing all feeds. Clicking a feed in the sidebar (mobile users will need to click the folder icon to open it) will open that feed’s view.
-## Article List
+### Article List
By default, the normal view includes six items per article. From left to right:
* **Read status:** An envelope icon to show if the article has been read or not. Closed envelopes are unread, open envelopes are read. Clicking on the icon will toggle the read status.
@@ -14,7 +16,7 @@ By default, the normal view includes six items per article. From left to right:
* **Article date/time:** The time the article was posted.
* **Link to original article:** A globe icon that can be clicked to go to the article on the original website.
-## Normal View Sidebar
+### Normal View Sidebar
Clicking the gear icon next to an individual feed will display additional options for that feed.
* **Filter:** Run the defined filter to mark articles as read
@@ -24,256 +26,15 @@ Clicking the gear icon next to an individual feed will display additional option
* **Actualize:** Force-update the feed
* **Mark as read:** Mark all items in the feed as read
-# Global view
+## Global view
Global view allows quick views of feed’s statuses at once. Feeds and categories are shown with the number of unread articles next to them. Clicking a feed’s name will open it in a view similar to normal view.
-# Reader view
+## Reader view
Reader view will display a feed will all articles already open for reading. Feeds can be switched by clicking the folder icon at the top to bring up the category/feed sidebar.
-# Refreshing feeds
-
-To take full advantage of FreshRSS, it needs to retrieve new items from the feeds you have subscribed to. There are several ways to do this.
-
-## Automatic update with cron
-
-This is the recommended method.
-
-This method is only available if you have access to the scheduled tasks of the machine on which your FreshRSS instance is installed.
-
-The script is named *actualize_script.php* and is located in the *app* folder. The scheduled task syntax will not be explained here. However, here is [a quick introduction to crontab](http://www.adminschoice.com/crontab-quick-reference/) that might help you.
-
-Here is an example to trigger article update every hour.
-
-```cron
-0 * * * * php /path/to/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
-```
-
-## Online cron
-
-If you do not have access to the installation server scheduled task, you can still automate the update process.
-
-To do so, you need to create a scheduled task, which need to call a specific URL:
-<https://freshrss.example.net/i/?c=feed&a=actualize> (it could be different depending on your installation). Depending on your application authentication method, you need to adapt the scheduled task.
-
-Special parameters to configure the script - all parameters can be combined:
-
-* Parameter "force"
-<https://freshrss.example.net/i/?c=feed&a=actualize&force=1>
-If *force* is set to 1 all feeds will be refreshed at once.
-
-* Parameter "ajax"
-<https://freshrss.example.net/i/?c=feed&a=actualize&ajax=1>
-Only a status site is returned and not a complete website. Example: "OK"
-
-* Parameter "maxFeeds"
-<https://freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=30>
-If *maxFeeds* is set the configured amount of feeds is refreshed at once. The default setting is "10".
-
-* Parameter "token"
-<https://freshrss.example.net/i/?c=feed&a=actualize&token=542345872345734>
-Security parameter to prevent unauthorized refreshes. For detailed Documentation see "Form authentication".
-
-### For Form Authentication
-
-If your FreshRSS instance is using Form Authentication, you can configure an authentication token to grant access to the online cron.
-
-![Token configuration](../img/users/token.1.png)
-
-You can target a specific user by adding their username to the query string, with `&user=insert-username`:
-
-The scheduled task syntax should look as follows:
-
-<https://freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=10&ajax=1&user=someone&token=my-token>
-
-Alternatively, but not recommended, if you configure the application to allow anonymous reading, you can also allow anonymous users to update feeds (“Allow anonymous refresh of the articles”), and that does not require a token.
-
-![Anonymous access configuration](../img/users/anonymous_access.1.png)
-
-### For HTTP authentication
-
-If your FreshRSS instance is using HTTP authentication, you’ll need to provide your credentials to the scheduled task.
-
-**Note:** This method is discouraged as your credentials are stored in plain text.
-
-```cron
-0 * * * * curl -u alice:password123 'https://freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=10&ajax=1&user=alice'
-```
-
-On some systems, that syntax might also work:
-
-<https://alice:password123@freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=10&ajax=1&user=alice>
-
-### For No authentication (None)
-
-If your FreshRSS instance uses no authentication (public instance, default user):
-
-<https://freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=10&ajax=1>
-
-## Manual update
-
-If you can’t or don’t want to use the automatic method, you can update manually. There are two methods for updating all or some of the feeds.
-
-### Complete update
-
-This update occurs on all feeds. To trigger it, simply click on the update link in the navigation menu.
-
-![Navigation menu](../img/users/refresh.1.png)
-
-When the update starts, a progress bar appears and changes while feeds are processed.
-
-![Progress bar](../img/users/refresh.5.png)
-
-### Partial update
-
-This update occurs on the selected feed only. To trigger it, simply click on the update link in the feed menu.
-
-![Feed menu](../img/users/refresh.2.png)
-
-# Filtering articles
-
-## Purpose
-
-When the number of articles stored by FreshRSS inevitably grows larger, it’s important to use efficient filters to display only a subset of the articles. There are several methods that filter with different criteria. Usually those methods can be combined.
-
-## How-to filter
-
-### By category
-
-This is the easiest method. You only need to click on the category title in the side panel. There are two special categories at the top of the panel:
-
-* *Main feed* displays only articles from feeds marked as available in that category
-* *Favourites* displays only articles marked as favourites
-
-### By feed
-
-There are several methods to filter articles by feed:
-
-* by clicking the feed title in the side panel
-* by clicking the feed title in the article details
-* by filtering in the feed options from the side panel
-* by filtering in the feed configuration
-
-![Feed filter](../img/users/feed.filter.1.png)
-
-### By status
-
-Each article has two attributes that can be combined. The first attribute indicates whether or not the article has been read. The second attribute indicates if the article was marked as favorite or not.
-
-In version 0.7, attribute filters are available in the article display dropdown list. With this version, it’s not possible to combine filters. For instance, it’s not possible to display only read and favorite articles.
-
-![Attribute filters in 0.7](../img/users/status.filter.0.7.png)
-
-Starting with version 0.8, all attribute filters are visible as toggle icons. They can be combined. As any combination is possible, some have the same result. For instance, the result for all filters selected is the same as no filter selected.
-
-![Attribute filters in 0.8](../img/users/status.filter.0.8.png)
-
-By default, this filter displays only unread articles
-
-### By content
-
-It is possible to filter articles by their content by inputting a string in the search field.
-
-### With the search field
-
-You can use the search field to further refine results:
-
-* by feed ID: `f:123` or multiple feed IDs (*or*): `f:123,234,345`
-* by author: `author:name` or `author:'composed name'`
-* by title: `intitle:keyword` or `intitle:'composed keyword'`
-* by URL: `inurl:keyword` or `inurl:'composed keyword'`
-* by tag: `#tag` or `#tag+with+whitespace`
-* by free-text: `keyword` or `'composed keyword'`
-* by date of discovery, using the [ISO 8601 time interval format](http://en.wikipedia.org/wiki/ISO_8601#Time_intervals): `date:<date-interval>`
- * From a specific day, or month, or year:
- * `date:2014-03-30`
- * `date:2014-03` or `date:201403`
- * `date:2014`
- * From a specific time of a given day:
- * `date:2014-05-30T13`
- * `date:2014-05-30T13:30`
- * Between two given dates:
- * `date:2014-02/2014-04`
- * `date:2014-02--2014-04`
- * `date:2014-02/04`
- * `date:2014-02-03/05`
- * `date:2014-02-03T22:00/22:15`
- * `date:2014-02-03T22:00/15`
- * After a given date:
- * `date:2014-03/`
- * Before a given date:
- * `date:/2014-03`
- * For a specific duration after a given date:
- * `date:2014-03/P1W`
- * For a specific duration before a given date:
- * `date:P1W/2014-05-25T23:59:59`
- * For the past duration before now (the trailing slash is optional):
- * `date:P1Y/` or `date:P1Y` (past year)
- * `date:P2M/` (past two months)
- * `date:P3W/` (past three weeks)
- * `date:P4D/` (past four days)
- * `date:PT5H/` (past five hours)
- * `date:PT30M/` (past thirty minutes)
- * `date:PT90S/` (past ninety seconds)
- * `date:P1DT1H/` (past one day and one hour)
-* by date of publication, using the same format: `pubdate:<date-interval>`
-* by custom label ID `L:12` or multiple label IDs: `L:12,13,14` or with any label: `L:*`
-* by custom label name `label:label`, `label:"my label"` or any label name from a list (*or*): `labels:"my label,my other label"`
-* by several label names (*and*): `label:"my label" label:"my other label"`
-* by entry (article) ID: `e:1639310674957894` or multiple entry IDs (*or*): `e:1639310674957894,1639310674957893`
-* by user query (saved search) name: `search:myQuery`, `search:"My query"` or saved search ID: `S:3`
- * internally, those references are replaced by the corresponding user query in the search expression
-
-Be careful not to enter a space between the operator and the search value.
-
-Some operators can be used negatively, to exclude articles, with the same syntax as above, but prefixed by a `!` or `-`:
-`!f:234`, `-author:name`, `-intitle:keyword`, `-inurl:keyword`, `-#tag`, `!keyword`, `!date:2019`, `!date:P1W`, `!pubdate:P3d/`.
-
-It is also possible to combine keywords to create a more precise filter.
-For example, you can enter multiple instances of `f:`, `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`
-
-You don’t have to do anything special to combine multiple negative operators. Writing `!intitle:'thing1' !intitle:'thing2'` implies AND, see above. For more pointers on how AND and OR interact with negation, see [this GitHub comment](https://github.com/FreshRSS/FreshRSS/issues/3236#issuecomment-891219460).
-Additional reading: [De Morgan's laws](https://en.wikipedia.org/wiki/De_Morgan%27s_laws).
-
-Finally, parentheses may be used to express more complex queries, with basic negation support:
-
-* `(author:Alice OR intitle:hello) (author:Bob OR intitle:world)`
-* `(author:Alice intitle:hello) OR (author:Bob intitle:world)`
-* `!((author:Alice intitle:hello) OR (author:Bob intitle:world))`
-* `(author:Alice intitle:hello) !(author:Bob intitle:world)`
-* `!(S:1 OR S:2)`
-
-### By sorting by date
-
-You can change the sort order by clicking the toggle button available in the header.
-
-## Store your filters
-
-Once you came up with your perfect filter, it would be a shame if you need to recreate it every time you need to use it.
-
-Hopefully, there is a way to bookmark them for later use.
-We call them *user queries*.
-You can create as many as you want, the only limit is how they will be displayed on your screen.
-
-### Bookmark the current query
-
-Display the user queries drop-down by clicking the button next to the state buttons.
-![User queries drop-down](../img/users/user.queries.drop-down.empty.png)
-
-Then click on the bookmark action.
-
-Congratulations, you’re done!
-
-### Using a bookmarked query
-
-Display the user queries drop-down by clicking the button next to the state buttons.
-![User queries drop-down](../img/users/user.queries.drop-down.not.empty.png)
-
-Then click on the bookmarked query, the previously stored query will be applied.
-
-> Note that only the query is stored, not the articles.
-> The results you are seeing now could be different from the results on the day you've created the query.
+---
+Read more:
+* [Refreshing the feeds](./09_refreshing_feeds.md)
+* [Filter the feeds and search](./10_filter.md)
diff --git a/docs/en/users/04_Subscriptions.md b/docs/en/users/04_Subscriptions.md
index d50d92bf2..7b0a488f0 100644
--- a/docs/en/users/04_Subscriptions.md
+++ b/docs/en/users/04_Subscriptions.md
@@ -5,7 +5,7 @@
3. Paste the URL in the “Feed URL” field.
4. (optional): You can select the category for your feed. By default, it will be in “Uncategorized”.
5. (optional): If the subscription requires credentials, you can enter them in the "HTTP username" and "HTTP password" fields.
-6. (optional): You can set a timeout for the feed request if the feed requires it.
+6. (optional): You can set a timeout for the feed request.
7. (optional): You can choose to ignore SSL certificate errors (such as with self-signed certificates) by setting "Verify SSL security" to "No". This is not recommended, and it is better to either add the root certificate to the FreshRSS server or to fix the SSL certificate problems on the feed hosting server.
## Subscription management
@@ -89,7 +89,7 @@ Complementary tools can be used to retrieve full article content, such as:
### Filter
-Articles can be automatically marked as read based on some search terms. See [filtering](./03_Main_view.md#filtering-articles) for more information on how to create these filters.
+Articles can be automatically marked as read based on some search terms. See [filtering](./10_filter.md) for more information on how to create these filters.
## Import / export
diff --git a/docs/en/users/05_Configuration.md b/docs/en/users/05_Configuration.md
index 9ad6a58ca..1ce0e0a0d 100644
--- a/docs/en/users/05_Configuration.md
+++ b/docs/en/users/05_Configuration.md
@@ -13,16 +13,23 @@ Available languages are: cz, de, en, es, fr, he, it, ko, nl, oc, pt-br, ru, tr,
## Theme
-There’s no accounting for tastes, which is why FreshRSS offers eight official themes:
-
-* *Blue Lagoon* by **Mister aiR**
-* *Dark* by **AD**
-* *Flat design* by **Marien Fressinaud**
-* *Origine* by **Marien Fressinaud**
-* *Origine-compact* by **Kevin Papst**
-* *Pafat* by **Plopoyop**
-* *Screwdriver* by **Mister aiR**
-* *Swage* by **Patrick Crandol**
+There’s no accounting for tastes, which is why FreshRSS offers 13 official themes:
+
+| Theme | designed by | Notes |
+|:--------------|:-------------------------------------------------------|:--------------------------------------------------------------|
+| Alternative Dark | Ghost | |
+| Ansum | Thomas Guesnon | |
+| Blue Lagoon |Mister aiR | No longer supported. Will be removed with FreshRSS V1.22.0 |
+| Dark | AD | |
+| Dark pink | Miicat_47 | |
+| Flat design | Marien Fressinaud | No longer supported. Will be removed with FreshRSS V1.22.0 |
+| Mapco | Thomas Guesnon | |
+| Nord theme | joelchrono12 | |
+| Origine | Marien Fressinaud | (default theme) |
+| Origine-compact | Kevin Papst | |
+| Pafat | Plopoyop | |
+| Screwdriver | Mister aiR | No longer supported. Will be removed with FreshRSS V1.22.0 |
+| Swage | Patrick Crandol | |
If you can’t find any themes you like, it’s always possible to [create your own](../developers/04_Frontend/02_Design.md).
diff --git a/docs/en/users/09_refreshing_feeds.md b/docs/en/users/09_refreshing_feeds.md
new file mode 100644
index 000000000..6d9c2af55
--- /dev/null
+++ b/docs/en/users/09_refreshing_feeds.md
@@ -0,0 +1,134 @@
+# Refreshing feeds
+
+To take full advantage of FreshRSS, it needs to retrieve new items from the feeds you have subscribed to. There are several ways to do this:
+
+- [Manual update](#manual-update)
+ - [Complete update](#complete-update)
+ - [Partial update](#partial-update)
+- [Automatic update with cron](#automatic-update-with-cron)
+- [Online cron](#online-cron)
+ - [For Form Authentication](#for-form-authentication)
+ - [For HTTP authentication](#for-http-authentication)
+ - [For No authentication None](#for-no-authentication-none)
+- [Feed configuration of “Do not automatically refresh more often than”](#feed-configuration-of-do-not-automatically-refresh-more-often-than)
+ - [Background](#background)
+ - [Default value](#default-value)
+ - [Individual feed configuration](#individual-feed-configuration)
+
+## Manual update
+
+If you can’t or don’t want to use the automatic method, you can update manually. There are two methods for updating all or some of the feeds.
+
+### Complete update
+
+This update occurs on all feeds. To trigger it, simply click on the update link in the navigation menu.
+
+![Navigation menu](../img/users/refresh.1.png)
+
+When the update starts, a progress bar appears and changes while feeds are processed.
+
+![Progress bar](../img/users/refresh.5.png)
+
+### Partial update
+
+This update occurs on the selected feed only. To trigger it, simply click on the update link in the feed menu.
+
+![Feed menu](../img/users/refresh.2.png)
+
+## Automatic update with cron
+
+This is the recommended method.
+
+This method is only available if you have access to the scheduled tasks of the machine on which your FreshRSS instance is installed.
+
+The script is named *actualize_script.php* and is located in the *app* folder. The scheduled task syntax will not be explained here. However, here is [a quick introduction to crontab](http://www.adminschoice.com/crontab-quick-reference/) that might help you.
+
+Here is an example to trigger article update every hour.
+
+```cron
+0 * * * * php /path/to/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
+```
+
+## Online cron
+
+If you do not have access to the installation server scheduled task, you can still automate the update process.
+
+To do so, you need to create a scheduled task, which need to call a specific URL:
+<https://freshrss.example.net/i/?c=feed&a=actualize> (it could be different depending on your installation). Depending on your application authentication method, you need to adapt the scheduled task.
+
+Special parameters to configure the script - all parameters can be combined:
+
+- Parameter "force"
+<https://freshrss.example.net/i/?c=feed&a=actualize&force=1>
+If *force* is set to 1 all feeds will be refreshed at once.
+
+- Parameter "ajax"
+<https://freshrss.example.net/i/?c=feed&a=actualize&ajax=1>
+Only a status site is returned and not a complete website. Example: "OK"
+
+- Parameter "maxFeeds"
+<https://freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=30>
+If *maxFeeds* is set the configured amount of feeds is refreshed at once. The default setting is "10".
+
+- Parameter "token"
+<https://freshrss.example.net/i/?c=feed&a=actualize&token=542345872345734>
+Security parameter to prevent unauthorized refreshes. For detailed Documentation see "Form authentication".
+
+### For Form Authentication
+
+If your FreshRSS instance is using Form Authentication, you can configure an authentication token to grant access to the online cron.
+
+![Token configuration](../img/users/token.1.png)
+
+You can target a specific user by adding their username to the query string, with `&user=insert-username`:
+
+The scheduled task syntax should look as follows:
+
+<https://freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=10&ajax=1&user=someone&token=my-token>
+
+Alternatively, but not recommended, if you configure the application to allow anonymous reading, you can also allow anonymous users to update feeds (“Allow anonymous refresh of the articles”), and that does not require a token.
+
+![Anonymous access configuration](../img/users/anonymous_access.1.png)
+
+### For HTTP authentication
+
+If your FreshRSS instance is using HTTP authentication, you’ll need to provide your credentials to the scheduled task.
+
+**Note:** This method is discouraged as your credentials are stored in plain text.
+
+```cron
+0 * * * * curl -u alice:password123 'https://freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=10&ajax=1&user=alice'
+```
+
+On some systems, that syntax might also work:
+
+<https://alice:password123@freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=10&ajax=1&user=alice>
+
+### For No authentication (None)
+
+If your FreshRSS instance uses no authentication (public instance, default user):
+
+<https://freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=10&ajax=1>
+
+## Feed configuration of “Do not automatically refresh more often than”
+
+### Background
+
+FreshRSS does not, by design, supports pull refreshes at frequencies higher than once every 15 minutes. But FreshRSS supports instant push (WebSub).
+
+FreshRSS is part of an RSS ecosystem. A typical reaction that we have seen from several servers is to simply ban by, IP, user-agent, or to remove their RSS feed altogether. Bad user behaviours affect the larger community.
+
+### Default value
+
+The default value of “Do not automatically refresh more often than” is set in Configuration -> Archiving.
+
+The lowest global/default purposely cannot be set faster than every 20 minutes, to avoid wasting resources and make sure the RSS ecosystem remains sane.
+
+### Individual feed configuration
+
+Under the settings for individual feeds, you can go down to 15min.
+
+---
+Read more:
+- [Normal, Global and Reader view](./03_Main_view.md)
+- [Filter the feeds and search](./10_filter.md)
diff --git a/docs/en/users/10_filter.md b/docs/en/users/10_filter.md
new file mode 100644
index 000000000..2e69b97f8
--- /dev/null
+++ b/docs/en/users/10_filter.md
@@ -0,0 +1,152 @@
+
+# Filtering articles
+
+## Purpose
+
+When the number of articles stored by FreshRSS inevitably grows larger, it’s important to use efficient filters to display only a subset of the articles. There are several methods that filter with different criteria. Usually those methods can be combined.
+
+## By category
+
+This is the easiest method. You only need to click on the category title in the side panel. There are two special categories at the top of the panel:
+
+* *Main feed* displays only articles from feeds marked as available in that category
+* *Favourites* displays only articles marked as favourites
+
+## By feed
+
+There are several methods to filter articles by feed:
+
+* by clicking the feed title in the side panel
+* by clicking the feed title in the article details
+* by filtering in the feed options from the side panel
+* by filtering in the feed configuration
+
+![Feed filter](../img/users/feed.filter.1.png)
+
+## By status
+
+Each article has two attributes that can be combined. The first attribute indicates whether or not the article has been read. The second attribute indicates if the article was marked as favorite or not.
+
+In version 0.7, attribute filters are available in the article display dropdown list. With this version, it’s not possible to combine filters. For instance, it’s not possible to display only read and favorite articles.
+
+![Attribute filters in 0.7](../img/users/status.filter.0.7.png)
+
+Starting with version 0.8, all attribute filters are visible as toggle icons. They can be combined. As any combination is possible, some have the same result. For instance, the result for all filters selected is the same as no filter selected.
+
+![Attribute filters in 0.8](../img/users/status.filter.0.8.png)
+
+By default, this filter displays only unread articles
+
+## By content
+
+It is possible to filter articles by their content by inputting a string in the search field.
+
+## With the search field
+
+You can use the search field to further refine results:
+
+* by feed ID: `f:123` or multiple feed IDs (*or*): `f:123,234,345`
+* by author: `author:name` or `author:'composed name'`
+* by title: `intitle:keyword` or `intitle:'composed keyword'`
+* by URL: `inurl:keyword` or `inurl:'composed keyword'`
+* by tag: `#tag` or `#tag+with+whitespace`
+* by free-text: `keyword` or `'composed keyword'`
+* by date of discovery, using the [ISO 8601 time interval format](http://en.wikipedia.org/wiki/ISO_8601#Time_intervals): `date:<date-interval>`
+ * From a specific day, or month, or year:
+ * `date:2014-03-30`
+ * `date:2014-03` or `date:201403`
+ * `date:2014`
+ * From a specific time of a given day:
+ * `date:2014-05-30T13`
+ * `date:2014-05-30T13:30`
+ * Between two given dates:
+ * `date:2014-02/2014-04`
+ * `date:2014-02--2014-04`
+ * `date:2014-02/04`
+ * `date:2014-02-03/05`
+ * `date:2014-02-03T22:00/22:15`
+ * `date:2014-02-03T22:00/15`
+ * After a given date:
+ * `date:2014-03/`
+ * Before a given date:
+ * `date:/2014-03`
+ * For a specific duration after a given date:
+ * `date:2014-03/P1W`
+ * For a specific duration before a given date:
+ * `date:P1W/2014-05-25T23:59:59`
+ * For the past duration before now (the trailing slash is optional):
+ * `date:P1Y/` or `date:P1Y` (past year)
+ * `date:P2M/` (past two months)
+ * `date:P3W/` (past three weeks)
+ * `date:P4D/` (past four days)
+ * `date:PT5H/` (past five hours)
+ * `date:PT30M/` (past thirty minutes)
+ * `date:PT90S/` (past ninety seconds)
+ * `date:P1DT1H/` (past one day and one hour)
+* by date of publication, using the same format: `pubdate:<date-interval>`
+* by custom label ID `L:12` or multiple label IDs: `L:12,13,14` or with any label: `L:*`
+* by custom label name `label:label`, `label:"my label"` or any label name from a list (*or*): `labels:"my label,my other label"`
+* by several label names (*and*): `label:"my label" label:"my other label"`
+* by entry (article) ID: `e:1639310674957894` or multiple entry IDs (*or*): `e:1639310674957894,1639310674957893`
+* by user query (saved search) name: `search:myQuery`, `search:"My query"` or saved search ID: `S:3`
+ * internally, those references are replaced by the corresponding user query in the search expression
+
+Be careful not to enter a space between the operator and the search value.
+
+Some operators can be used negatively, to exclude articles, with the same syntax as above, but prefixed by a `!` or `-`:
+`!f:234`, `-author:name`, `-intitle:keyword`, `-inurl:keyword`, `-#tag`, `!keyword`, `!date:2019`, `!date:P1W`, `!pubdate:P3d/`.
+
+It is also possible to combine keywords to create a more precise filter.
+For example, you can enter multiple instances of `f:`, `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`
+
+You don’t have to do anything special to combine multiple negative operators. Writing `!intitle:'thing1' !intitle:'thing2'` implies AND, see above. For more pointers on how AND and OR interact with negation, see [this GitHub comment](https://github.com/FreshRSS/FreshRSS/issues/3236#issuecomment-891219460).
+Additional reading: [De Morgan's laws](https://en.wikipedia.org/wiki/De_Morgan%27s_laws).
+
+Finally, parentheses may be used to express more complex queries, with basic negation support:
+
+* `(author:Alice OR intitle:hello) (author:Bob OR intitle:world)`
+* `(author:Alice intitle:hello) OR (author:Bob intitle:world)`
+* `!((author:Alice intitle:hello) OR (author:Bob intitle:world))`
+* `(author:Alice intitle:hello) !(author:Bob intitle:world)`
+* `!(S:1 OR S:2)`
+
+> ℹ️ If you need to search for a parenthesis, it needs to be escaped like `\(` or `\)`
+
+## By sorting by date
+
+You can change the sort order by clicking the toggle button available in the header.
+
+## Store your filters
+
+Once you came up with your perfect filter, it would be a shame if you need to recreate it every time you need to use it.
+
+Hopefully, there is a way to bookmark them for later use.
+We call them *user queries*.
+You can create as many as you want, the only limit is how they will be displayed on your screen.
+
+### Bookmark the current query
+
+Display the user queries drop-down by clicking the button next to the state buttons.
+![User queries drop-down](../img/users/user.queries.drop-down.empty.png)
+
+Then click on the bookmark action.
+
+Congratulations, you’re done!
+
+### Using a bookmarked query
+
+Display the user queries drop-down by clicking the button next to the state buttons.
+![User queries drop-down](../img/users/user.queries.drop-down.not.empty.png)
+
+Then click on the bookmarked query, the previously stored query will be applied.
+
+> Note that only the query is stored, not the articles.
+> The results you are seeing now could be different from the results on the day you've created the query.
+
+---
+Read more:
+* [Normal, Global and Reader view](./03_Main_view.md)
+* [Refreshing the feeds](./09_refreshing_feeds.md)
diff --git a/docs/fr/developers/01_First_steps.md b/docs/fr/developers/01_First_steps.md
index 6c02b5058..f53cbaeb8 100644
--- a/docs/fr/developers/01_First_steps.md
+++ b/docs/fr/developers/01_First_steps.md
@@ -57,7 +57,7 @@ Vous pouvez arrêter les conteneurs en tapant <kbd>Control</kbd> + <kbd>c</kbd>
make stop
```
-Si la configuration vous intéresse, les commandes `make' sont définies dans
+Si la configuration vous intéresse, les commandes `make` sont définies dans
le fichier [`Makefile`](/Makefile).
Si vous avez besoin d’utiliser une image Docker identifiée par un tag
diff --git a/docs/fr/developers/03_Backend/02_Minz.md b/docs/fr/developers/03_Backend/02_Minz.md
deleted file mode 100644
index 5daf684f0..000000000
--- a/docs/fr/developers/03_Backend/02_Minz.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Minz
-
-## Modèles
-
-> **À FAIRE**
-
-## Contrôleurs et actions
-
-> **À FAIRE**
-
-## Vues
-
-> **À FAIRE**
-
-## Routage
-
-> **À FAIRE**
-
-## Écriture des URL
-
-> **À FAIRE**
-
-## Internationalisation
-
-> **À FAIRE**
-
-## Comprendres les mécanismes internes
-
-> **À FAIRE**
diff --git a/docs/fr/developers/03_Backend/05_Extensions.md b/docs/fr/developers/03_Backend/05_Extensions.md
index a715c40b3..548aebf4f 100644
--- a/docs/fr/developers/03_Backend/05_Extensions.md
+++ b/docs/fr/developers/03_Backend/05_Extensions.md
@@ -36,329 +36,9 @@ puissent par la suite être intégrées dans le code initial de FreshRSS de
façon officielle. Cela permet de proposer un « proof of concept » assez
facilement.
-## Comprendre les mécaniques de base (Minz et MVC)
-
-**TODO** : bouger dans 02_Minz.md
-
-Cette fiche technique devrait renvoyer vers la documentation officielle de
-FreshRSS et de Minz (le framework PHP sur lequel repose
-FreshRSS). Malheureusement cette documentation n’existe pas encore. Voici
-donc en quelques mots les principaux éléments à connaître. Il n’est pas
-nécessaire de lire l’ensemble des chapitres de cette section si vous n’avez
-pas à utiliser une fonctionnalité dans votre extension (si vous n’avez pas
-besoin de traduire votre extension, pas besoin d’en savoir plus sur le
-module `Minz_Translate` par exemple).
-
-### Architecture MVC
-
-Minz repose et impose une architecture MVC pour les projets l’utilisant. On
-distingue dans cette architecture trois composants principaux :
-
-* Le Modèle : c’est l’objet de base que l’on va manipuler. Dans FreshRSS,
- les catégories, les flux et les articles sont des modèles. La partie du
- code qui permet de les manipuler en base de données fait aussi partie du
- modèle mais est séparée du modèle de base : on parle de DAO (pour « Data
- Access Object »). Les modèles sont stockés dans un répertoire `Models`.
-* La Vue : c’est ce qui représente ce que verra l’utilisateur. La vue est
- donc simplement du code HTML que l’on mixe avec du PHP pour afficher les
- informations dynamiques. Les vues sont stockées dans un répertoire
- `views`.
-* Le Contrôleur : c’est ce qui permet de lier modèles et vues entre
- eux. Typiquement, un contrôleur va charger des modèles à partir de la base
- de données (une liste d’articles par exemple) pour les « passer » à une
- vue afin qu’elle les affiche. Les contrôleurs sont stockés dans un
- répertoire `Controllers`.
-
-### Routage
-
-Afin de lier une URL à un contrôleur, on doit passer par une phase dite de «
-routage ». Dans FreshRSS, cela est particulièrement simple car il suffit
-d’indiquer le nom du contrôleur à charger dans l’URL à l’aide d’un paramètre `c`.
-Par exemple, l’adresse <http://exemple.com?c=hello> va exécuter le code
-contenu dans le contrôleur `hello`.
-
-Une notion qui n’a pas encore été évoquée est le système d'« actions ». Une
-action est exécutée *sur* un contrôleur. Concrètement, un contrôleur va être
-représenté par une classe et ses actions par des méthodes. Pour exécuter une
-action, il est nécessaire d’indiquer un paramètre `a` dans l’URL.
-
-Exemple de code :
-
-```php
-<?php
-
-class FreshRSS_hello_Controller extends FreshRSS_ActionController {
- public function indexAction() {
- $this->view->a_variable = 'FooBar';
- }
-
- public function worldAction() {
- $this->view->a_variable = 'Hello World!';
- }
-}
-
-?>
-```
-
-Si l’on charge l’adresse <http://exemple.com?c=hello&a=world>, l’action
-`world` va donc être exécutée sur le contrôleur `hello`.
-
-Note : si `c` ou `a` n’est pas précisée, la valeur par défaut de chacune de
-ces variables est `index`. Ainsi l’adresse <http://exemple.com?c=hello> va
-exécuter l’action `index` du contrôleur `hello`.
-
-Plus loin, sera utilisée la convention `hello/world` pour évoquer un couple
-contrôleur/action.
-
-### Vues
-
-Chaque vue est associée à un contrôleur et à une action. La vue associée à
-`hello/world` va être stockée dans un fichier bien spécifique :
-`views/hello/world.phtml`. Cette convention est imposée par Minz.
-
-Comme expliqué plus haut, les vues sont du code HTML mixé à du PHP. Exemple
-de code :
-
-```html
-<p>
- Phrase passée en paramètre : <?= $this->a_variable ?>
-</p>
-```
-
-La variable `$this->a_variable` a été passée précédemment par le contrôleur (voir exemple précédent). La différence est que dans le contrôleur il est nécessaire de passer par `$this->view` et que dans la vue `$this` suffit.
-
-### Accéder aux paramètres GET / POST
-
-Il est souvent nécessaire de profiter des paramètres passés par GET ou par
-POST. Dans Minz, ces paramètres sont accessibles de façon indistincts à
-l’aide de la classe `Minz_Request`. Exemple de code :
-
-```php
-<?php
-
-$default_value = 'foo';
-$param = Minz_Request::param('bar', $default_value);
-
-// Affichera la valeur du paramètre `bar` (passé via GET ou POST)
-// ou "foo" si le paramètre n’existe pas.
-echo $param;
-
-// Force la valeur du paramètre `bar`
-Minz_Request::_param('bar', 'baz');
-
-// Affichera forcément "baz" puisque nous venons de forcer sa valeur.
-// Notez que le second paramètre (valeur par défaut) est facultatif.
-echo Minz_Request::param('bar');
-
-?>
-```
-
-La méthode `Minz_Request::isPost()` peut être utile pour n’exécuter un
-morceau de code que s’il s’agit d’une requête POST.
-
-Note : il est préférable de n’utiliser `Minz_Request` que dans les
-contrôleurs. Il est probable que vous rencontriez cette méthode dans les
-vues de FreshRSS, voire dans les modèles, mais sachez qu’il ne s’agit
-**pas** d’une bonne pratique.
-
-### Accéder aux paramètres de session
-
-L’accès aux paramètres de session est étrangement similaire aux paramètres
-GET / POST mais passe par la classe `Minz_Session` cette fois-ci ! Il n’y a
-pas d’exemple ici car vous pouvez reprendre le précédent en changeant tous
-les `Minz_Request` par des `Minz_Session`.
-
-### Gestion des URL
-
-Pour profiter pleinement du système de routage de Minz, il est fortement
-déconseillé d’écrire les URL en dur dans votre code. Par exemple, la vue
-suivante doit être évitée :
-
-```html
-<p>
- Accéder à la page <a href="http://exemple.com?c=hello&amp;a=world">Hello world</a>!
-</p>
-```
-
-Si un jour il est décidé d’utiliser un système d'« url rewriting » pour
-avoir des adresses au format <http://exemple.com/controller/action>, toutes
-les adresses précédentes deviendraient ineffectives !
-
-Préférez donc l’utilisation de la classe `Minz_Url` et de sa méthode
-`display()`. `Minz_Url::display()` prend en paramètre un tableau de la forme
-suivante :
-
-```php
-<?php
-
-$url_array = [
- 'c' => 'hello',
- 'a' => 'world',
- 'params' => [
- 'foo' => 'bar',
- ],
-];
-
-// Affichera quelque chose comme .?c=hello&amp;a=world&amp;foo=bar
-echo Minz_Url::display($url_array);
-
-?>
-```
-
-Comme cela peut devenir un peu pénible à utiliser à la longue, surtout dans
-les vues, il est préférable d’utiliser le raccourci `_url()` :
-
-```php
-<?php
-
-// Affichera la même chose que précédemment
-echo _url('hello', 'world', 'foo', 'bar');
-
-?>
-```
-
-Note : en règle générale, la forme raccourcie (`_url()`) doit être utilisée
-dans les vues tandis que la forme longue (`Minz_Url::display()`) doit être
-utilisée dans les contrôleurs.
-
-### Redirections
-
-Il est souvent nécessaire de rediriger un utilisateur vers une autre
-page. Pour cela, la classe `Minz_Request` dispose d’une autre méthode utile
-: `forward()`. Cette méthode prend en argument le même format d’URL que
-celui vu juste avant.
-
-Exemple de code :
-
-```php
-<?php
-
-$url_array = [
- 'c' => 'hello',
- 'a' => 'world',
-];
-
-// Indique à Minz de rediriger l’utilisateur vers la page hello/world.
-// Notez qu’il s’agit d’une redirection au sens Minz du terme, pas d’une redirection que le navigateur va avoir à gérer (code HTTP 301 ou 302)
-// Le code qui suit forward() va ainsi être exécuté !
-Minz_Request::forward($url_array);
-
-// Pour effectuer une redirection type 302, ajoutez "true".
-// Le code qui suivra ne sera alors jamais exécuté.
-Minz_Request::forward($url_array, true);
-
-?>
-```
-
-Il est très fréquent de vouloir effectuer une redirection tout en affichant
-un message à l’utilisateur pour lui indiquer comment s’est déroulée l’action
-effectuée juste avant (validation d’un formulaire par exemple). Un tel
-message est passé par une variable de session `notification` (note : nous
-parlerons plutôt de « feedback » désormais pour éviter la confusion avec une
-notification qui peut survenir à tout moment). Pour faciliter ce genre
-d’action très fréquente, il existe deux raccourcis qui effectuent tout deux
-une redirection type 302 en affectant un message de feedback :
-
-```php
-<?php
-
-$url_array = [
- 'c' => 'hello',
- 'a' => 'world',
-];
-$feedback_good = 'Tout s’est bien passé !';
-$feedback_bad = 'Oups, quelque chose n’a pas marché.';
-
-Minz_Request::good($feedback_good, $url_array);
-
-// ou
-
-Minz_Request::bad($feedback_bad, $url_array);
-
-?>
-```
-
-### Gestion de la traduction
-
-Il est fréquent (et c’est un euphémisme) de vouloir afficher des phrases à
-l’utilisateur. Dans l’exemple précédent par exemple, nous affichions un
-feedback à l’utilisateur en fonction du résultat d’une validation de
-formulaire. Le problème est que FreshRSS possède des utilisateurs de
-différentes nationalités. Il est donc nécessaire de pouvoir gérer
-différentes langues pour ne pas rester cantonné à l’Anglais ou au Français.
-
-La solution consiste à utiliser la classe `Minz_Translate` qui permet de
-traduire dynamiquement FreshRSS (ou toute application basée sur Minz). Avant
-d’utiliser ce module, il est nécessaire de savoir où trouver les chaînes de
-caractères à traduire. Chaque langue possède son propre sous-répertoire dans
-un répertoire parent nommé `i18n`. Par exemple, les fichiers de langue en
-Français sont situés dans `i18n/fr/`. Il existe sept fichiers différents :
-
-* `admin.php` pour tout ce qui est relatif à l’administration de FreshRSS ;
-* `conf.php` pour l’aspect configuration ;
-* `feedback.php` contient les traductions des messages de feedback ;
-* `gen.php` stocke ce qui est global à FreshRSS (gen pour « general ») ;
-* `index.php` pour la page principale qui liste les flux et la page « À propos » ;
-* `install.php` contient les phrases relatives à l’installation de FreshRSS ;
-* `sub.php` pour l’aspect gestion des abonnements (sub pour « subscription »).
-
-Cette organisation permet de ne pas avoir un unique énorme fichier de
-traduction.
-
-Les fichiers de traduction sont assez simples : il s’agit seulement de
-retourner un tableau PHP contenant les traductions. Extrait du fichier
-`app/i18n/fr/gen.php` :
-
-```php
-<?php
-
-return array(
- 'action' => [
- 'actualize' => 'Actualiser',
- 'back_to_rss_feeds' => '← Retour à vos flux RSS',
- 'cancel' => 'Annuler',
- 'create' => 'Créer',
- 'disable' => 'Désactiver',
- ),
- 'freshrss' => array(
- '_' => 'FreshRSS',
- 'about' => 'À propos de FreshRSS',
- ),
-];
-
-?>
-```
-
-Pour accéder à ces traductions, `Minz_Translate` va nous aider à l’aide de
-sa méthode `Minz_Translate::t()`. Comme cela peut être un peu long à taper,
-il a été introduit un raccourci qui **doit** être utilisé en toutes
-circonstances : `_t()`. Exemple de code :
-
-```html
-<p>
- <a href="<?= _url('index', 'index') ?>">
- <?= _t('gen.action.back_to_rss_feeds') ?>
- </a>
-</p>
-```
+## Minz Framework
-La chaîne à passer à la fonction `_t()` consiste en une série d’identifiants
-séparés par des points. Le premier identifiant indique de quel fichier on
-veut extraire la traduction (dans notre cas présent, de `gen.php`), tandis
-que les suivantes indiquent des entrées de tableaux. Ainsi `action` est une
-entrée du tableau principal et `back_to_rss_feeds` est une entrée du tableau
-`action`. Cela permet d’organiser encore un peu plus nos fichiers de
-traduction.
-
-Il existe un petit cas particulier qui permet parfois de se simplifier la
-vie : le cas de l’identifiant `_`. Celui-ci doit nécessairement être présent
-en bout de chaîne et permet de donner une valeur à l’identifiant de niveau
-supérieur. C’est assez dur à expliquer mais très simple à comprendre. Dans
-l’exemple donné plus haut, un `_` est associé à la valeur `FreshRSS` : cela
-signifie qu’il n’y a pas besoin d’écrire `_t('gen.freshrss._')` mais
-`_t('gen.freshrss')` suffit.
-
-### Gestion de la configuration
+see [Minz documentation](/docs/fr/developers/Minz/index.md)
## Écrire une extension pour FreshRSS
diff --git a/docs/fr/developers/Minz/index.md b/docs/fr/developers/Minz/index.md
new file mode 100644
index 000000000..0d1d2124a
--- /dev/null
+++ b/docs/fr/developers/Minz/index.md
@@ -0,0 +1,249 @@
+# Minz
+
+Cette fiche technique devrait renvoyer vers la documentation officielle de
+FreshRSS et de Minz (le framework PHP sur lequel repose
+FreshRSS). Malheureusement cette documentation n’existe pas encore. Voici
+donc en quelques mots les principaux éléments à connaître. Il n’est pas
+nécessaire de lire l’ensemble des chapitres de cette section si vous n’avez
+pas à utiliser une fonctionnalité dans votre extension (si vous n’avez pas
+besoin de traduire votre extension, pas besoin d’en savoir plus sur le
+module `Minz_Translate` par exemple).
+
+## Architecture MVC
+
+Minz repose et impose une architecture MVC pour les projets l’utilisant. On
+distingue dans cette architecture trois composants principaux :
+
+* Le Modèle : c’est l’objet de base que l’on va manipuler. Dans FreshRSS,
+ les catégories, les flux et les articles sont des modèles. La partie du
+ code qui permet de les manipuler en base de données fait aussi partie du
+ modèle mais est séparée du modèle de base : on parle de DAO (pour « Data
+ Access Object »). Les modèles sont stockés dans un répertoire `Models`.
+* La Vue : c’est ce qui représente ce que verra l’utilisateur. La vue est
+ donc simplement du code HTML que l’on mixe avec du PHP pour afficher les
+ informations dynamiques. Les vues sont stockées dans un répertoire
+ `views`.
+* Le Contrôleur : c’est ce qui permet de lier modèles et vues entre
+ eux. Typiquement, un contrôleur va charger des modèles à partir de la base
+ de données (une liste d’articles par exemple) pour les « passer » à une
+ vue afin qu’elle les affiche. Les contrôleurs sont stockés dans un
+ répertoire `Controllers`.
+
+## Routage
+
+Afin de lier une URL à un contrôleur, on doit passer par une phase dite de «
+routage ». Dans FreshRSS, cela est particulièrement simple car il suffit
+d’indiquer le nom du contrôleur à charger dans l’URL à l’aide d’un paramètre `c`.
+Par exemple, l’adresse <http://exemple.com?c=hello> va exécuter le code
+contenu dans le contrôleur `hello`.
+
+Une notion qui n’a pas encore été évoquée est le système d'« actions ». Une
+action est exécutée *sur* un contrôleur. Concrètement, un contrôleur va être
+représenté par une classe et ses actions par des méthodes. Pour exécuter une
+action, il est nécessaire d’indiquer un paramètre `a` dans l’URL.
+
+Exemple de code :
+
+```php
+<?php
+
+class FreshRSS_hello_Controller extends FreshRSS_ActionController {
+ public function indexAction() {
+ $this->view->a_variable = 'FooBar';
+ }
+
+ public function worldAction() {
+ $this->view->a_variable = 'Hello World!';
+ }
+}
+
+?>
+```
+
+Si l’on charge l’adresse <http://exemple.com?c=hello&a=world>, l’action
+`world` va donc être exécutée sur le contrôleur `hello`.
+
+Note : si `c` ou `a` n’est pas précisée, la valeur par défaut de chacune de
+ces variables est `index`. Ainsi l’adresse <http://exemple.com?c=hello> va
+exécuter l’action `index` du contrôleur `hello`.
+
+Plus loin, sera utilisée la convention `hello/world` pour évoquer un couple
+contrôleur/action.
+
+## Vues
+
+Chaque vue est associée à un contrôleur et à une action. La vue associée à
+`hello/world` va être stockée dans un fichier bien spécifique :
+`views/hello/world.phtml`. Cette convention est imposée par Minz.
+
+Comme expliqué plus haut, les vues sont du code HTML mixé à du PHP. Exemple
+de code :
+
+```html
+<p>
+ Phrase passée en paramètre : <?= $this->a_variable ?>
+</p>
+```
+
+La variable `$this->a_variable` a été passée précédemment par le contrôleur (voir exemple précédent). La différence est que dans le contrôleur il est nécessaire de passer par `$this->view` et que dans la vue `$this` suffit.
+
+## Accéder aux paramètres GET / POST
+
+Il est souvent nécessaire de profiter des paramètres passés par GET ou par
+POST. Dans Minz, ces paramètres sont accessibles de façon indistincts à
+l’aide de la classe `Minz_Request`. Exemple de code :
+
+```php
+<?php
+
+$default_value = 'foo';
+$param = Minz_Request::param('bar', $default_value);
+
+// Affichera la valeur du paramètre `bar` (passé via GET ou POST)
+// ou "foo" si le paramètre n’existe pas.
+echo $param;
+
+// Force la valeur du paramètre `bar`
+Minz_Request::_param('bar', 'baz');
+
+// Affichera forcément "baz" puisque nous venons de forcer sa valeur.
+// Notez que le second paramètre (valeur par défaut) est facultatif.
+echo Minz_Request::param('bar');
+
+?>
+```
+
+La méthode `Minz_Request::isPost()` peut être utile pour n’exécuter un
+morceau de code que s’il s’agit d’une requête POST.
+
+Note : il est préférable de n’utiliser `Minz_Request` que dans les
+contrôleurs. Il est probable que vous rencontriez cette méthode dans les
+vues de FreshRSS, voire dans les modèles, mais sachez qu’il ne s’agit
+**pas** d’une bonne pratique.
+
+## Accéder aux paramètres de session
+
+L’accès aux paramètres de session est étrangement similaire aux paramètres
+GET / POST mais passe par la classe `Minz_Session` cette fois-ci ! Il n’y a
+pas d’exemple ici car vous pouvez reprendre le précédent en changeant tous
+les `Minz_Request` par des `Minz_Session`.
+
+## Gestion des URL
+
+Pour profiter pleinement du système de routage de Minz, il est fortement
+déconseillé d’écrire les URL en dur dans votre code. Par exemple, la vue
+suivante doit être évitée :
+
+```html
+<p>
+ Accéder à la page <a href="http://exemple.com?c=hello&amp;a=world">Hello world</a>!
+</p>
+```
+
+Si un jour il est décidé d’utiliser un système d'« url rewriting » pour
+avoir des adresses au format <http://exemple.com/controller/action>, toutes
+les adresses précédentes deviendraient ineffectives !
+
+Préférez donc l’utilisation de la classe `Minz_Url` et de sa méthode
+`display()`. `Minz_Url::display()` prend en paramètre un tableau de la forme
+suivante :
+
+```php
+<?php
+
+$url_array = [
+ 'c' => 'hello',
+ 'a' => 'world',
+ 'params' => [
+ 'foo' => 'bar',
+ ],
+];
+
+// Affichera quelque chose comme .?c=hello&amp;a=world&amp;foo=bar
+echo Minz_Url::display($url_array);
+
+?>
+```
+
+Comme cela peut devenir un peu pénible à utiliser à la longue, surtout dans
+les vues, il est préférable d’utiliser le raccourci `_url()` :
+
+```php
+<?php
+
+// Affichera la même chose que précédemment
+echo _url('hello', 'world', 'foo', 'bar');
+
+?>
+```
+
+Note : en règle générale, la forme raccourcie (`_url()`) doit être utilisée
+dans les vues tandis que la forme longue (`Minz_Url::display()`) doit être
+utilisée dans les contrôleurs.
+
+## Redirections
+
+Il est souvent nécessaire de rediriger un utilisateur vers une autre
+page. Pour cela, la classe `Minz_Request` dispose d’une autre méthode utile
+: `forward()`. Cette méthode prend en argument le même format d’URL que
+celui vu juste avant.
+
+Exemple de code :
+
+```php
+<?php
+
+$url_array = [
+ 'c' => 'hello',
+ 'a' => 'world',
+];
+
+// Indique à Minz de rediriger l’utilisateur vers la page hello/world.
+// Notez qu’il s’agit d’une redirection au sens Minz du terme, pas d’une redirection que le navigateur va avoir à gérer (code HTTP 301 ou 302)
+// Le code qui suit forward() va ainsi être exécuté !
+Minz_Request::forward($url_array);
+
+// Pour effectuer une redirection type 302, ajoutez "true".
+// Le code qui suivra ne sera alors jamais exécuté.
+Minz_Request::forward($url_array, true);
+
+?>
+```
+
+Il est très fréquent de vouloir effectuer une redirection tout en affichant
+un message à l’utilisateur pour lui indiquer comment s’est déroulée l’action
+effectuée juste avant (validation d’un formulaire par exemple). Un tel
+message est passé par une variable de session `notification` (note : nous
+parlerons plutôt de « feedback » désormais pour éviter la confusion avec une
+notification qui peut survenir à tout moment). Pour faciliter ce genre
+d’action très fréquente, il existe deux raccourcis qui effectuent tout deux
+une redirection type 302 en affectant un message de feedback :
+
+```php
+<?php
+
+$url_array = [
+ 'c' => 'hello',
+ 'a' => 'world',
+];
+$feedback_good = 'Tout s’est bien passé !';
+$feedback_bad = 'Oups, quelque chose n’a pas marché.';
+
+Minz_Request::good($feedback_good, $url_array);
+
+// ou
+
+Minz_Request::bad($feedback_bad, $url_array);
+
+?>
+```
+
+## Gestion de la traduction
+
+Cette partie est [expliquée dans la page dédiée](/docs/fr/internationalization.md).
+
+## Migration
+
+Existing documentation includes:
+
+* [How to manage migrations](migrations.md)
diff --git a/docs/fr/developers/Minz/migration.md b/docs/fr/developers/Minz/migration.md
new file mode 100644
index 000000000..ad2eef949
--- /dev/null
+++ b/docs/fr/developers/Minz/migration.md
@@ -0,0 +1,3 @@
+# Migration
+
+see [English documentation](/docs/en/developers/Minz/migrations.md)
diff --git a/docs/fr/internationalization.md b/docs/fr/internationalization.md
new file mode 100644
index 000000000..532ed457d
--- /dev/null
+++ b/docs/fr/internationalization.md
@@ -0,0 +1,79 @@
+# Gestion de la traduction
+
+Il est fréquent (et c’est un euphémisme) de vouloir afficher des phrases à
+l’utilisateur. Dans l’exemple précédent par exemple, nous affichions un
+feedback à l’utilisateur en fonction du résultat d’une validation de
+formulaire. Le problème est que FreshRSS possède des utilisateurs de
+différentes nationalités. Il est donc nécessaire de pouvoir gérer
+différentes langues pour ne pas rester cantonné à l’Anglais ou au Français.
+
+La solution consiste à utiliser la classe `Minz_Translate` qui permet de
+traduire dynamiquement FreshRSS (ou toute application basée sur Minz). Avant
+d’utiliser ce module, il est nécessaire de savoir où trouver les chaînes de
+caractères à traduire. Chaque langue possède son propre sous-répertoire dans
+un répertoire parent nommé `i18n`. Par exemple, les fichiers de langue en
+Français sont situés dans `i18n/fr/`. Il existe sept fichiers différents :
+
+* `admin.php` pour tout ce qui est relatif à l’administration de FreshRSS ;
+* `conf.php` pour l’aspect configuration ;
+* `feedback.php` contient les traductions des messages de feedback ;
+* `gen.php` stocke ce qui est global à FreshRSS (gen pour « general ») ;
+* `index.php` pour la page principale qui liste les flux et la page « À propos » ;
+* `install.php` contient les phrases relatives à l’installation de FreshRSS ;
+* `sub.php` pour l’aspect gestion des abonnements (sub pour « subscription »).
+
+Cette organisation permet de ne pas avoir un unique énorme fichier de
+traduction.
+
+Les fichiers de traduction sont assez simples : il s’agit seulement de
+retourner un tableau PHP contenant les traductions. Extrait du fichier
+`app/i18n/fr/gen.php` :
+
+```php
+<?php
+
+return array(
+ 'action' => [
+ 'actualize' => 'Actualiser',
+ 'back_to_rss_feeds' => '← Retour à vos flux RSS',
+ 'cancel' => 'Annuler',
+ 'create' => 'Créer',
+ 'disable' => 'Désactiver',
+ ),
+ 'freshrss' => array(
+ '_' => 'FreshRSS',
+ 'about' => 'À propos de FreshRSS',
+ ),
+];
+
+?>
+```
+
+Pour accéder à ces traductions, `Minz_Translate` va nous aider à l’aide de
+sa méthode `Minz_Translate::t()`. Comme cela peut être un peu long à taper,
+il a été introduit un raccourci qui **doit** être utilisé en toutes
+circonstances : `_t()`. Exemple de code :
+
+```html
+<p>
+ <a href="<?= _url('index', 'index') ?>">
+ <?= _t('gen.action.back_to_rss_feeds') ?>
+ </a>
+</p>
+```
+
+La chaîne à passer à la fonction `_t()` consiste en une série d’identifiants
+séparés par des points. Le premier identifiant indique de quel fichier on
+veut extraire la traduction (dans notre cas présent, de `gen.php`), tandis
+que les suivantes indiquent des entrées de tableaux. Ainsi `action` est une
+entrée du tableau principal et `back_to_rss_feeds` est une entrée du tableau
+`action`. Cela permet d’organiser encore un peu plus nos fichiers de
+traduction.
+
+Il existe un petit cas particulier qui permet parfois de se simplifier la
+vie : le cas de l’identifiant `_`. Celui-ci doit nécessairement être présent
+en bout de chaîne et permet de donner une valeur à l’identifiant de niveau
+supérieur. C’est assez dur à expliquer mais très simple à comprendre. Dans
+l’exemple donné plus haut, un `_` est associé à la valeur `FreshRSS` : cela
+signifie qu’il n’y a pas besoin d’écrire `_t('gen.freshrss._')` mais
+`_t('gen.freshrss')` suffit.
diff --git a/docs/fr/users/01_Installation.md b/docs/fr/users/01_Installation.md
index 0a04206c2..221e0ff4e 100644
--- a/docs/fr/users/01_Installation.md
+++ b/docs/fr/users/01_Installation.md
@@ -7,7 +7,7 @@ Il est toutefois de votre responsabilité de vérifier que votre hébergement pe
| Logiciel | Recommandé | Fonctionne aussi avec |
| -------- | ----------- | --------------------- |
| Serveur web | **Apache 2** | Nginx |
-| PHP | **PHP 7+** | |
+| PHP | **PHP 7.2+** | |
| Modules PHP | Requis : libxml, cURL, JSON, PDO_MySQL, PCRE et ctype<br />Requis (32 bits seulement) : GMP<br />Recommandé : Zlib, mbstring et iconv, ZipArchive<br />*Pour une liste complète des modules nécessaires voir le [Dockerfile](https://github.com/FreshRSS/FreshRSS/blob/edge/Docker/Dockerfile-Alpine#L7-L9)* | |
| Base de données | **MySQL 5.5.3+** | SQLite 3.7.4+, PostgreSQL 9.5+ |
| Navigateur | **Firefox** | Chrome, Opera, Safari, or Edge |
diff --git a/docs/fr/users/03_Main_view.md b/docs/fr/users/03_Main_view.md
index 3a65c1f7f..3ca3b907c 100644
--- a/docs/fr/users/03_Main_view.md
+++ b/docs/fr/users/03_Main_view.md
@@ -275,3 +275,5 @@ Enfin, les parenthèses peuvent être utilisées pour des expressions plus compl
* `!((author:Alice intitle:bonjour) OR (author:Bob intitle:monde))`
* `(author:Alice intitle:bonjour) !(author:Bob intitle:monde)`
* `!(S:1 OR S:2)`
+
+> ℹ️ Si vous devez chercher une parenthèse, elle doit être *échappée* comme suit : `\(` ou `\)`
diff --git a/docs/fr/users/05_Configuration.md b/docs/fr/users/05_Configuration.md
index 88478a280..c64572d01 100644
--- a/docs/fr/users/05_Configuration.md
+++ b/docs/fr/users/05_Configuration.md
@@ -21,16 +21,23 @@ pt-br, ru, tr, zh-cn.
## Thème
Les goûts et les couleurs, ça ne se discute pas. C’est pourquoi FreshRSS
-propose huit thèmes officiels :
-
-* *Blue Lagoon* par **Mister aiR**
-* *Dark* par **AD**
-* *Flat design* par **Marien Fressinaud**
-* *Origine* par **Marien Fressinaud**
-* *Origine-compact* par **Kevin Papst**
-* *Pafat* par **Plopoyop**
-* *Screwdriver* par **Mister aiR**
-* *Swage* par **Patrick Crandol**
+propose 13 thèmes officiels :
+
+| Thème | Auteur | Notes |
+|:--------------|:-------------------------------------------------------|:--------------------------------------------------------------|
+| Alternative Dark | Ghost | |
+| Ansum | Thomas Guesnon | |
+| Blue Lagoon |Mister aiR | N'est plus pris en charge. Sera supprimé avec FreshRSS V1.22.0 |
+| Dark | AD | |
+| Dark pink | Miicat_47 | |
+| Flat design | Marien Fressinaud | N'est plus pris en charge. Sera supprimé avec FreshRSS V1.22.0 |
+| Mapco | Thomas Guesnon | |
+| Nord theme | joelchrono12 | |
+| Origine | Marien Fressinaud | (default theme) |
+| Origine-compact | Kevin Papst | |
+| Pafat | Plopoyop | |
+| Screwdriver | Mister aiR | N'est plus pris en charge. Sera supprimé avec FreshRSS V1.22.0 |
+| Swage | Patrick Crandol | |
Si aucun de ceux proposés ne convient, il est toujours possible de [créer
son propre thème](../developers/04_Frontend/02_Design.md).
diff --git a/lib/.gitignore b/lib/.gitignore
index 812bbfe76..a1df80381 100644
--- a/lib/.gitignore
+++ b/lib/.gitignore
@@ -1,6 +1,14 @@
autoload.php
composer.lock
composer/
+marienfressinaud/lib_opml/.git/
+marienfressinaud/lib_opml/.gitlab-ci.yml
+marienfressinaud/lib_opml/.gitlab/
+marienfressinaud/lib_opml/ci/
+marienfressinaud/lib_opml/examples/
+marienfressinaud/lib_opml/Makefile
+marienfressinaud/lib_opml/src/functions.php
+marienfressinaud/lib_opml/tests/
phpgt/cssxpath/.*
phpgt/cssxpath/composer.json
phpgt/cssxpath/CONTRIBUTING.md
diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php
index 403d6ccba..6d4aed0ab 100644
--- a/lib/Minz/Configuration.php
+++ b/lib/Minz/Configuration.php
@@ -26,7 +26,7 @@ class Minz_Configuration {
* @param object $configuration_setter an optional helper to set values in configuration
*/
public static function register($namespace, $config_filename, $default_filename = null, $configuration_setter = null) {
- self::$config_list[$namespace] = new Minz_Configuration(
+ self::$config_list[$namespace] = new static(
$namespace, $config_filename, $default_filename, $configuration_setter
);
}
@@ -51,7 +51,7 @@ class Minz_Configuration {
* Return the configuration related to a given namespace.
*
* @param string $namespace the name of the configuration to get.
- * @return Minz_Configuration object
+ * @return static object
* @throws Minz_ConfigurationNamespaceException if the namespace does not exist.
*/
public static function get($namespace) {
@@ -117,7 +117,7 @@ class Minz_Configuration {
* @param string $default_filename the file containing default values, null by default.
* @param object $configuration_setter an optional helper to set values in configuration
*/
- private function __construct($namespace, $config_filename, $default_filename = null, $configuration_setter = null) {
+ private final function __construct($namespace, $config_filename, $default_filename = null, $configuration_setter = null) {
$this->namespace = $namespace;
$this->config_filename = $config_filename;
$this->default_filename = $default_filename;
diff --git a/lib/Minz/Migrator.php b/lib/Minz/Migrator.php
index 0f28237c5..ef89a3b55 100644
--- a/lib/Minz/Migrator.php
+++ b/lib/Minz/Migrator.php
@@ -55,7 +55,7 @@ class Minz_Migrator
}
$lock_path = $applied_migrations_path . '.lock';
- if (!@mkdir($lock_path)) {
+ if (!@mkdir($lock_path, 0770, true)) {
// Someone is probably already executing the migrations (the folder
// already exists).
// We should probably return something else, but we don't want the
diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php
index 0f5b9efca..85796b53a 100644
--- a/lib/Minz/ModelPdo.php
+++ b/lib/Minz/ModelPdo.php
@@ -26,7 +26,7 @@ class Minz_ModelPdo {
private static $sharedCurrentUser;
/**
- * @var Minz_Pdo|null
+ * @var Minz_Pdo
*/
protected $pdo;
diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php
index 19a86a00e..07d48ec08 100644
--- a/lib/Minz/Translate.php
+++ b/lib/Minz/Translate.php
@@ -87,10 +87,10 @@ class Minz_Translate {
* preferred languages then returns the default language
* @param string|null $user the connected user language (nullable)
* @param array<string> $preferred an array of the preferred languages
- * @param string $default the preferred language to use
+ * @param string|null $default the preferred language to use
* @return string containing the language to use
*/
- public static function getLanguage($user, $preferred, $default) {
+ public static function getLanguage(?string $user, array $preferred, ?string $default): string {
if (null !== $user) {
return $user;
}
@@ -232,7 +232,7 @@ class Minz_Translate {
}
// Get the facultative arguments to replace i18n variables.
- return vsprintf($translation_value, $args);
+ return empty($args) ? $translation_value : vsprintf($translation_value, $args);
}
/**
diff --git a/lib/Minz/View.php b/lib/Minz/View.php
index 8faeb9078..459ef1e23 100644
--- a/lib/Minz/View.php
+++ b/lib/Minz/View.php
@@ -202,36 +202,38 @@ class Minz_View {
$styles = '';
foreach(self::$styles as $style) {
- $cond = $style['cond'];
- if ($cond) {
- $styles .= '<!--[if ' . $cond . ']>';
- }
-
$styles .= '<link rel="stylesheet" ' .
($style['media'] === 'all' ? '' : 'media="' . $style['media'] . '" ') .
'href="' . $style['url'] . '" />';
- if ($cond) {
- $styles .= '<![endif]-->';
- }
-
$styles .= "\n";
}
return $styles;
}
- public static function prependStyle ($url, $media = 'all', $cond = false) {
+ /**
+ * Prepends a <link> element referencing stylesheet.
+ *
+ * @param string $url
+ * @param string $media
+ * @param bool $cond Conditional comment for IE, now deprecated and ignored
+ */
+ public static function prependStyle($url, $media = 'all', $cond = false) {
array_unshift (self::$styles, array (
'url' => $url,
'media' => $media,
- 'cond' => $cond
));
}
- public static function appendStyle ($url, $media = 'all', $cond = false) {
+ /**
+ * Append a `<link>` element referencing stylesheet.
+ * @param string $url
+ * @param string $media
+ * @param bool $cond Conditional comment for IE, now deprecated and ignored
+ */
+ public static function appendStyle($url, $media = 'all', $cond = false) {
self::$styles[] = array (
'url' => $url,
'media' => $media,
- 'cond' => $cond
);
}
@@ -242,11 +244,6 @@ class Minz_View {
$scripts = '';
foreach (self::$scripts as $script) {
- $cond = $script['cond'];
- if ($cond) {
- $scripts .= '<!--[if ' . $cond . ']>';
- }
-
$scripts .= '<script src="' . $script['url'] . '"';
if (!empty($script['id'])) {
$scripts .= ' id="' . $script['id'] . '"';
@@ -258,29 +255,38 @@ class Minz_View {
$scripts .= ' async="async"';
}
$scripts .= '></script>';
-
- if ($cond) {
- $scripts .= '<![endif]-->';
- }
-
$scripts .= "\n";
}
return $scripts;
}
- public static function prependScript ($url, $cond = false, $defer = true, $async = true, $id = '') {
+ /**
+ * Prepend a `<script>` element.
+ * @param string $url
+ * @param bool $cond Conditional comment for IE, now deprecated and ignored
+ * @param bool $defer Use `defer` flag
+ * @param bool $async Use `async` flag
+ * @param string $id Add a script `id` attribute
+ */
+ public static function prependScript($url, $cond = false, $defer = true, $async = true, $id = '') {
array_unshift(self::$scripts, array (
'url' => $url,
- 'cond' => $cond,
'defer' => $defer,
'async' => $async,
'id' => $id,
));
}
- public static function appendScript ($url, $cond = false, $defer = true, $async = true, $id = '') {
+/**
+ * Append a `<script>` element.
+ * @param string $url
+ * @param bool $cond Conditional comment for IE, now deprecated and ignored
+ * @param bool $defer Use `defer` flag
+ * @param bool $async Use `async` flag
+ * @param string $id Add a script `id` attribute
+ */
+ public static function appendScript($url, $cond = false, $defer = true, $async = true, $id = '') {
self::$scripts[] = array (
'url' => $url,
- 'cond' => $cond,
'defer' => $defer,
'async' => $async,
'id' => $id,
diff --git a/lib/SimplePie/SimplePie.php b/lib/SimplePie/SimplePie.php
index 9983f577e..c0b2e24f6 100644
--- a/lib/SimplePie/SimplePie.php
+++ b/lib/SimplePie/SimplePie.php
@@ -418,6 +418,12 @@ define('SIMPLEPIE_FILE_SOURCE_FILE_GET_CONTENTS', 16);
*/
class SimplePie
{
+
+ /**
+ * @internal Default value of the HTTP Accept header when fetching/locating feeds
+ */
+ public const DEFAULT_HTTP_ACCEPT_HEADER = 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1';
+
/**
* @var array Raw data
* @access private
@@ -1690,7 +1696,7 @@ class SimplePie
else
{
$headers = array(
- 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
+ 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER,
);
if (isset($this->data['headers']['last-modified']))
{
@@ -1754,7 +1760,7 @@ class SimplePie
else
{
$headers = array(
- 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
+ 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER,
);
$file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options, $this->syslog_enabled));
}
diff --git a/lib/SimplePie/SimplePie/Enclosure.php b/lib/SimplePie/SimplePie/Enclosure.php
index cc0d038b5..04bade09f 100644
--- a/lib/SimplePie/SimplePie/Enclosure.php
+++ b/lib/SimplePie/SimplePie/Enclosure.php
@@ -627,7 +627,7 @@ class SimplePie_Enclosure
{
if ($this->link !== null)
{
- return urldecode($this->link);
+ return $this->link;
}
return null;
diff --git a/lib/SimplePie/SimplePie/Item.php b/lib/SimplePie/SimplePie/Item.php
index 2fb1d3284..1285fd536 100644
--- a/lib/SimplePie/SimplePie/Item.php
+++ b/lib/SimplePie/SimplePie/Item.php
@@ -427,7 +427,16 @@ class SimplePie_Item
{
if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'thumbnail'))
{
- $this->data['thumbnail'] = $return[0]['attribs'][''];
+ $thumbnail = $return[0]['attribs'][''];
+ if (empty($thumbnail['url']))
+ {
+ $this->data['thumbnail'] = null;
+ }
+ else
+ {
+ $thumbnail['url'] = $this->sanitize($thumbnail['url'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0]));
+ $this->data['thumbnail'] = $thumbnail;
+ }
}
else
{
@@ -2847,9 +2856,9 @@ class SimplePie_Item
}
}
- if ($enclosure = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'enclosure'))
+ foreach ($this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'enclosure') ?? [] as $enclosure)
{
- if (isset($enclosure[0]['attribs']['']['url']))
+ if (isset($enclosure['attribs']['']['url']))
{
// Attributes
$bitrate = null;
@@ -2867,15 +2876,15 @@ class SimplePie_Item
$url = null;
$width = null;
- $url = $this->sanitize($enclosure[0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($enclosure[0]));
+ $url = $this->sanitize($enclosure['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($enclosure));
$url = $this->feed->sanitize->https_url($url);
- if (isset($enclosure[0]['attribs']['']['type']))
+ if (isset($enclosure['attribs']['']['type']))
{
- $type = $this->sanitize($enclosure[0]['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT);
+ $type = $this->sanitize($enclosure['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT);
}
- if (isset($enclosure[0]['attribs']['']['length']))
+ if (isset($enclosure['attribs']['']['length']))
{
- $length = intval($enclosure[0]['attribs']['']['length']);
+ $length = intval($enclosure['attribs']['']['length']);
}
// Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor
diff --git a/lib/SimplePie/SimplePie/Locator.php b/lib/SimplePie/SimplePie/Locator.php
index c5fae0579..10b50cadf 100644
--- a/lib/SimplePie/SimplePie/Locator.php
+++ b/lib/SimplePie/SimplePie/Locator.php
@@ -256,7 +256,7 @@ class SimplePie_Locator
{
$this->checked_feeds++;
$headers = array(
- 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
+ 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER,
);
$feed = $this->registry->create('File', array($href, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options));
if ($feed->success && ($feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed, true))
@@ -386,7 +386,7 @@ class SimplePie_Locator
$this->checked_feeds++;
$headers = array(
- 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
+ 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER,
);
$feed = $this->registry->create('File', array($value, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options));
if ($feed->success && ($feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed))
@@ -414,9 +414,9 @@ class SimplePie_Locator
{
$this->checked_feeds++;
$headers = array(
- 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
+ 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER,
);
- $feed = $this->registry->create('File', array($value, $this->timeout, 5, null, $this->useragent, $this->force_fsockopen, $this->curl_options));
+ $feed = $this->registry->create('File', array($value, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options));
if ($feed->success && ($feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed))
{
return array($feed);
diff --git a/lib/SimplePie/SimplePie/Registry.php b/lib/SimplePie/SimplePie/Registry.php
index 1aac51d07..1aac51d07 100755..100644
--- a/lib/SimplePie/SimplePie/Registry.php
+++ b/lib/SimplePie/SimplePie/Registry.php
diff --git a/lib/composer.json b/lib/composer.json
index 017adfea6..6e9e0ee32 100644
--- a/lib/composer.json
+++ b/lib/composer.json
@@ -11,7 +11,8 @@
}
],
"require": {
- "php": ">=7.0.0",
+ "php": ">=7.2.0",
+ "marienfressinaud/lib_opml": "0.5.0",
"phpgt/cssxpath": "dev-master#4fbe420aba3d9e729940107ded4236a835a1a132",
"phpmailer/phpmailer": "6.6.0"
},
diff --git a/lib/http-conditional.php b/lib/http-conditional.php
index 853fdf983..6c7c89d32 100644
--- a/lib/http-conditional.php
+++ b/lib/http-conditional.php
@@ -7,7 +7,7 @@
- Possibility to control cache for client and proxies (public or private policy, life time).
- When $feedMode is set to true, in the case of a RSS/ATOM feed,
it puts a timestamp in the global variable $clientCacheDate to allow the sending of only the articles newer than the client's cache.
- - When $compression is set to true, compress the data before sending it to the client and persitent connections are allowed.
+ - When $compression is set to true, compress the data before sending it to the client and persistent connections are allowed.
- When $session is set to true, automatically checks if $_SESSION has been modified during the last generation the document.
Interface:
diff --git a/lib/lib_install.php b/lib/lib_install.php
index 494ddc6dd..931de21a2 100644
--- a/lib/lib_install.php
+++ b/lib/lib_install.php
@@ -1,7 +1,5 @@
<?php
-define('BCRYPT_COST', 9);
-
Minz_Configuration::register('default_system', join_path(FRESHRSS_PATH, 'config.default.php'));
Minz_Configuration::register('default_user', join_path(FRESHRSS_PATH, 'config-user.default.php'));
@@ -42,14 +40,14 @@ function checkRequirements($dbType = '') {
$json = function_exists('json_encode');
$mbstring = extension_loaded('mbstring');
// @phpstan-ignore-next-line
- $data = DATA_PATH && is_writable(DATA_PATH);
+ $data = DATA_PATH && touch(DATA_PATH . '/index.html'); // is_writable() is not reliable for a folder on NFS
// @phpstan-ignore-next-line
- $cache = CACHE_PATH && is_writable(CACHE_PATH);
+ $cache = CACHE_PATH && touch(CACHE_PATH . '/index.html');
// @phpstan-ignore-next-line
$tmp = TMP_PATH && is_writable(TMP_PATH);
// @phpstan-ignore-next-line
- $users = USERS_PATH && is_writable(USERS_PATH);
- $favicons = is_writable(join_path(DATA_PATH, 'favicons'));
+ $users = USERS_PATH && touch(USERS_PATH . '/index.html');
+ $favicons = touch(DATA_PATH . '/favicons/index.html');
return array(
'php' => $php ? 'ok' : 'ko',
diff --git a/lib/lib_opml.php b/lib/lib_opml.php
deleted file mode 100644
index f86d780b7..000000000
--- a/lib/lib_opml.php
+++ /dev/null
@@ -1,353 +0,0 @@
-<?php
-
-/**
- * lib_opml is a free library to manage OPML format in PHP.
- *
- * By default, it takes in consideration version 2.0 but can be compatible with
- * OPML 1.0. More information on http://dev.opml.org.
- * Difference is "text" attribute is optional in version 1.0. It is highly
- * recommended to use this attribute.
- *
- * lib_opml requires SimpleXML (php.net/simplexml) and DOMDocument (php.net/domdocument)
- *
- * @author Marien Fressinaud <dev@marienfressinaud.fr>
- * @link https://github.com/marienfressinaud/lib_opml
- * @version 0.2-FreshRSS~1.20.0
- * @license public domain
- *
- * Usages:
- * > include('lib_opml.php');
- * > $filename = 'my_opml_file.xml';
- * > $opml_array = libopml_parse_file($filename);
- * > print_r($opml_array);
- *
- * > $opml_string = [...];
- * > $opml_array = libopml_parse_string($opml_string);
- * > print_r($opml_array);
- *
- * > $opml_array = [...];
- * > $opml_string = libopml_render($opml_array);
- * > $opml_object = libopml_render($opml_array, true);
- * > echo $opml_string;
- * > print_r($opml_object);
- *
- * You can set $strict argument to false if you want to bypass "text" attribute
- * requirement.
- *
- * If parsing fails for any reason (e.g. not an XML string, does not match with
- * the specifications), a LibOPML_Exception is raised.
- *
- * lib_opml array format is described here:
- * $array = array(
- * 'head' => array( // 'head' element is optional (but recommended)
- * 'key' => 'value', // key must be a part of available OPML head elements
- * ),
- * 'body' => array( // body is required
- * array( // this array represents an outline (at least one)
- * 'text' => 'value', // 'text' element is required if $strict is true
- * 'key' => 'value', // key and value are what you want (optional)
- * '@outlines' = array( // @outlines is a special value and represents sub-outlines
- * array(
- * [...] // where [...] is a valid outline definition
- * ),
- * ),
- * ),
- * array( // other outline definitions
- * [...]
- * ),
- * [...],
- * )
- * )
- *
- */
-
-/**
- * A simple Exception class which represents any kind of OPML problem.
- * Message should precise the current problem.
- */
-class LibOPML_Exception extends Exception {}
-
-
-// Define the list of available head attributes. All of them are optional.
-define('HEAD_ELEMENTS', serialize(array(
- 'title', 'dateCreated', 'dateModified', 'ownerName', 'ownerEmail',
- 'ownerId', 'docs', 'expansionState', 'vertScrollState', 'windowTop',
- 'windowLeft', 'windowBottom', 'windowRight'
-)));
-
-
-/**
- * Parse an XML object as an outline object and return corresponding array
- *
- * @param SimpleXMLElement $outline_xml the XML object we want to parse
- * @param bool $strict true if "text" attribute is required, false else
- * @return array corresponding to an outline and following format described above
- * @throws LibOPML_Exception
- * @access private
- */
-function libopml_parse_outline($outline_xml, $strict = true) {
- $outline = array();
-
- // An outline may contain any kind of attributes but "text" attribute is
- // required !
- $text_is_present = false;
-
- $elem = dom_import_simplexml($outline_xml);
- /** @var DOMAttr $attr */
- foreach ($elem->attributes as $attr) {
- $key = $attr->localName;
-
- if ($attr->namespaceURI == '') {
- $outline[$key] = $attr->value;
- } else {
- $outline[$key] = [
- 'namespace' => $attr->namespaceURI,
- 'value' => $attr->value,
- ];
- }
-
- if ($key === 'text') {
- $text_is_present = true;
- }
- }
-
- if (!$text_is_present && $strict) {
- throw new LibOPML_Exception(
- 'Outline does not contain any text attribute'
- );
- }
-
- if (empty($outline['text']) && isset($outline['title'])) {
- $outline['text'] = $outline['title'];
- }
-
- foreach ($outline_xml->children() as $key => $value) {
- // An outline may contain any number of outline children
- if ($key === 'outline') {
- $outline['@outlines'][] = libopml_parse_outline($value, $strict);
- }
- }
-
- return $outline;
-}
-
-/**
- * Reformat the XML document as a hierarchy when
- * the OPML 2.0 category attribute is used
- */
-function preprocessing_categories($doc) {
- $outline_categories = array();
- $body = $doc->getElementsByTagName('body')->item(0);
- $xpath = new DOMXpath($doc);
- $outlines = $xpath->query('/opml/body/outline[@category]');
- foreach ($outlines as $outline) {
- $category = trim($outline->getAttribute('category'));
- if ($category != '') {
- $outline_category = null;
- if (!isset($outline_categories[$category])) {
- $outline_category = $doc->createElement('outline');
- $outline_category->setAttribute('text', $category);
- $body->insertBefore($outline_category, $body->firstChild);
- $outline_categories[$category] = $outline_category;
- } else {
- $outline_category = $outline_categories[$category];
- }
- $outline->parentNode->removeChild($outline);
- $outline_category->appendChild($outline);
- }
- }
-}
-
-/**
- * Parse a string as a XML one and returns the corresponding array
- *
- * @param string $xml is the string we want to parse
- * @param bool $strict true to perform some validation (e.g. require "text" attribute), false to relax
- * @return array corresponding to the XML string and following format described above
- * @throws LibOPML_Exception
- * @access public
- */
-function libopml_parse_string($xml, $strict = true) {
- $dom = new DOMDocument();
- $dom->recover = true;
- $dom->strictErrorChecking = false;
- $dom->loadXML($xml);
- $dom->encoding = 'UTF-8';
-
- //Partial compatibility with the category attribute of OPML 2.0
- preprocessing_categories($dom);
-
- $opml = simplexml_import_dom($dom);
-
- if (!$opml) {
- throw new LibOPML_Exception();
- }
-
- $array = array(
- 'version' => (string)$opml['version'],
- 'head' => array(),
- 'body' => array()
- );
-
- if (isset($opml->head)) {
- // We get all "head" elements. Head is required but its sub-elements are optional.
- foreach ($opml->head->children() as $key => $value) {
- if (in_array($key, unserialize(HEAD_ELEMENTS), true)) {
- $array['head'][$key] = (string)$value;
- } elseif ($strict) {
- throw new LibOPML_Exception($key . ' is not part of the OPML 2.0 specification');
- }
- }
- } elseif ($strict) {
- throw new LibOPML_Exception('Required OPML head element is missing!');
- }
-
- // Then, we get body oulines. Body must contain at least one outline
- // element.
- $at_least_one_outline = false;
- foreach ($opml->body->children() as $key => $value) {
- if ($key === 'outline') {
- $at_least_one_outline = true;
- $array['body'][] = libopml_parse_outline($value, $strict);
- }
- }
-
- if (!$at_least_one_outline) {
- throw new LibOPML_Exception(
- 'OPML body must contain at least one outline element'
- );
- }
-
- return $array;
-}
-
-
-/**
- * Parse a string contained into a file as a XML string and returns the corresponding array
- *
- * @param string $filename should indicates a valid XML file
- * @param bool $strict true if "text" attribute is required, false else
- * @return array corresponding to the file content and following format described above
- * @throws LibOPML_Exception
- * @access public
- */
-function libopml_parse_file($filename, $strict = true) {
- $file_content = file_get_contents($filename);
-
- if ($file_content === false) {
- throw new LibOPML_Exception(
- $filename . ' cannot be found'
- );
- }
-
- return libopml_parse_string($file_content, $strict);
-}
-
-
-/**
- * Create a XML outline object in a parent object.
- *
- * @param SimpleXMLElement $parent_elt is the parent object of current outline
- * @param array $outline array representing an outline object
- * @param bool $strict true if "text" attribute is required, false else
- * @throws LibOPML_Exception
- * @access private
- */
-function libopml_render_outline($parent_elt, $outline, $strict) {
- // Outline MUST be an array!
- if (!is_array($outline)) {
- throw new LibOPML_Exception(
- 'Outline element must be defined as array'
- );
- }
-
- $outline_elt = $parent_elt->addChild('outline');
- $text_is_present = false;
- /** @var string|array<string,mixed> $value */
- foreach ($outline as $key => $value) {
- // Only outlines can be an array and so we consider children are also
- // outline elements.
- if ($key === '@outlines' && is_array($value)) {
- foreach ($value as $outline_child) {
- libopml_render_outline($outline_elt, $outline_child, $strict);
- }
- } elseif (is_array($value) && !isset($value['namespace'])) {
- throw new LibOPML_Exception(
- 'Type of outline elements cannot be array (except for providing a namespace): ' . $key
- );
- } else {
- // Detect text attribute is present, that's good :)
- if ($key === 'text') {
- $text_is_present = true;
- }
- if (is_array($value)) {
- if (!empty($value['namespace']) && !empty($value['value'])) {
- $outline_elt->addAttribute($key, $value['value'], $value['namespace']);
- }
- } else {
- $outline_elt->addAttribute($key, $value);
- }
- }
- }
-
- if (!$text_is_present && $strict) {
- throw new LibOPML_Exception(
- 'You must define at least a text element for all outlines'
- );
- }
-}
-
-
-/**
- * Render an array as an OPML string or a XML object.
- *
- * @param array $array is the array we want to render and must follow structure defined above
- * @param bool $as_xml_object false if function must return a string, true for a XML object
- * @param bool $strict true if "text" attribute is required, false else
- * @return string|SimpleXMLElement XML string corresponding to $array or XML object
- * @throws LibOPML_Exception
- * @access public
- */
-function libopml_render($array, $as_xml_object = false, $strict = true) {
- $opml = new SimpleXMLElement('<opml></opml>');
- $opml->addAttribute('version', $strict ? '2.0' : '1.0');
-
- // Create head element. $array['head'] is optional but head element will
- // exist in the final XML object.
- $head = $opml->addChild('head');
- if (isset($array['head'])) {
- foreach ($array['head'] as $key => $value) {
- if (in_array($key, unserialize(HEAD_ELEMENTS), true)) {
- $head->addChild($key, $value);
- }
- }
- }
-
- // Check body is set and contains at least one element
- if (!isset($array['body'])) {
- throw new LibOPML_Exception(
- '$array must contain a body element'
- );
- }
- if (count($array['body']) <= 0) {
- throw new LibOPML_Exception(
- 'Body element must contain at least one element (array)'
- );
- }
-
- // Create outline elements
- $body = $opml->addChild('body');
- foreach ($array['body'] as $outline) {
- libopml_render_outline($body, $outline, $strict);
- }
-
- // And return the final result
- if ($as_xml_object) {
- return $opml;
- } else {
- $dom = dom_import_simplexml($opml)->ownerDocument;
- $dom->formatOutput = true;
- $dom->encoding = 'UTF-8';
- return $dom->saveXML();
- }
-}
diff --git a/lib/lib_rss.php b/lib/lib_rss.php
index 592ad8149..434d0f9fb 100644
--- a/lib/lib_rss.php
+++ b/lib/lib_rss.php
@@ -4,8 +4,8 @@ if (version_compare(PHP_VERSION, FRESHRSS_MIN_PHP_VERSION, '<')) {
}
if (!function_exists('mb_strcut')) {
- function mb_strcut($str, $start, $length = null, $encoding = 'UTF-8') {
- return substr($str, $start, $length);
+ function mb_strcut(string $str, int $start, ?int $length = null, string $encoding = 'UTF-8'): string {
+ return substr($str, $start, $length) ?: '';
}
}
@@ -16,11 +16,27 @@ if (!function_exists('str_starts_with')) {
}
}
-// @phpstan-ignore-next-line
-if (COPY_SYSLOG_TO_STDERR) {
- openlog('FreshRSS', LOG_CONS | LOG_ODELAY | LOG_PID | LOG_PERROR, LOG_USER);
-} else {
- openlog('FreshRSS', LOG_CONS | LOG_ODELAY | LOG_PID, LOG_USER);
+if (!function_exists('syslog')) {
+ // @phpstan-ignore-next-line
+ if (COPY_SYSLOG_TO_STDERR && !defined('STDERR')) {
+ define('STDERR', fopen('php://stderr', 'w'));
+ }
+ function syslog(int $priority, string $message): bool {
+ // @phpstan-ignore-next-line
+ if (COPY_SYSLOG_TO_STDERR && defined('STDERR') && STDERR) {
+ return fwrite(STDERR, $message . "\n") != false;
+ }
+ return false;
+ }
+}
+
+if (function_exists('openlog')) {
+ // @phpstan-ignore-next-line
+ if (COPY_SYSLOG_TO_STDERR) {
+ openlog('FreshRSS', LOG_CONS | LOG_ODELAY | LOG_PID | LOG_PERROR, LOG_USER);
+ } else {
+ openlog('FreshRSS', LOG_CONS | LOG_ODELAY | LOG_PID, LOG_USER);
+ }
}
/**
@@ -34,7 +50,7 @@ function join_path(...$path_parts): string {
}
//<Auto-loading>
-function classAutoloader($class) {
+function classAutoloader(string $class): void {
if (strpos($class, 'FreshRSS') === 0) {
$components = explode('_', $class);
switch (count($components)) {
@@ -57,6 +73,11 @@ function classAutoloader($class) {
$base_dir = LIB_PATH . '/phpgt/cssxpath/src/';
$relative_class_name = substr($class, strlen($prefix));
require $base_dir . str_replace('\\', '/', $relative_class_name) . '.php';
+ } elseif (str_starts_with($class, 'marienfressinaud\\LibOpml\\')) {
+ $prefix = 'marienfressinaud\\LibOpml\\';
+ $base_dir = LIB_PATH . '/marienfressinaud/lib_opml/src/LibOpml/';
+ $relative_class_name = substr($class, strlen($prefix));
+ require $base_dir . str_replace('\\', '/', $relative_class_name) . '.php';
} elseif (str_starts_with($class, 'PHPMailer\\PHPMailer\\')) {
$prefix = 'PHPMailer\\PHPMailer\\';
$base_dir = LIB_PATH . '/phpmailer/phpmailer/src/';
@@ -68,14 +89,10 @@ function classAutoloader($class) {
spl_autoload_register('classAutoloader');
//</Auto-loading>
-/**
- * @param string $url
- * @return string
- */
-function idn_to_puny($url) {
+function idn_to_puny(string $url): string {
if (function_exists('idn_to_ascii')) {
$idn = parse_url($url, PHP_URL_HOST);
- if ($idn != '') {
+ if (is_string($idn) && $idn != '') {
// https://wiki.php.net/rfc/deprecate-and-remove-intl_idna_variant_2003
if (defined('INTL_IDNA_VARIANT_UTS46')) {
$puny = idn_to_ascii($idn, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
@@ -85,7 +102,7 @@ function idn_to_puny($url) {
$puny = idn_to_ascii($idn);
}
$pos = strpos($url, $idn);
- if ($puny != '' && $pos !== false) {
+ if ($puny != false && $pos !== false) {
$url = substr_replace($url, $puny, $pos, strlen($idn));
}
}
@@ -94,11 +111,9 @@ function idn_to_puny($url) {
}
/**
- * @param string $url
- * @param bool $fixScheme
* @return string|false
*/
-function checkUrl($url, $fixScheme = true) {
+function checkUrl(string $url, bool $fixScheme = true) {
$url = trim($url);
if ($url == '') {
return '';
@@ -122,31 +137,19 @@ function checkUrl($url, $fixScheme = true) {
* @return string
*/
function safe_ascii($text) {
- return filter_var($text, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
+ return filter_var($text, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?: '';
}
if (function_exists('mb_convert_encoding')) {
- /**
- * @param string $text
- * @return string
- */
- function safe_utf8($text) {
- return mb_convert_encoding($text, 'UTF-8', 'UTF-8');
+ function safe_utf8(string $text): string {
+ return mb_convert_encoding($text, 'UTF-8', 'UTF-8') ?: '';
}
} elseif (function_exists('iconv')) {
- /**
- * @param string $text
- * @return string
- */
- function safe_utf8($text) {
- return iconv('UTF-8', 'UTF-8//IGNORE', $text);
+ function safe_utf8(string $text): string {
+ return iconv('UTF-8', 'UTF-8//IGNORE', $text) ?: '';
}
} else {
- /**
- * @param string $text
- * @return string
- */
- function safe_utf8($text) {
+ function safe_utf8(string $text): string {
return $text;
}
}
@@ -173,14 +176,14 @@ function escapeToUnicodeAlternative($text, $extended = true) {
return trim(str_replace($problem, $replace, $text));
}
-function format_number($n, $precision = 0) {
+function format_number(float $n, int $precision = 0): string {
// number_format does not seem to be Unicode-compatible
return str_replace(' ', ' ', // Thin non-breaking space
number_format($n, $precision, '.', ' ')
);
}
-function format_bytes($bytes, $precision = 2, $system = 'IEC') {
+function format_bytes(int $bytes, int $precision = 2, string $system = 'IEC'): string {
if ($system === 'IEC') {
$base = 1024;
$units = array('B', 'KiB', 'MiB', 'GiB', 'TiB');
@@ -197,7 +200,7 @@ function format_bytes($bytes, $precision = 2, $system = 'IEC') {
return format_number($bytes, $precision) . ' ' . $units[$pow];
}
-function timestamptodate ($t, $hour = true) {
+function timestamptodate(int $t, bool $hour = true): string {
$month = _t('gen.date.' . date('M', $t));
if ($hour) {
$date = _t('gen.date.format_date_hour', $month);
@@ -205,14 +208,13 @@ function timestamptodate ($t, $hour = true) {
$date = _t('gen.date.format_date', $month);
}
- return @date ($date, $t);
+ return @date($date, $t) ?: '';
}
/**
* Decode HTML entities but preserve XML entities.
- * @param string|null $text
*/
-function html_only_entity_decode($text): string {
+function html_only_entity_decode(?string $text): string {
static $htmlEntitiesOnly = null;
if ($htmlEntitiesOnly === null) {
$htmlEntitiesOnly = array_flip(array_diff(
@@ -220,13 +222,43 @@ function html_only_entity_decode($text): string {
get_html_translation_table(HTML_SPECIALCHARS, ENT_NOQUOTES, 'UTF-8') //Preserve XML entities
));
}
- return $text == '' ? '' : strtr($text, $htmlEntitiesOnly);
+ return $text == null ? '' : strtr($text, $htmlEntitiesOnly);
+}
+
+/**
+ * Remove passwords in FreshRSS logs.
+ * See also ../cli/sensitive-log.sh for Web server logs.
+ * @param array<string,mixed>|string $log
+ * @return array<string,mixed>|string
+ */
+function sensitive_log($log) {
+ if (is_array($log)) {
+ foreach ($log as $k => $v) {
+ if (in_array($k, ['api_key', 'Passwd', 'T'])) {
+ $log[$k] = '██';
+ } elseif (is_array($v) || is_string($v)) {
+ $log[$k] = sensitive_log($v);
+ } else {
+ return '';
+ }
+ }
+ } elseif (is_string($log)) {
+ $log = preg_replace([
+ '/\b(auth=.*?\/)[^&]+/i',
+ '/\b(Passwd=)[^&]+/i',
+ '/\b(Authorization)[^&]+/i',
+ ], '$1█', $log) ?? '';
+ }
+ return $log;
}
/**
* @param array<string,mixed> $attributes
*/
function customSimplePie($attributes = array()): SimplePie {
+ if (FreshRSS_Context::$system_conf === null) {
+ throw new FreshRSS_Context_Exception('System configuration not initialised!');
+ }
$limits = FreshRSS_Context::$system_conf->limits;
$simplePie = new SimplePie();
$simplePie->set_useragent(FRESHRSS_USERAGENT);
@@ -308,13 +340,13 @@ function customSimplePie($attributes = array()): SimplePie {
}
/**
- * @param int|false $maxLength
+ * @param string $data
*/
-function sanitizeHTML($data, string $base = '', $maxLength = false) {
- if (!is_string($data) || ($maxLength !== false && $maxLength <= 0)) {
+function sanitizeHTML($data, string $base = '', ?int $maxLength = null): string {
+ if (!is_string($data) || ($maxLength !== null && $maxLength <= 0)) {
return '';
}
- if ($maxLength !== false) {
+ if ($maxLength !== null) {
$data = mb_strcut($data, 0, $maxLength, 'UTF-8');
}
static $simplePie = null;
@@ -323,7 +355,7 @@ function sanitizeHTML($data, string $base = '', $maxLength = false) {
$simplePie->init();
}
$result = html_only_entity_decode($simplePie->sanitize->sanitize($data, SIMPLEPIE_CONSTRUCT_HTML, $base));
- if ($maxLength !== false && strlen($result) > $maxLength) {
+ if ($maxLength !== null && strlen($result) > $maxLength) {
//Sanitizing has made the result too long so try again shorter
$data = mb_strcut($result, 0, (2 * $maxLength) - strlen($result) - 2, 'UTF-8');
return sanitizeHTML($data, $base, $maxLength);
@@ -331,9 +363,13 @@ function sanitizeHTML($data, string $base = '', $maxLength = false) {
return $result;
}
-function cleanCache(int $hours = 720) {
+function cleanCache(int $hours = 720): void {
// N.B.: GLOB_BRACE is not available on all platforms
- $files = array_merge(glob(CACHE_PATH . '/*.html', GLOB_NOSORT), glob(CACHE_PATH . '/*.spc', GLOB_NOSORT));
+ $files = array_merge(
+ glob(CACHE_PATH . '/*.html', GLOB_NOSORT) ?: [],
+ glob(CACHE_PATH . '/*.json', GLOB_NOSORT) ?: [],
+ glob(CACHE_PATH . '/*.spc', GLOB_NOSORT) ?: [],
+ glob(CACHE_PATH . '/*.xml', GLOB_NOSORT) ?: []);
foreach ($files as $file) {
if (substr($file, -10) === 'index.html') {
continue;
@@ -378,17 +414,20 @@ function enforceHttpEncoding(string $html, string $contentType = ''): string {
}
/**
- * @param string $type {html,opml}
+ * @param string $type {html,json,opml,xml}
* @param array<string,mixed> $attributes
*/
function httpGet(string $url, string $cachePath, string $type = 'html', array $attributes = []): string {
+ if (FreshRSS_Context::$system_conf === null) {
+ throw new FreshRSS_Context_Exception('System configuration not initialised!');
+ }
$limits = FreshRSS_Context::$system_conf->limits;
$feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']);
$cacheMtime = @filemtime($cachePath);
if ($cacheMtime !== false && $cacheMtime > time() - intval($limits['cache_duration'])) {
$body = @file_get_contents($cachePath);
- if ($body != '') {
+ if ($body != false) {
syslog(LOG_DEBUG, 'FreshRSS uses cache for ' . SimplePie_Misc::url_remove_credentials($url));
return $body;
}
@@ -404,9 +443,15 @@ function httpGet(string $url, string $cachePath, string $type = 'html', array $a
$accept = '*/*;q=0.8';
switch ($type) {
+ case 'json':
+ $accept = 'application/json,application/javascript;q=0.9,text/javascript;q=0.8,*/*;q=0.7';
+ break;
case 'opml':
$accept = 'text/x-opml,text/xml;q=0.9,application/xml;q=0.9,*/*;q=0.8';
break;
+ case 'xml':
+ $accept = 'application/xml,application/xhtml+xml,text/xml;q=0.9,*/*;q=0.8';
+ break;
case 'html':
default:
$accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
@@ -442,7 +487,7 @@ function httpGet(string $url, string $cachePath, string $type = 'html', array $a
}
$body = curl_exec($ch);
$c_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- $c_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); //TODO: Check if that may be null
+ $c_content_type = '' . curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$c_error = curl_error($ch);
curl_close($ch);
@@ -451,7 +496,7 @@ function httpGet(string $url, string $cachePath, string $type = 'html', array $a
$body = '';
// TODO: Implement HTTP 410 Gone
}
- if ($body == false) {
+ if (!is_string($body)) {
$body = '';
} else {
$body = enforceHttpEncoding($body, $c_content_type);
@@ -468,10 +513,9 @@ function httpGet(string $url, string $cachePath, string $type = 'html', array $a
* Validate an email address, supports internationalized addresses.
*
* @param string $email The address to validate
- *
* @return bool true if email is valid, else false
*/
-function validateEmailAddress($email) {
+function validateEmailAddress(string $email): bool {
$mailer = new PHPMailer\PHPMailer\PHPMailer();
$mailer->CharSet = 'utf-8';
$punyemail = $mailer->punyencodeAddress($email);
@@ -482,9 +526,8 @@ function validateEmailAddress($email) {
* Add support of image lazy loading
* Move content from src attribute to data-original
* @param string $content is the text we want to parse
- * @return string
*/
-function lazyimg($content) {
+function lazyimg(string $content): string {
return preg_replace([
'/<((?:img|iframe)[^>]+?)src="([^"]+)"([^>]*)>/i',
"/<((?:img|iframe)[^>]+?)src='([^']+)'([^>]*)>/i",
@@ -493,18 +536,15 @@ function lazyimg($content) {
"<$1src='" . Minz_Url::display('/themes/icons/grey.gif') . "' data-original='$2'$3>",
],
$content
- );
+ ) ?? '';
}
-/**
- * @return string
- */
-function uTimeString() {
+function uTimeString(): string {
$t = @gettimeofday();
return $t['sec'] . str_pad('' . $t['usec'], 6, '0', STR_PAD_LEFT);
}
-function invalidateHttpCache($username = '') {
+function invalidateHttpCache(string $username = ''): bool {
if (!FreshRSS_user_Controller::checkUsername($username)) {
Minz_Session::_param('touch', uTimeString());
$username = Minz_Session::param('currentUser', '_');
@@ -519,12 +559,12 @@ function invalidateHttpCache($username = '') {
/**
* @return array<string>
*/
-function listUsers() {
+function listUsers(): array {
$final_list = array();
$base_path = join_path(DATA_PATH, 'users');
$dir_list = array_values(array_diff(
- scandir($base_path),
- array('..', '.', '_')
+ scandir($base_path) ?: [],
+ ['..', '.', '_']
));
foreach ($dir_list as $file) {
if ($file[0] !== '.' && is_dir(join_path($base_path, $file)) && file_exists(join_path($base_path, $file, 'config.php'))) {
@@ -537,12 +577,14 @@ function listUsers() {
/**
* Return if the maximum number of registrations has been reached.
- *
- * Note a max_regstrations of 0 means there is no limit.
+ * Note a max_registrations of 0 means there is no limit.
*
* @return boolean true if number of users >= max registrations, false else.
*/
-function max_registrations_reached() {
+function max_registrations_reached(): bool {
+ if (FreshRSS_Context::$system_conf === null) {
+ throw new FreshRSS_Context_Exception('System configuration not initialised!');
+ }
$limit_registrations = FreshRSS_Context::$system_conf->limits['max_registrations'];
$number_accounts = count(listUsers());
@@ -559,7 +601,7 @@ function max_registrations_reached() {
* @param string $username the name of the user of which we want the configuration.
* @return FreshRSS_UserConfiguration|null object, or null if the configuration cannot be loaded.
*/
-function get_user_configuration($username) {
+function get_user_configuration(string $username) {
if (!FreshRSS_user_Controller::checkUsername($username)) {
return null;
}
@@ -591,7 +633,7 @@ function get_user_configuration($username) {
*/
function ipToBits(string $ip): string {
$binaryip = '';
- foreach (str_split(inet_pton($ip)) as $char) {
+ foreach (str_split(inet_pton($ip) ?: '') as $char) {
$binaryip .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
}
return $binaryip;
@@ -624,6 +666,9 @@ function checkCIDR(string $ip, string $range): bool {
* @return boolean, true if the sender's IP is in one of the ranges defined in the configuration, else false
*/
function checkTrustedIP(): bool {
+ if (FreshRSS_Context::$system_conf === null) {
+ throw new FreshRSS_Context_Exception('System configuration not initialised!');
+ }
if (!empty($_SERVER['REMOTE_ADDR'])) {
foreach (FreshRSS_Context::$system_conf->trusted_sources as $cidr) {
if (checkCIDR($_SERVER['REMOTE_ADDR'], $cidr)) {
@@ -634,10 +679,7 @@ function checkTrustedIP(): bool {
return false;
}
-/**
- * @return string
- */
-function httpAuthUser() {
+function httpAuthUser(): string {
if (!empty($_SERVER['REMOTE_USER'])) {
return $_SERVER['REMOTE_USER'];
} elseif (!empty($_SERVER['HTTP_REMOTE_USER']) && checkTrustedIP()) {
@@ -650,10 +692,7 @@ function httpAuthUser() {
return '';
}
-/**
- * @return bool
- */
-function cryptAvailable() {
+function cryptAvailable(): bool {
try {
$hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
return $hash === @crypt('password', $hash);
@@ -669,7 +708,7 @@ function cryptAvailable() {
*
* @return array<string,bool> of tested values.
*/
-function check_install_php() {
+function check_install_php(): array {
$pdo_mysql = extension_loaded('pdo_mysql');
$pdo_pgsql = extension_loaded('pdo_pgsql');
$pdo_sqlite = extension_loaded('pdo_sqlite');
@@ -693,16 +732,16 @@ function check_install_php() {
*
* @return array<string,bool> of tested values.
*/
-function check_install_files() {
+function check_install_files(): array {
return array(
// @phpstan-ignore-next-line
- 'data' => DATA_PATH && is_writable(DATA_PATH),
+ 'data' => DATA_PATH && touch(DATA_PATH . '/index.html'), // is_writable() is not reliable for a folder on NFS
// @phpstan-ignore-next-line
- 'cache' => CACHE_PATH && is_writable(CACHE_PATH),
+ 'cache' => CACHE_PATH && touch(CACHE_PATH . '/index.html'),
// @phpstan-ignore-next-line
- 'users' => USERS_PATH && is_writable(USERS_PATH),
- 'favicons' => is_writable(DATA_PATH . '/favicons'),
- 'tokens' => is_writable(DATA_PATH . '/tokens'),
+ 'users' => USERS_PATH && touch(USERS_PATH . '/index.html'),
+ 'favicons' => touch(DATA_PATH . '/favicons/index.html'),
+ 'tokens' => touch(DATA_PATH . '/tokens/index.html'),
);
}
@@ -712,7 +751,7 @@ function check_install_files() {
*
* @return array<string,bool> of tested values.
*/
-function check_install_database() {
+function check_install_database(): array {
$status = array(
'connection' => true,
'tables' => false,
@@ -743,17 +782,14 @@ function check_install_database() {
/**
* Remove a directory recursively.
- *
* From http://php.net/rmdir#110489
- *
- * @param string $dir the directory to remove
*/
-function recursive_unlink($dir) {
+function recursive_unlink(string $dir): bool {
if (!is_dir($dir)) {
return true;
}
- $files = array_diff(scandir($dir), array('.', '..'));
+ $files = array_diff(scandir($dir) ?: [], ['.', '..']);
foreach ($files as $filename) {
$filename = $dir . '/' . $filename;
if (is_dir($filename)) {
@@ -773,7 +809,7 @@ function recursive_unlink($dir) {
* @param array<int,array<string,string>> $queries an array of queries.
* @return array<int,array<string,string>> without queries where $get is appearing.
*/
-function remove_query_by_get($get, $queries) {
+function remove_query_by_get(string $get, array $queries): array {
$final_queries = array();
foreach ($queries as $key => $query) {
if (empty($query['get']) || $query['get'] !== $get) {
@@ -797,7 +833,11 @@ const SHORTCUT_KEYS = [
'End', 'Enter', 'Escape', 'Home', 'Insert', 'PageDown', 'PageUp', 'Space', 'Tab',
];
-function getNonStandardShortcuts($shortcuts) {
+/**
+ * @param array<string> $shortcuts
+ * @return array<string>
+ */
+function getNonStandardShortcuts(array $shortcuts): array {
$standard = strtolower(implode(' ', SHORTCUT_KEYS));
$nonStandard = array_filter($shortcuts, function ($shortcut) use ($standard) {
@@ -808,14 +848,14 @@ function getNonStandardShortcuts($shortcuts) {
return $nonStandard;
}
-function errorMessageInfo($errorTitle, $error = '') {
+function errorMessageInfo(string $errorTitle, string $error = ''): string {
$errorTitle = htmlspecialchars($errorTitle, ENT_NOQUOTES, 'UTF-8');
$message = '';
$details = '';
// Prevent empty tags by checking if error isn not empty first
if ($error) {
- $error = htmlspecialchars($error, ENT_NOQUOTES, 'UTF-8');
+ $error = htmlspecialchars($error, ENT_NOQUOTES, 'UTF-8') . "\n";
// First line is the main message, other lines are the details
list($message, $details) = explode("\n", $error, 2);
diff --git a/lib/marienfressinaud/lib_opml/.gitattributes b/lib/marienfressinaud/lib_opml/.gitattributes
new file mode 100644
index 000000000..669ea8c8d
--- /dev/null
+++ b/lib/marienfressinaud/lib_opml/.gitattributes
@@ -0,0 +1,8 @@
+/.* export-ignore
+
+/ci export-ignore
+/examples export-ignore
+/tests export-ignore
+
+/CHANGELOG.md export-ignore
+/Makefile export-ignore
diff --git a/lib/marienfressinaud/lib_opml/.gitignore b/lib/marienfressinaud/lib_opml/.gitignore
new file mode 100644
index 000000000..ca9baaf91
--- /dev/null
+++ b/lib/marienfressinaud/lib_opml/.gitignore
@@ -0,0 +1,2 @@
+/coverage
+/vendor
diff --git a/lib/marienfressinaud/lib_opml/CHANGELOG.md b/lib/marienfressinaud/lib_opml/CHANGELOG.md
new file mode 100644
index 000000000..ee9245e7e
--- /dev/null
+++ b/lib/marienfressinaud/lib_opml/CHANGELOG.md
@@ -0,0 +1,63 @@
+# Changelog of lib\_opml
+
+## 2022-07-25 - v0.5.0
+
+- BREAKING CHANGE: Reverse parameters in `libopml_render()`
+- BREAKING CHANGE: Validate email and URL address elements
+- Add support for PHP 7.2+
+- Add a .gitattributes file
+- Improve the documentation about usage
+- Add a note about stability in README
+- Fix a PHPDoc annotation
+- Homogeneize tests with "Newspapers" examples
+
+## 2022-06-04 - v0.4.0
+
+- Refactor the LibOpml class to be not static
+- Parse or render attributes according to their types
+- Add support for namespaces
+- Don't require text attribute if OPML version is 1.0
+- Check that outline text attribute is not empty
+- Verify that xmlUrl and url attributes are present according to the type
+ attribute
+- Accept a version attribute in render method
+- Handle OPML 1.1 as 1.0
+- Fail if version, head or body is missing
+- Fail if OPML version is not supported
+- Fail if head contains invalid elements
+- Fail if sub-outlines are not arrays when rendering
+- Make parsing less strict by default
+- Don't raise most parsing errors when strict is false
+- Force type attribute to lowercase
+- Remove SimpleXML as a requirement
+- Homogenize exception messages
+- Close pre tags in the example file
+- Improve documentation in the README
+- Improve comments in the source code
+- Add a MR checklist item about changes
+- Update the description in composer.json
+- Update dev dependencies
+
+## 2022-04-23 - v0.3.0
+
+- Reorganize the architecture of code (using namespaces and classes)
+- Change PHP minimum version to 7.4
+- Move to Framagit instead of GitHub
+- Change the license to MIT
+- Configure lib\_opml with Composer
+- Add PHPUnit tests for all the methods and functions
+- Add a linter to the project
+- Provide a Makefile
+- Configure Gitlab CI instead of Travis
+- Add a merge request template
+- Improve the comments, documentation and examples
+
+## 2014-03-31 - v0.2.0
+
+- Allow to make optional the `text` attribute
+- Improve and complete documentation
+- Fix examples
+
+## 2014-03-29 - v0.1.0
+
+First version
diff --git a/lib/marienfressinaud/lib_opml/LICENSE b/lib/marienfressinaud/lib_opml/LICENSE
new file mode 100644
index 000000000..2ad7f2db4
--- /dev/null
+++ b/lib/marienfressinaud/lib_opml/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Marien Fressinaud
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/lib/marienfressinaud/lib_opml/README.md b/lib/marienfressinaud/lib_opml/README.md
new file mode 100644
index 000000000..34026bc14
--- /dev/null
+++ b/lib/marienfressinaud/lib_opml/README.md
@@ -0,0 +1,338 @@
+# lib\_opml
+
+lib\_opml is a library to read and write OPML in PHP.
+
+OPML is a standard designed to store and exchange outlines (i.e. a tree
+structure arranged to show hierarchical relationships). It is mainly used to
+exchange list of feeds between feed aggregators. The specification is
+available at [opml.org](http://opml.org).
+
+lib\_opml has been tested with PHP 7.2+. It requires [DOMDocument](https://www.php.net/manual/book.dom.php)
+to work.
+
+It supports versions 1.0 and 2.0 of OPML since these are the only published
+versions. Version 1.1 is treated as version 1.0, as stated by the specification.
+
+It is licensed under the [MIT license](/LICENSE).
+
+## Installation
+
+lib\_opml is available on [Packagist](https://packagist.org/packages/marienfressinaud/lib_opml)
+and it is recommended to install it with Composer:
+
+```console
+$ composer require marienfressinaud/lib_opml
+```
+
+If you don’t use Composer, you can download [the ZIP archive](https://framagit.org/marienfressinaud/lib_opml/-/archive/main/lib_opml-main.zip)
+and copy the content of the `src/` folder in your project. Then, load the files
+manually:
+
+```php
+<?php
+require 'path/to/lib_opml/LibOpml/Exception.php';
+require 'path/to/lib_opml/LibOpml/LibOpml.php';
+require 'path/to/lib_opml/functions.php';
+```
+
+## Usage
+
+### Parse OPML
+
+Let’s say that you have an OPML file named `my_opml_file.xml`:
+
+```xml
+<?xml version="1.0" encoding="UTF-8" ?>
+<opml version="2.0">
+ <head>
+ <title>My OPML</title>
+ </head>
+ <body>
+ <outline text="Newspapers">
+ <outline text="El País" />
+ <outline text="Le Monde" />
+ <outline text="The Guardian" />
+ <outline text="The New York Times" />
+ </outline>
+ </body>
+</opml>
+```
+
+You can load it with:
+
+```php
+$opml_array = libopml_parse_file('my_opml_file.xml');
+```
+
+lib\_opml parses the file and returns an array:
+
+```php
+[
+ 'version' => '2.0',
+ 'namespaces' => [],
+ 'head' => [
+ 'title' => 'My OPML'
+ ],
+ 'body' => [ // each entry of the body is an outline
+ [
+ 'text' => 'Newspapers',
+ '@outlines' => [ // sub-outlines are accessible with the @outlines key
+ ['text' => 'El País'],
+ ['text' => 'Le Monde'],
+ ['text' => 'The Guardian'],
+ ['text' => 'The New York Times']
+ ]
+ ]
+ ]
+]
+```
+
+Since it's just an array, it's very simple to manipulate:
+
+```php
+foreach ($opml_array['body'] as $outline) {
+ echo $outline['text'];
+}
+```
+
+You also can load directly an OPML string:
+
+```php
+$opml_string = '<opml>...</opml>';
+$opml_array = libopml_parse_string($opml_string);
+```
+
+### Render OPML
+
+lib\_opml is able to render an OPML string from an array. It checks that the
+data is valid and respects the specification.
+
+```php
+$opml_array = [
+ 'head' => [
+ 'title' => 'My OPML',
+ ],
+ 'body' => [
+ [
+ 'text' => 'Newspapers',
+ '@outlines' => [
+ ['text' => 'El País'],
+ ['text' => 'Le Monde'],
+ ['text' => 'The Guardian'],
+ ['text' => 'The New York Times']
+ ]
+ ]
+ ]
+];
+
+$opml_string = libopml_render($opml_array);
+
+file_put_contents('my_opml_file.xml', $opml_string);
+```
+
+### Handle errors
+
+If rendering (or parsing) fails for any reason (e.g. empty `body`, missing
+`text` attribute, wrong element type), a `\marienfressinaud\LibOpml\Exception`
+is raised:
+
+```php
+try {
+ $opml_array = libopml_render([
+ 'body' => []
+ ]);
+} catch (\marienfressinaud\LibOpml\Exception $e) {
+ echo $e->getMessage();
+}
+```
+
+### Class style
+
+lib\_opml can also be used with a class style:
+
+```php
+use marienfressinaud\LibOpml;
+
+$libopml = new LibOpml\LibOpml();
+
+$opml_array = $libopml->parseFile($filename);
+$opml_array = $libopml->parseString($opml_string);
+$opml_string = $libopml->render($opml_array);
+```
+
+### Special elements and attributes
+
+Some elements have special meanings according to the specification, which means
+they can be parsed to a specific type by lib\_opml. In the other way, when
+rendering an OPML string, you must pass these elements with their correct
+types.
+
+Head elements:
+
+- `dateCreated` is parsed to a `\DateTime`;
+- `dateModified` is parsed to a `\DateTime`;
+- `expansionState` is parsed to an array of integers;
+- `vertScrollState` is parsed to an integer;
+- `windowTop` is parsed to an integer;
+- `windowLeft` is parsed to an integer;
+- `windowBottom` is parsed to an integer;
+- `windowRight` is parsed to an integer.
+
+Outline attributes:
+
+- `created` is parsed to a `\DateTime`;
+- `category` is parsed to an array of strings;
+- `isComment` is parsed to a boolean;
+- `isBreakpoint` is parsed to a boolean.
+
+If one of these elements is not of the correct type, an Exception is raised.
+
+Finally, there are additional checks based on the outline type attribute:
+
+- if `type="rss"`, then the `xmlUrl` attribute is required;
+- if `type="link"`, then the `url` attribute is required;
+- if `type="include"`, then the `url` attribute is required.
+
+Note that the `type` attribute is case-insensitive and will always be lowercased.
+
+### Namespaces
+
+OPML can be extended with namespaces:
+
+> An OPML file may contain elements and attributes not described on this page,
+> only if those elements are defined in a namespace, as specified by the W3C.
+
+When rendering an OPML, you can include a `namespaces` key to specify
+namespaces:
+
+```php
+$opml_array = [
+ 'namespaces' => [
+ 'test' => 'https://example.com/test',
+ ],
+ 'body' => [
+ ['text' => 'My outline', 'test:path' => '/some/example/path'],
+ ],
+];
+
+$opml_string = libopml_render($opml_array);
+echo $opml_string;
+```
+
+This will output:
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<opml xmlns:test="https://example.com/test" version="2.0">
+ <head/>
+ <body>
+ <outline text="My outline" test:path="/some/example/path"/>
+ </body>
+</opml>
+```
+
+### Strictness
+
+You can tell lib\_opml to be less or more strict when parsing or rendering OPML.
+This is done by passing an optional `$strict` attribute to the functions. When
+strict is `false`, most of the specification requirements are simply ignored
+and lib\_opml will do its best to parse (or generate) an OPML.
+
+By default, parsing is not strict so you’ll be able to read most of the files
+out there. If you want the parsing to be strict (to validate a file for
+instance), pass `true` to `libopml_parse_file()` or `libopml_parse_string()`:
+
+```php
+$opml_array = libopml_parse_file($filename, true);
+$opml_array = libopml_parse_string($opml_string, true);
+```
+
+On the other side, reading is strict by default, so you are encouraged to
+generate valid OPMLs. If you need to relax the strictness, pass `false` to
+`libopml_render()`:
+
+```php
+$opml_string = libopml_render($opml_array, false);
+```
+
+Please note that when using the class form, strict is passed during the object
+instantiation:
+
+```php
+use marienfressinaud\LibOpml;
+
+// lib_opml will be strict for both parsing and rendering!
+$libopml = new LibOpml\LibOpml(true);
+
+$opml_array = $libopml->parseString($opml_string);
+$opml_string = $libopml->render($opml_array);
+```
+
+## Examples and documented source code
+
+See the [`examples/`](/examples) folder for concrete examples.
+
+You are encouraged to read the source code to learn more about lib\_opml. Thus,
+the full documentation is available as comments in the code:
+
+- [`src/LibOpml/LibOpml.php`](src/LibOpml/LibOpml.php)
+- [`src/LibOpml/Exception.php`](src/LibOpml/Exception.php)
+- [`src/functions.php`](src/functions.php)
+
+## Changelog
+
+See [CHANGELOG.md](/CHANGELOG.md).
+
+## Support and stability
+
+Today, lib\_opml covers all the aspects of the OPML specification. Since the
+spec didn't change for more than 15 years, it is expected for the library to
+not change a lot in the future. Thus, I plan to release the v1.0 in a near
+future. I'm only waiting for more tests to be done on its latest version (in
+particular in FreshRSS, see [FreshRSS/FreshRSS#4403](https://github.com/FreshRSS/FreshRSS/pull/4403)).
+I would also wait for clarifications about the specification (see [scripting/opml.org#3](https://github.com/scripting/opml.org/issues/3)),
+but it isn't a hard requirement.
+
+After the release of 1.0, lib\_opml will be considered as “finished”. This
+means I will not add new features, nor break the existing code. However, I
+commit myself to continue to support the library to fix security issues, bugs,
+or to add support to new PHP versions.
+
+In consequence, you can expect lib\_opml to be stable.
+
+## Tests and linters
+
+This section is for developers of lib\_opml.
+
+To run the tests, you’ll have to install Composer first (see [the official
+documentation](https://getcomposer.org/doc/00-intro.md)). Then, install the
+dependencies:
+
+```console
+$ make install
+```
+
+You should now have a `vendor/` folder containing the development dependencies.
+
+Run the tests with:
+
+```console
+$ make test
+```
+
+Run the linter with:
+
+```console
+$ make lint
+$ make lint-fix
+```
+
+## Contributing
+
+Please submit bug reports and merge requests to the [Framagit repository](https://framagit.org/marienfressinaud/lib_opml).
+
+There’s not a lot to do, but the documentation and examples could probably be
+improved.
+
+Merge requests require that you fill a short checklist to save me time while
+reviewing your changes. You also must make sure the test suite succeeds.
diff --git a/lib/marienfressinaud/lib_opml/composer.json b/lib/marienfressinaud/lib_opml/composer.json
new file mode 100644
index 000000000..ba48d16ed
--- /dev/null
+++ b/lib/marienfressinaud/lib_opml/composer.json
@@ -0,0 +1,35 @@
+{
+ "name": "marienfressinaud/lib_opml",
+ "description": "A library to read and write OPML in PHP.",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Marien Fressinaud",
+ "email": "dev@marienfressinaud.fr"
+ }
+ ],
+ "require": {
+ "php": ">=7.2.0",
+ "ext-dom": "*"
+ },
+ "config": {
+ "platform": {
+ "php": "7.2.0"
+ }
+ },
+ "support": {
+ "issues": "https://framagit.org/marienfressinaud/lib_opml/-/issues"
+ },
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "marienfressinaud\\": "src/"
+ }
+ },
+ "require-dev": {
+ "squizlabs/php_codesniffer": "^3.6",
+ "phpunit/phpunit": "^8"
+ }
+}
diff --git a/lib/marienfressinaud/lib_opml/src/LibOpml/Exception.php b/lib/marienfressinaud/lib_opml/src/LibOpml/Exception.php
new file mode 100644
index 000000000..27c3287a2
--- /dev/null
+++ b/lib/marienfressinaud/lib_opml/src/LibOpml/Exception.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace marienfressinaud\LibOpml;
+
+/**
+ * A simple Exception class which represents any kind of OPML problem.
+ * Message precises the current problem.
+ *
+ * @author Marien Fressinaud <dev@marienfressinaud.fr>
+ * @link https://framagit.org/marienfressinaud/lib_opml
+ * @license MIT
+ */
+class Exception extends \Exception
+{
+}
diff --git a/lib/marienfressinaud/lib_opml/src/LibOpml/LibOpml.php b/lib/marienfressinaud/lib_opml/src/LibOpml/LibOpml.php
new file mode 100644
index 000000000..4ba0df821
--- /dev/null
+++ b/lib/marienfressinaud/lib_opml/src/LibOpml/LibOpml.php
@@ -0,0 +1,770 @@
+<?php
+
+namespace marienfressinaud\LibOpml;
+
+/**
+ * The LibOpml class provides the methods to read and write OPML files and
+ * strings. It transforms OPML files or strings to PHP arrays (or the reverse).
+ *
+ * How to read this file?
+ *
+ * The first methods are dedicated to the parsing, and the next ones to the
+ * reading. The three last methods are helpful methods, but you don't have to
+ * worry too much about them.
+ *
+ * The main methods are the public ones: parseFile, parseString and render.
+ * They call the other parse* and render* methods internally.
+ *
+ * These three main methods are available as functions (see the src/functions.php
+ * file).
+ *
+ * What's the array format?
+ *
+ * As said before, LibOpml transforms OPML to PHP arrays, or the reverse. The
+ * format is pretty simple. It contains four keys:
+ *
+ * - version: the version of the OPML;
+ * - namespaces: an array of namespaces used in the OPML, if any;
+ * - head: an array of OPML head elements, where keys are the names of the
+ * elements;
+ * - body: an array of arrays representing OPML outlines, where keys are the
+ * name of the attributes (the special @outlines key contains the sub-outlines).
+ *
+ * When rendering, only the body key is required (version will default to 2.0).
+ *
+ * Example:
+ *
+ * [
+ * version => '2.0',
+ * namespaces => [],
+ * head => [
+ * title => 'An OPML file'
+ * ],
+ * body => [
+ * [
+ * text => 'Newspapers',
+ * @outlines => [
+ * [text => 'El País'],
+ * [text => 'Le Monde'],
+ * [text => 'The Guardian'],
+ * [text => 'The New York Times'],
+ * ]
+ * ]
+ * ]
+ * ]
+ *
+ * @see http://opml.org/spec2.opml
+ *
+ * @author Marien Fressinaud <dev@marienfressinaud.fr>
+ * @link https://framagit.org/marienfressinaud/lib_opml
+ * @license MIT
+ */
+class LibOpml
+{
+ /**
+ * The list of valid head elements.
+ */
+ public const HEAD_ELEMENTS = [
+ 'title', 'dateCreated', 'dateModified', 'ownerName', 'ownerEmail',
+ 'ownerId', 'docs', 'expansionState', 'vertScrollState', 'windowTop',
+ 'windowLeft', 'windowBottom', 'windowRight'
+ ];
+
+ /**
+ * The list of numeric head elements.
+ */
+ public const NUMERIC_HEAD_ELEMENTS = [
+ 'vertScrollState',
+ 'windowTop',
+ 'windowLeft',
+ 'windowBottom',
+ 'windowRight',
+ ];
+
+ /** @var boolean */
+ private $strict = true;
+
+ /** @var string */
+ private $version = '2.0';
+
+ /** @var string[] */
+ private $namespaces = [];
+
+ /**
+ * @param bool $strict
+ * Set to true (default) to check for violations of the specification,
+ * false otherwise.
+ */
+ public function __construct($strict = true)
+ {
+ $this->strict = $strict;
+ }
+
+ /**
+ * Parse a XML file and return the corresponding array.
+ *
+ * @param string $filename
+ * The XML file to parse.
+ *
+ * @throws \marienfressinaud\LibOpml\Exception
+ * Raised if the file cannot be read. See also exceptions raised by the
+ * parseString method.
+ *
+ * @return array
+ * An array reflecting the OPML (the structure is described above).
+ */
+ public function parseFile($filename)
+ {
+ $file_content = @file_get_contents($filename);
+
+ if ($file_content === false) {
+ throw new Exception("OPML file {$filename} cannot be found or read");
+ }
+
+ return $this->parseString($file_content);
+ }
+
+ /**
+ * Parse a XML string and return the corresponding array.
+ *
+ * @param string $xml
+ * The XML string to parse.
+ *
+ * @throws \marienfressinaud\LibOpml\Exception
+ * Raised if the XML cannot be parsed, if version is missing or
+ * invalid, if head is missing or contains invalid (or not parsable)
+ * elements, or if body is missing, empty or contain non outline
+ * elements. The exceptions (except XML parsing errors) are not raised
+ * if strict is false. See also exceptions raised by the parseOutline
+ * method.
+ *
+ * @return array
+ * An array reflecting the OPML (the structure is described above).
+ */
+ public function parseString($xml)
+ {
+ $dom = new \DOMDocument();
+ $dom->recover = true;
+ $dom->encoding = 'UTF-8';
+
+ try {
+ $result = @$dom->loadXML($xml);
+ } catch (\Exception | \Error $e) {
+ $result = false;
+ }
+
+ if (!$result) {
+ throw new Exception('OPML string is not valid XML');
+ }
+
+ $opml_element = $dom->documentElement;
+
+ // Load the custom namespaces of the document
+ $xpath = new \DOMXPath($dom);
+ $this->namespaces = [];
+ foreach ($xpath->query('//namespace::*') as $node) {
+ if ($node->prefix === 'xml') {
+ // This is the base namespace, we don't need to store it
+ continue;
+ }
+
+ $this->namespaces[$node->prefix] = $node->namespaceURI;
+ }
+
+ // Get the version of the document
+ $version = $opml_element->getAttribute('version');
+ if (!$version) {
+ $this->throwExceptionIfStrict('OPML version attribute is required');
+ }
+
+ $version = trim($version);
+ if ($version === '1.1') {
+ $version = '1.0';
+ }
+
+ if ($version !== '1.0' && $version !== '2.0') {
+ $this->throwExceptionIfStrict('OPML supported versions are 1.0 and 2.0');
+ }
+
+ $this->version = $version;
+
+ // Get head and body child elements
+ $head_elements = $opml_element->getElementsByTagName('head');
+ $child_head_elements = [];
+ if (count($head_elements) === 1) {
+ $child_head_elements = $head_elements[0]->childNodes;
+ } else {
+ $this->throwExceptionIfStrict('OPML must contain one and only one head element');
+ }
+
+ $body_elements = $opml_element->getElementsByTagName('body');
+ $child_body_elements = [];
+ if (count($body_elements) === 1) {
+ $child_body_elements = $body_elements[0]->childNodes;
+ } else {
+ $this->throwExceptionIfStrict('OPML must contain one and only one body element');
+ }
+
+ $array = [
+ 'version' => $this->version,
+ 'namespaces' => $this->namespaces,
+ 'head' => [],
+ 'body' => [],
+ ];
+
+ // Load the child head elements in the head array
+ foreach ($child_head_elements as $child_head_element) {
+ if ($child_head_element->nodeType !== XML_ELEMENT_NODE) {
+ continue;
+ }
+
+ $name = $child_head_element->nodeName;
+ $value = $child_head_element->nodeValue;
+ $namespaced = $child_head_element->namespaceURI !== null;
+
+ if (!in_array($name, self::HEAD_ELEMENTS) && !$namespaced) {
+ $this->throwExceptionIfStrict(
+ "OPML head {$name} element is not part of the specification"
+ );
+ }
+
+ if ($name === 'dateCreated' || $name === 'dateModified') {
+ try {
+ $value = $this->parseDate($value);
+ } catch (\DomainException $e) {
+ $this->throwExceptionIfStrict(
+ "OPML head {$name} element must be a valid RFC822 or RFC1123 date"
+ );
+ }
+ } elseif ($name === 'ownerEmail') {
+ // Testing email validity is hard. PHP filter_var() function is
+ // too strict compared to the RFC 822, so we can't use it.
+ if (strpos($value, '@') === false) {
+ $this->throwExceptionIfStrict(
+ 'OPML head ownerEmail element must be an email address'
+ );
+ }
+ } elseif ($name === 'ownerId' || $name === 'docs') {
+ if (!$this->checkHttpAddress($value)) {
+ $this->throwExceptionIfStrict(
+ "OPML head {$name} element must be a HTTP address"
+ );
+ }
+ } elseif ($name === 'expansionState') {
+ $numbers = explode(',', $value);
+ $value = array_map(function ($str_number) {
+ if (is_numeric($str_number)) {
+ return intval($str_number);
+ } else {
+ $this->throwExceptionIfStrict(
+ 'OPML head expansionState element must be a list of numbers'
+ );
+ return $str_number;
+ }
+ }, $numbers);
+ } elseif (in_array($name, self::NUMERIC_HEAD_ELEMENTS)) {
+ if (is_numeric($value)) {
+ $value = intval($value);
+ } else {
+ $this->throwExceptionIfStrict("OPML head {$name} element must be a number");
+ }
+ }
+
+ $array['head'][$name] = $value;
+ }
+
+ // Load the child body elements in the body array
+ foreach ($child_body_elements as $child_body_element) {
+ if ($child_body_element->nodeType !== XML_ELEMENT_NODE) {
+ continue;
+ }
+
+ if ($child_body_element->nodeName === 'outline') {
+ $array['body'][] = $this->parseOutline($child_body_element);
+ } else {
+ $this->throwExceptionIfStrict(
+ 'OPML body element can only contain outline elements'
+ );
+ }
+ }
+
+ if (empty($array['body'])) {
+ $this->throwExceptionIfStrict(
+ 'OPML body element must contain at least one outline element'
+ );
+ }
+
+ return $array;
+ }
+
+ /**
+ * Parse a XML element as an outline element and return the corresponding array.
+ *
+ * @param \DOMElement $outline_element
+ * The element to parse.
+ *
+ * @throws \marienfressinaud\LibOpml\Exception
+ * Raised if the outline contains non-outline elements, if it doesn't
+ * contain a text attribute (or if empty), if a special attribute is
+ * not parsable, or if type attribute requirements are not met. The
+ * exceptions are not raised if strict is false. The exception about
+ * missing text attribute is not raised if version is 1.0.
+ *
+ * @return array
+ * An array reflecting the OPML outline (the structure is described above).
+ */
+ private function parseOutline($outline_element)
+ {
+ $outline = [];
+
+ // Load the element attributes in the outline array
+ foreach ($outline_element->attributes as $outline_attribute) {
+ $name = $outline_attribute->nodeName;
+ $value = $outline_attribute->nodeValue;
+
+ if ($name === 'created') {
+ try {
+ $value = $this->parseDate($value);
+ } catch (\DomainException $e) {
+ $this->throwExceptionIfStrict(
+ 'OPML outline created attribute must be a valid RFC822 or RFC1123 date'
+ );
+ }
+ } elseif ($name === 'category') {
+ $categories = explode(',', $value);
+ $categories = array_map(function ($category) {
+ return trim($category);
+ }, $categories);
+ $value = $categories;
+ } elseif ($name === 'isComment' || $name === 'isBreakpoint') {
+ if ($value === 'true' || $value === 'false') {
+ $value = $value === 'true';
+ } else {
+ $this->throwExceptionIfStrict(
+ "OPML outline {$name} attribute must be a boolean (true or false)"
+ );
+ }
+ } elseif ($name === 'type') {
+ // type attribute is case-insensitive
+ $value = strtolower($value);
+ }
+
+ $outline[$name] = $value;
+ }
+
+ if (empty($outline['text']) && $this->version !== '1.0') {
+ $this->throwExceptionIfStrict(
+ 'OPML outline text attribute is required'
+ );
+ }
+
+ // Perform additional check based on the type of the outline
+ $type = $outline['type'] ?? '';
+ if ($type === 'rss') {
+ if (empty($outline['xmlUrl'])) {
+ $this->throwExceptionIfStrict(
+ 'OPML outline xmlUrl attribute is required when type is "rss"'
+ );
+ } elseif (!$this->checkHttpAddress($outline['xmlUrl'])) {
+ $this->throwExceptionIfStrict(
+ 'OPML outline xmlUrl attribute must be a HTTP address when type is "rss"'
+ );
+ }
+ } elseif ($type === 'link' || $type === 'include') {
+ if (empty($outline['url'])) {
+ $this->throwExceptionIfStrict(
+ "OPML outline url attribute is required when type is \"{$type}\""
+ );
+ } elseif (!$this->checkHttpAddress($outline['url'])) {
+ $this->throwExceptionIfStrict(
+ "OPML outline url attribute must be a HTTP address when type is \"{$type}\""
+ );
+ }
+ }
+
+ // Load the sub-outlines in a @outlines array
+ foreach ($outline_element->childNodes as $child_outline_element) {
+ if ($child_outline_element->nodeType !== XML_ELEMENT_NODE) {
+ continue;
+ }
+
+ if ($child_outline_element->nodeName === 'outline') {
+ $outline['@outlines'][] = $this->parseOutline($child_outline_element);
+ } else {
+ $this->throwExceptionIfStrict(
+ 'OPML body element can only contain outline elements'
+ );
+ }
+ }
+
+ return $outline;
+ }
+
+ /**
+ * Parse a value as a date.
+ *
+ * @param string $value
+ *
+ * @throws \DomainException
+ * Raised if the value cannot be parsed.
+ *
+ * @return \DateTime
+ */
+ private function parseDate($value)
+ {
+ $formats = [
+ \DateTimeInterface::RFC822,
+ \DateTimeInterface::RFC1123,
+ ];
+
+ foreach ($formats as $format) {
+ $date = date_create_from_format($format, $value);
+ if ($date !== false) {
+ return $date;
+ }
+ }
+
+ throw new \DomainException('The argument cannot be parsed as a date');
+ }
+
+ /**
+ * Render an OPML array as a string or a \DOMDocument.
+ *
+ * @param array $array
+ * The array to render, it must follow the structure defined above.
+ * @param bool $as_dom_document
+ * Set to false (default) to return the array as a string, true to
+ * return as a \DOMDocument.
+ *
+ * @throws \marienfressinaud\LibOpml\Exception
+ * Raised if the `head` array contains unknown or invalid elements
+ * (i.e. not of correct type), or if the `body` array is missing or
+ * empty. The exceptions are not raised if strict is false. See also
+ * exceptions raised by the renderOutline method.
+ *
+ * @return string|\DOMDocument
+ * The XML string or DOM document corresponding to the given array.
+ */
+ public function render($array, $as_dom_document = false)
+ {
+ $dom = new \DOMDocument('1.0', 'UTF-8');
+ $opml_element = new \DOMElement('opml');
+ $dom->appendChild($opml_element);
+
+ // Set the version attribute of the OPML document
+ $version = $array['version'] ?? '2.0';
+
+ if ($version === '1.1') {
+ $version = '1.0';
+ }
+
+ if ($version !== '1.0' && $version !== '2.0') {
+ $this->throwExceptionIfStrict('OPML supported versions are 1.0 and 2.0');
+ }
+
+ $this->version = $version;
+ $opml_element->setAttribute('version', $this->version);
+
+ // Declare the namespace on the opml element
+ $this->namespaces = $array['namespaces'] ?? [];
+ foreach ($this->namespaces as $prefix => $namespace) {
+ $opml_element->setAttributeNS(
+ 'http://www.w3.org/2000/xmlns/',
+ "xmlns:{$prefix}",
+ $namespace
+ );
+ }
+
+ // Add the head element to the OPML document. $array['head'] is
+ // optional but head tag will always exist in the final XML.
+ $head_element = new \DOMElement('head');
+ $opml_element->appendChild($head_element);
+ if (isset($array['head'])) {
+ foreach ($array['head'] as $name => $value) {
+ $namespace = $this->getNamespace($name);
+
+ if (!in_array($name, self::HEAD_ELEMENTS, true) && !$namespace) {
+ $this->throwExceptionIfStrict(
+ "OPML head {$name} element is not part of the specification"
+ );
+ }
+
+ if ($name === 'dateCreated' || $name === 'dateModified') {
+ if ($value instanceof \DateTimeInterface) {
+ $value = $value->format(\DateTimeInterface::RFC1123);
+ } else {
+ $this->throwExceptionIfStrict(
+ "OPML head {$name} element must be a DateTime"
+ );
+ }
+ } elseif ($name === 'ownerEmail') {
+ // Testing email validity is hard. PHP filter_var() function is
+ // too strict compared to the RFC 822, so we can't use it.
+ if (strpos($value, '@') === false) {
+ $this->throwExceptionIfStrict(
+ 'OPML head ownerEmail element must be an email address'
+ );
+ }
+ } elseif ($name === 'ownerId' || $name === 'docs') {
+ if (!$this->checkHttpAddress($value)) {
+ $this->throwExceptionIfStrict(
+ "OPML head {$name} element must be a HTTP address"
+ );
+ }
+ } elseif ($name === 'expansionState') {
+ if (is_array($value)) {
+ foreach ($value as $number) {
+ if (!is_int($number)) {
+ $this->throwExceptionIfStrict(
+ 'OPML head expansionState element must be an array of integers'
+ );
+ }
+ }
+
+ $value = implode(', ', $value);
+ } else {
+ $this->throwExceptionIfStrict(
+ 'OPML head expansionState element must be an array of integers'
+ );
+ }
+ } elseif (in_array($name, self::NUMERIC_HEAD_ELEMENTS)) {
+ if (!is_int($value)) {
+ $this->throwExceptionIfStrict(
+ "OPML head {$name} element must be an integer"
+ );
+ }
+ }
+
+ $child_head_element = new \DOMElement($name, $value, $namespace);
+ $head_element->appendChild($child_head_element);
+ }
+ }
+
+ // Check body is set and contains at least one element
+ if (!isset($array['body'])) {
+ $this->throwExceptionIfStrict('OPML array must contain a body key');
+ }
+
+ $array_body = $array['body'] ?? [];
+ if (count($array_body) <= 0) {
+ $this->throwExceptionIfStrict(
+ 'OPML body element must contain at least one outline array'
+ );
+ }
+
+ // Create outline elements in the body element
+ $body_element = new \DOMElement('body');
+ $opml_element->appendChild($body_element);
+ foreach ($array_body as $outline) {
+ $this->renderOutline($body_element, $outline);
+ }
+
+ // And return the final result
+ if ($as_dom_document) {
+ return $dom;
+ } else {
+ $dom->formatOutput = true;
+ return $dom->saveXML();
+ }
+ }
+
+ /**
+ * Transform an outline array to a \DOMElement and add it to a parent element.
+ *
+ * @param \DOMElement $parent_element
+ * The DOM parent element of the current outline.
+ * @param array $outline
+ * The outline array to transform in a \DOMElement, it must follow the
+ * structure defined above.
+ *
+ * @throws \marienfressinaud\LibOpml\Exception
+ * Raised if the outline is not an array, if it doesn't contain a text
+ * attribute (or if empty), if the `@outlines` key is not an array, if
+ * a special attribute does not match its corresponding type, or if
+ * `type` key requirements are not met. The exceptions (except errors
+ * about outline or suboutlines not being arrays) are not raised if
+ * strict is false. The exception about missing text attribute is not
+ * raised if version is 1.0.
+ */
+ private function renderOutline($parent_element, $outline)
+ {
+ // Perform initial checks to verify the outline is correctly declared
+ if (!is_array($outline)) {
+ throw new Exception(
+ 'OPML outline element must be defined as an array'
+ );
+ }
+
+ if (empty($outline['text']) && $this->version !== '1.0') {
+ $this->throwExceptionIfStrict(
+ 'OPML outline text attribute is required'
+ );
+ }
+
+ if (isset($outline['type'])) {
+ $type = strtolower($outline['type']);
+
+ if ($type === 'rss') {
+ if (empty($outline['xmlUrl'])) {
+ $this->throwExceptionIfStrict(
+ 'OPML outline xmlUrl attribute is required when type is "rss"'
+ );
+ } elseif (!$this->checkHttpAddress($outline['xmlUrl'])) {
+ $this->throwExceptionIfStrict(
+ 'OPML outline xmlUrl attribute must be a HTTP address when type is "rss"'
+ );
+ }
+ } elseif ($type === 'link' || $type === 'include') {
+ if (empty($outline['url'])) {
+ $this->throwExceptionIfStrict(
+ "OPML outline url attribute is required when type is \"{$type}\""
+ );
+ } elseif (!$this->checkHttpAddress($outline['url'])) {
+ $this->throwExceptionIfStrict(
+ "OPML outline url attribute must be a HTTP address when type is \"{$type}\""
+ );
+ }
+ }
+ }
+
+ // Create the outline element and add it to the parent
+ $outline_element = new \DOMElement('outline');
+ $parent_element->appendChild($outline_element);
+
+ // Load the sub-outlines as child elements
+ if (isset($outline['@outlines'])) {
+ $outline_children = $outline['@outlines'];
+
+ if (!is_array($outline_children)) {
+ throw new Exception(
+ 'OPML outline element must be defined as an array'
+ );
+ }
+
+ foreach ($outline_children as $outline_child) {
+ $this->renderOutline($outline_element, $outline_child);
+ }
+
+ // We don't want the sub-outlines to be loaded as attributes, so we
+ // remove the key from the array.
+ unset($outline['@outlines']);
+ }
+
+ // Load the other elements of the array as attributes
+ foreach ($outline as $name => $value) {
+ $namespace = $this->getNamespace($name);
+
+ if ($name === 'created') {
+ if ($value instanceof \DateTimeInterface) {
+ $value = $value->format(\DateTimeInterface::RFC1123);
+ } else {
+ $this->throwExceptionIfStrict(
+ 'OPML outline created attribute must be a DateTime'
+ );
+ }
+ } elseif ($name === 'isComment' || $name === 'isBreakpoint') {
+ if (is_bool($value)) {
+ $value = $value ? 'true' : 'false';
+ } else {
+ $this->throwExceptionIfStrict(
+ "OPML outline {$name} attribute must be a boolean"
+ );
+ }
+ } elseif (is_array($value)) {
+ $value = implode(', ', $value);
+ }
+
+ $outline_element->setAttributeNS($namespace, $name, $value);
+ }
+ }
+
+ /**
+ * Return wether a value is a valid HTTP address or not.
+ *
+ * HTTP address is not strictly defined by the OPML spec, so it is assumed:
+ *
+ * - it can be parsed by parse_url
+ * - it has a host part
+ * - scheme is http or https
+ *
+ * filter_var is not used because it would reject internationalized URLs
+ * (i.e. with non ASCII chars). An alternative would be to punycode such
+ * URLs, but it's more work to do it properly, and lib_opml needs to stay
+ * simple.
+ *
+ * @param string $value
+ *
+ * @return boolean
+ * Return true if the value is a valid HTTP address, false otherwise.
+ */
+ public function checkHttpAddress($value)
+ {
+ $value = trim($value);
+ $parsed_url = parse_url($value);
+ if (!$parsed_url) {
+ return false;
+ }
+
+ if (
+ !isset($parsed_url['scheme']) ||
+ !isset($parsed_url['host'])
+ ) {
+ return false;
+ }
+
+ if (
+ $parsed_url['scheme'] !== 'http' &&
+ $parsed_url['scheme'] !== 'https'
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the namespace of a qualified name. An empty string is returned if
+ * the name is not namespaced.
+ *
+ * @param string $qualified_name
+ *
+ * @throws \marienfressinaud\LibOpml\Exception
+ * Raised if the namespace prefix isn't declared.
+ *
+ * @return string
+ */
+ private function getNamespace($qualified_name)
+ {
+ $split_name = explode(':', $qualified_name, 2);
+ // count will always be 1 or 2.
+ if (count($split_name) === 1) {
+ // If 1, there's no prefix, thus no namespace
+ return '';
+ } else {
+ // If 2, it means it has a namespace prefix, so we get the
+ // namespace from the declared ones.
+ $namespace_prefix = $split_name[0];
+ if (!isset($this->namespaces[$namespace_prefix])) {
+ throw new Exception(
+ "OPML namespace {$namespace_prefix} is not declared"
+ );
+ }
+
+ return $this->namespaces[$namespace_prefix];
+ }
+ }
+
+ /**
+ * Raise an exception only if strict is true.
+ *
+ * @param string $message
+ *
+ * @throws \marienfressinaud\LibOpml\Exception
+ */
+ private function throwExceptionIfStrict($message)
+ {
+ if ($this->strict) {
+ throw new Exception($message);
+ }
+ }
+}
diff --git a/p/.htaccess b/p/.htaccess
index f2f2ad901..79e315c07 100644
--- a/p/.htaccess
+++ b/p/.htaccess
@@ -20,8 +20,6 @@ AddDefaultCharset UTF-8
<IfModule mod_expires.c>
ExpiresActive on
ExpiresDefault "access plus 1 month"
- ExpiresByType application/font-woff "access plus 1 month"
- ExpiresByType application/font-woff2 "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/xhtml+xml "access plus 1 month"
ExpiresByType image/x-icon "access plus 1 month"
diff --git a/p/api/fever.php b/p/api/fever.php
index b7f9b9167..88bd05d81 100644
--- a/p/api/fever.php
+++ b/p/api/fever.php
@@ -17,8 +17,9 @@ require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
FreshRSS_Context::initSystem();
// check if API is enabled globally
-if (!FreshRSS_Context::$system_conf->api_enabled) {
- Minz_Log::warning('Fever API: serviceUnavailable() ' . debugInfo(), API_LOG);
+if (FreshRSS_Context::$system_conf == null || !FreshRSS_Context::$system_conf->api_enabled) {
+ Minz_Log::warning('Fever API: service unavailable!');
+ Minz_Log::debug('Fever API: serviceUnavailable() ' . debugInfo(), API_LOG);
header('HTTP/1.1 503 Service Unavailable');
header('Content-Type: text/plain; charset=UTF-8');
die('Service Unavailable!');
@@ -28,12 +29,9 @@ Minz_Session::init('FreshRSS', true);
// ================================================================================================
// <Debug>
-$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576);
+$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576) ?: '';;
-/**
- * @return string
- */
-function debugInfo() {
+function debugInfo(): string {
if (function_exists('getallheaders')) {
$ALL_HEADERS = getallheaders();
} else { //nginx http://php.net/getallheaders#84262
@@ -45,24 +43,28 @@ function debugInfo() {
}
}
global $ORIGINAL_INPUT;
- return print_r(
- array(
+ $log = sensitive_log([
'date' => date('c'),
'headers' => $ALL_HEADERS,
'_SERVER' => $_SERVER,
'_GET' => $_GET,
'_POST' => $_POST,
'_COOKIE' => $_COOKIE,
- 'INPUT' => $ORIGINAL_INPUT
- ), true);
+ 'INPUT' => $ORIGINAL_INPUT,
+ ]);
+ return print_r($log, true);
}
//Minz_Log::debug('----------------------------------------------------------------', API_LOG);
//Minz_Log::debug(debugInfo(), API_LOG);
// </Debug>
-class FeverDAO extends Minz_ModelPdo
+final class FeverDAO extends Minz_ModelPdo
{
+ /**
+ * @param array<string|int> $values
+ * @param array<string,string|int> $bindArray
+ */
protected function bindParamArray(string $prefix, array $values, array &$bindArray): string {
$str = '';
for ($i = 0; $i < count($values); $i++) {
@@ -73,9 +75,11 @@ class FeverDAO extends Minz_ModelPdo
}
/**
+ * @param array<string|int> $feed_ids
+ * @param array<string> $entry_ids
* @return FreshRSS_Entry[]
*/
- public function findEntries(array $feed_ids, array $entry_ids, string $max_id, string $since_id) {
+ public function findEntries(array $feed_ids, array $entry_ids, string $max_id, string $since_id): array {
$values = array();
$order = '';
$entryDAO = FreshRSS_Factory::createEntryDao();
@@ -109,36 +113,34 @@ class FeverDAO extends Minz_ModelPdo
$sql .= ' LIMIT 50';
$stm = $this->pdo->prepare($sql);
- $stm->execute($values);
- $result = $stm->fetchAll(PDO::FETCH_ASSOC);
+ if ($stm && $stm->execute($values)) {
+ $result = $stm->fetchAll(PDO::FETCH_ASSOC);
- $entries = array();
- foreach ($result as $dao) {
- $entries[] = FreshRSS_Entry::fromArray($dao);
- }
+ $entries = array();
+ foreach ($result as $dao) {
+ $entries[] = FreshRSS_Entry::fromArray($dao);
+ }
- return $entries;
+ return $entries;
+ }
+ return [];
}
}
/**
* Class FeverAPI
*/
-class FeverAPI
+final class FeverAPI
{
const API_LEVEL = 3;
const STATUS_OK = 1;
const STATUS_ERR = 0;
- /**
- * @var FreshRSS_EntryDAO|null
- */
- private $entryDAO = null;
+ /** @var FreshRSS_EntryDAO */
+ private $entryDAO;
- /**
- * @var FreshRSS_FeedDAO|null
- */
- private $feedDAO = null;
+ /** @var FreshRSS_FeedDAO */
+ private $feedDAO;
/**
* Authenticate the user
@@ -147,6 +149,9 @@ class FeverAPI
* your FreshRSS "username:your-api-password" combination
*/
private function authenticate(): bool {
+ if (FreshRSS_Context::$system_conf === null) {
+ throw new FreshRSS_Context_Exception('System configuration not initialised!');
+ }
FreshRSS_Context::$user_conf = null;
Minz_Session::_param('currentUser');
$feverKey = empty($_POST['api_key']) ? '' : substr(trim($_POST['api_key']), 0, 128);
@@ -175,16 +180,12 @@ class FeverAPI
public function isAuthenticatedApiUser(): bool {
$this->authenticate();
-
- if (FreshRSS_Context::$user_conf !== null) {
- return true;
- }
-
- return false;
+ return FreshRSS_Context::$user_conf !== null;
}
/**
* This does all the processing, since the fever api does not have a specific variable that specifies the operation
+ * @return array<string,mixed>
* @throws Exception
*/
public function process(): array {
@@ -225,37 +226,54 @@ class FeverAPI
$response_arr['saved_item_ids'] = $this->getSavedItemIds();
}
- $id = isset($_REQUEST['id']) ? '' . $_REQUEST['id'] : '';
- if (isset($_REQUEST['mark'], $_REQUEST['as'], $_REQUEST['id']) && ctype_digit($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)) {
- switch (strtolower($_REQUEST['mark'])) {
- case 'item':
- $this->{$method_name}($id);
- break;
- case 'feed':
- case 'group':
- $before = $_REQUEST['before'] ?? '';
- $this->{$method_name}($id, $before);
- break;
- }
+ if (isset($_REQUEST['mark'], $_REQUEST['as'], $_REQUEST['id']) && ctype_digit($_REQUEST['id'])) {
+ $id = intval($_REQUEST['id']);
+ $before = intval($_REQUEST['before'] ?? '0');
+ switch (strtolower($_REQUEST['mark'])) {
+ case 'item':
+ switch ($_REQUEST['as']) {
+ case 'read':
+ $this->setItemAsRead($id);
+ break;
+ case 'saved':
+ $this->setItemAsSaved($id);
+ break;
+ case 'unread':
+ $this->setItemAsUnread($id);
+ break;
+ case 'unsaved':
+ $this->setItemAsUnsaved($id);
+ break;
+ }
+ break;
+ case 'feed':
+ switch ($_REQUEST['as']) {
+ case 'read':
+ $this->setFeedAsRead($id, $before);
+ break;
+ }
+ break;
+ case 'group':
+ switch ($_REQUEST['as']) {
+ case 'read':
+ $this->setFeedAsRead($id, $before);
+ break;
+ }
+ break;
+ }
- switch ($_REQUEST['as']) {
- case 'read':
- case 'unread':
- $response_arr['unread_item_ids'] = $this->getUnreadItemIds();
- 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;
- }
+ case 'saved':
+ case 'unsaved':
+ $response_arr['saved_item_ids'] = $this->getSavedItemIds();
+ break;
}
+
}
return $response_arr;
@@ -263,6 +281,7 @@ class FeverAPI
/**
* Returns the complete JSON, with 'api_version' and status as 'auth'.
+ * @param array<string,mixed> $reply
*/
public function wrap(int $status, array $reply = array()): string {
$arr = array('api_version' => self::API_LEVEL, 'auth' => $status);
@@ -272,7 +291,7 @@ class FeverAPI
$arr = array_merge($arr, $reply);
}
- return json_encode($arr);
+ return json_encode($arr) ?: '';
}
/**
@@ -291,6 +310,7 @@ class FeverAPI
return $lastUpdate;
}
+ /** @return array<array<string,string|int>> */
protected function getFeeds(): array {
$feeds = array();
$myFeeds = $this->feedDAO->listFeeds();
@@ -311,6 +331,7 @@ class FeverAPI
return $feeds;
}
+ /** @return array<array<string,int|string>> */
protected function getGroups(): array {
$groups = array();
@@ -328,12 +349,15 @@ class FeverAPI
return $groups;
}
+ /** @return array<array<string,int|string>> */
protected function getFavicons(): array {
+ if (FreshRSS_Context::$system_conf == null) {
+ return [];
+ }
$favicons = array();
$salt = FreshRSS_Context::$system_conf->salt;
$myFeeds = $this->feedDAO->listFeeds();
- /** @var FreshRSS_Feed $feed */
foreach ($myFeeds as $feed) {
$id = hash('crc32b', $salt . $feed->url());
@@ -344,7 +368,7 @@ class FeverAPI
$favicons[] = array(
'id' => $feed->id(),
- 'data' => image_type_to_mime_type(exif_imagetype($filename)) . ';base64,' . base64_encode(file_get_contents($filename))
+ 'data' => image_type_to_mime_type(exif_imagetype($filename) ?: 0) . ';base64,' . base64_encode(file_get_contents($filename) ?: '')
);
}
@@ -358,17 +382,19 @@ class FeverAPI
return $this->entryDAO->count();
}
+ /**
+ * @return array<array<string,int|string>>
+ */
protected function getFeedsGroup(): array {
$groups = array();
$ids = array();
$myFeeds = $this->feedDAO->listFeeds();
- /** @var FreshRSS_Feed $feed */
foreach ($myFeeds as $feed) {
$ids[$feed->categoryId()][] = $feed->id();
}
- foreach($ids as $category => $feedIds) {
+ foreach ($ids as $category => $feedIds) {
$groups[] = array(
'group_id' => $category,
'feed_ids' => implode(',', $feedIds)
@@ -380,13 +406,14 @@ class FeverAPI
/**
* AFAIK there is no 'hot links' alternative in FreshRSS
+ * @return array<string>
*/
protected function getLinks(): array {
return array();
}
/**
- * @param array $ids
+ * @param array<string> $ids
*/
protected function entriesToIdList(array $ids = array()): string {
return implode(',', array_values($ids));
@@ -397,10 +424,7 @@ class FeverAPI
return $this->entriesToIdList($entries);
}
- /**
- * @return string
- */
- protected function getSavedItemIds() {
+ protected function getSavedItemIds(): string {
$entries = $this->entryDAO->listIdsWhere('a', '', FreshRSS_Entry::STATE_FAVORITE, 'ASC', 0);
return $this->entriesToIdList($entries);
}
@@ -408,31 +432,32 @@ class FeverAPI
/**
* @return integer|false
*/
- protected function setItemAsRead($id) {
+ protected function setItemAsRead(int $id) {
return $this->entryDAO->markRead($id, true);
}
/**
* @return integer|false
*/
- protected function setItemAsUnread($id) {
+ protected function setItemAsUnread(int $id) {
return $this->entryDAO->markRead($id, false);
}
/**
* @return integer|false
*/
- protected function setItemAsSaved($id) {
+ protected function setItemAsSaved(int $id) {
return $this->entryDAO->markFavorite($id, true);
}
/**
* @return integer|false
*/
- protected function setItemAsUnsaved($id) {
+ protected function setItemAsUnsaved(int $id) {
return $this->entryDAO->markFavorite($id, false);
}
+ /** @return array<array<string,string|int>> */
protected function getItems(): array {
$feed_ids = array();
$entry_ids = array();
@@ -447,16 +472,16 @@ class FeverAPI
if (isset($_REQUEST['group_ids'])) {
$categoryDAO = FreshRSS_Factory::createCategoryDao();
$group_ids = explode(',', $_REQUEST['group_ids']);
+ $feeds = [];
foreach ($group_ids as $id) {
- /** @var FreshRSS_Category $category */
$category = $categoryDAO->searchById($id); //TODO: Transform to SQL query without loop! Consider FreshRSS_CategoryDAO::listCategories(true)
- /** @var FreshRSS_Feed $feed */
- $feeds = [];
+ if ($category == null) {
+ continue;
+ }
foreach ($category->feeds() as $feed) {
$feeds[] = $feed->id();
}
}
-
$feed_ids = array_unique($feeds);
}
}
@@ -510,30 +535,30 @@ class FeverAPI
/**
* TODO replace by a dynamic fetch for id <= $before timestamp
*/
- protected function convertBeforeToId(string $beforeTimestamp): string {
- return $beforeTimestamp == '0' ? '0' : $beforeTimestamp . '000000';
+ protected function convertBeforeToId(int $beforeTimestamp): string {
+ return $beforeTimestamp == 0 ? '0' : $beforeTimestamp . '000000';
}
/**
* @return integer|false
*/
- protected function setFeedAsRead(string $id, string $before) {
+ protected function setFeedAsRead(int $id, int $before) {
$before = $this->convertBeforeToId($before);
- return $this->entryDAO->markReadFeed(intval($id), $before);
+ return $this->entryDAO->markReadFeed($id, $before);
}
/**
* @return integer|false
*/
- protected function setGroupAsRead(string $id, string $before) {
+ protected function setGroupAsRead(int $id, int $before) {
$before = $this->convertBeforeToId($before);
// special case to mark all items as read
- if ($id == '0') {
+ if ($id == 0) {
return $this->entryDAO->markReadEntries($before);
}
- return $this->entryDAO->markReadCat(intval($id), $before);
+ return $this->entryDAO->markReadCat($id, $before);
}
}
diff --git a/p/api/greader.php b/p/api/greader.php
index b08013850..5412bcf1d 100644
--- a/p/api/greader.php
+++ b/p/api/greader.php
@@ -26,23 +26,15 @@ Server-side API compatible with Google Reader API layer 2
require(__DIR__ . '/../../constants.php');
require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
-$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576);
+$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576) ?: '';
if (PHP_INT_SIZE < 8) { //32-bit
- /**
- * @param string $hex
- * @return string
- */
- function hex2dec($hex) {
+ function hex2dec(string $hex): string {
if (!ctype_xdigit($hex)) return '0';
return gmp_strval(gmp_init($hex, 16), 10);
}
} else { //64-bit
- /**
- * @param string $hex
- * @return string
- */
- function hex2dec($hex) {
+ function hex2dec(string $hex): string {
if (!ctype_xdigit($hex)) return '0';
return '' . hexdec($hex);
}
@@ -50,24 +42,28 @@ if (PHP_INT_SIZE < 8) { //32-bit
define('JSON_OPTIONS', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
-function headerVariable($headerName, $varName) {
+function headerVariable(string $headerName, string $varName): string {
$header = '';
$upName = 'HTTP_' . strtoupper($headerName);
if (isset($_SERVER[$upName])) {
- $header = $_SERVER[$upName];
+ $header = '' . $_SERVER[$upName];
} elseif (isset($_SERVER['REDIRECT_' . $upName])) {
- $header = $_SERVER['REDIRECT_' . $upName];
+ $header = '' . $_SERVER['REDIRECT_' . $upName];
} elseif (function_exists('getallheaders')) {
$ALL_HEADERS = getallheaders();
if (isset($ALL_HEADERS[$headerName])) {
- $header = $ALL_HEADERS[$headerName];
+ $header = '' . $ALL_HEADERS[$headerName];
}
}
parse_str($header, $pairs);
- return isset($pairs[$varName]) ? $pairs[$varName] : null;
+ if (empty($pairs[$varName])) {
+ return '';
+ }
+ return is_string($pairs[$varName]) ? $pairs[$varName] : '';
}
-function multiplePosts($name) {
+/** @return array<string> */
+function multiplePosts(string $name): array {
//https://bugs.php.net/bug.php?id=51633
global $ORIGINAL_INPUT;
$inputs = explode('&', $ORIGINAL_INPUT);
@@ -82,10 +78,7 @@ function multiplePosts($name) {
return $result;
}
-/**
- * @return string
- */
-function debugInfo() {
+function debugInfo(): string {
if (function_exists('getallheaders')) {
$ALL_HEADERS = getallheaders();
} else { //nginx http://php.net/getallheaders#84262
@@ -97,1035 +90,1119 @@ function debugInfo() {
}
}
global $ORIGINAL_INPUT;
- return print_r(
- array(
+ $log = sensitive_log([
'date' => date('c'),
'headers' => $ALL_HEADERS,
'_SERVER' => $_SERVER,
'_GET' => $_GET,
'_POST' => $_POST,
'_COOKIE' => $_COOKIE,
- 'INPUT' => $ORIGINAL_INPUT
- ), true);
+ 'INPUT' => $ORIGINAL_INPUT,
+ ]);
+ return print_r($log, true);
}
-function badRequest() {
- Minz_Log::warning('badRequest() ' . debugInfo(), API_LOG);
- header('HTTP/1.1 400 Bad Request');
- header('Content-Type: text/plain; charset=UTF-8');
- die('Bad Request!');
-}
+final class GReaderAPI {
-function unauthorized() {
- Minz_Log::warning('unauthorized() ' . debugInfo(), API_LOG);
- header('HTTP/1.1 401 Unauthorized');
- header('Content-Type: text/plain; charset=UTF-8');
- header('Google-Bad-Token: true');
- die('Unauthorized!');
-}
+ /** @return never */
+ private static function badRequest() {
+ Minz_Log::warning(__METHOD__, API_LOG);
+ Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
+ header('HTTP/1.1 400 Bad Request');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Bad Request!');
+ }
-function notImplemented() {
- Minz_Log::warning('notImplemented() ' . debugInfo(), API_LOG);
- header('HTTP/1.1 501 Not Implemented');
- header('Content-Type: text/plain; charset=UTF-8');
- die('Not Implemented!');
-}
+ /** @return never */
+ private static function unauthorized() {
+ Minz_Log::warning(__METHOD__, API_LOG);
+ Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
+ header('HTTP/1.1 401 Unauthorized');
+ header('Content-Type: text/plain; charset=UTF-8');
+ header('Google-Bad-Token: true');
+ die('Unauthorized!');
+ }
-function serviceUnavailable() {
- Minz_Log::warning('serviceUnavailable() ' . debugInfo(), API_LOG);
- header('HTTP/1.1 503 Service Unavailable');
- header('Content-Type: text/plain; charset=UTF-8');
- die('Service Unavailable!');
-}
+ /** @return never */
+ private static function internalServerError() {
+ Minz_Log::warning(__METHOD__, API_LOG);
+ Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
+ header('HTTP/1.1 500 Internal Server Error');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Internal Server Error!');
+ }
-function checkCompatibility() {
- Minz_Log::warning('checkCompatibility() ' . debugInfo(), API_LOG);
- header('Content-Type: text/plain; charset=UTF-8');
- if (PHP_INT_SIZE < 8 && !function_exists('gmp_init')) {
- die('FAIL 64-bit or GMP extension! Wrong PHP configuration.');
+ /** @return never */
+ private static function notImplemented() {
+ Minz_Log::warning(__METHOD__, API_LOG);
+ Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
+ header('HTTP/1.1 501 Not Implemented');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Not Implemented!');
}
- $headerAuth = headerVariable('Authorization', 'GoogleLogin_auth');
- if ($headerAuth == '') {
- die('FAIL get HTTP Authorization header! Wrong Web server configuration.');
+
+ /** @return never */
+ private static function serviceUnavailable() {
+ Minz_Log::warning(__METHOD__, API_LOG);
+ Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
+ header('HTTP/1.1 503 Service Unavailable');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Service Unavailable!');
}
- echo 'PASS';
- exit();
-}
-function authorizationToUser() {
- //Input is 'GoogleLogin auth', but PHP replaces spaces by '_' http://php.net/language.variables.external
- $headerAuth = headerVariable('Authorization', 'GoogleLogin_auth');
- if ($headerAuth != '') {
- $headerAuthX = explode('/', $headerAuth, 2);
- if (count($headerAuthX) === 2) {
- $user = $headerAuthX[0];
- if (FreshRSS_user_Controller::checkUsername($user)) {
- FreshRSS_Context::initUser($user);
- if (FreshRSS_Context::$user_conf == null) {
- Minz_Log::warning('Invalid API user ' . $user . ': configuration cannot be found.');
- unauthorized();
- }
- if (!FreshRSS_Context::$user_conf->enabled) {
- Minz_Log::warning('Invalid API user ' . $user . ': configuration cannot be found.');
- unauthorized();
- }
- if ($headerAuthX[1] === sha1(FreshRSS_Context::$system_conf->salt . $user . FreshRSS_Context::$user_conf->apiPasswordHash)) {
- return $user;
+ /** @return never */
+ private static function checkCompatibility() {
+ Minz_Log::warning(__METHOD__, API_LOG);
+ Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
+ header('Content-Type: text/plain; charset=UTF-8');
+ if (PHP_INT_SIZE < 8 && !function_exists('gmp_init')) {
+ die('FAIL 64-bit or GMP extension! Wrong PHP configuration.');
+ }
+ $headerAuth = headerVariable('Authorization', 'GoogleLogin_auth');
+ if ($headerAuth == '') {
+ die('FAIL get HTTP Authorization header! Wrong Web server configuration.');
+ }
+ echo 'PASS';
+ exit();
+ }
+
+ private static function authorizationToUser(): string {
+ //Input is 'GoogleLogin auth', but PHP replaces spaces by '_' http://php.net/language.variables.external
+ $headerAuth = headerVariable('Authorization', 'GoogleLogin_auth');
+ if ($headerAuth != '') {
+ $headerAuthX = explode('/', $headerAuth, 2);
+ if (count($headerAuthX) === 2) {
+ $user = $headerAuthX[0];
+ if (FreshRSS_user_Controller::checkUsername($user)) {
+ FreshRSS_Context::initUser($user);
+ if (FreshRSS_Context::$user_conf == null || FreshRSS_Context::$system_conf == null) {
+ Minz_Log::warning('Invalid API user ' . $user . ': configuration cannot be found.');
+ self::unauthorized();
+ }
+ if (!FreshRSS_Context::$user_conf->enabled) {
+ Minz_Log::warning('Invalid API user ' . $user . ': configuration cannot be found.');
+ self::unauthorized();
+ }
+ if ($headerAuthX[1] === sha1(FreshRSS_Context::$system_conf->salt . $user . FreshRSS_Context::$user_conf->apiPasswordHash)) {
+ return $user;
+ } else {
+ Minz_Log::warning('Invalid API authorisation for user ' . $user);
+ self::unauthorized();
+ }
} else {
- Minz_Log::warning('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1], API_LOG);
- Minz_Log::warning('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1]);
- unauthorized();
+ self::badRequest();
}
- } else {
- badRequest();
}
}
+ return '';
}
- return '';
-}
-function clientLogin($email, $pass) {
- //https://web.archive.org/web/20130604091042/http://undoc.in/clientLogin.html
- if (FreshRSS_user_Controller::checkUsername($email)) {
- FreshRSS_Context::initUser($email);
- if (FreshRSS_Context::$user_conf == null) {
- Minz_Log::warning('Invalid API user ' . $email . ': configuration cannot be found.');
- unauthorized();
- }
+ /** @return never */
+ private static function clientLogin(string $email, string $pass) {
+ //https://web.archive.org/web/20130604091042/http://undoc.in/clientLogin.html
+ if (FreshRSS_user_Controller::checkUsername($email)) {
+ FreshRSS_Context::initUser($email);
+ if (FreshRSS_Context::$user_conf == null || FreshRSS_Context::$system_conf == null) {
+ Minz_Log::warning('Invalid API user ' . $email . ': configuration cannot be found.');
+ self::unauthorized();
+ }
- if (FreshRSS_Context::$user_conf->apiPasswordHash != '' && password_verify($pass, FreshRSS_Context::$user_conf->apiPasswordHash)) {
- header('Content-Type: text/plain; charset=UTF-8');
- $auth = $email . '/' . sha1(FreshRSS_Context::$system_conf->salt . $email . FreshRSS_Context::$user_conf->apiPasswordHash);
- echo 'SID=', $auth, "\n",
- 'LSID=null', "\n", //Vienna RSS
- 'Auth=', $auth, "\n";
- exit();
+ if (FreshRSS_Context::$user_conf->apiPasswordHash != '' && password_verify($pass, FreshRSS_Context::$user_conf->apiPasswordHash)) {
+ header('Content-Type: text/plain; charset=UTF-8');
+ $auth = $email . '/' . sha1(FreshRSS_Context::$system_conf->salt . $email . FreshRSS_Context::$user_conf->apiPasswordHash);
+ echo 'SID=', $auth, "\n",
+ 'LSID=null', "\n", //Vienna RSS
+ 'Auth=', $auth, "\n";
+ exit();
+ } else {
+ Minz_Log::warning('Password API mismatch for user ' . $email);
+ self::unauthorized();
+ }
} else {
- Minz_Log::warning('Password API mismatch for user ' . $email);
- unauthorized();
+ self::badRequest();
}
- } else {
- badRequest();
}
- die();
-}
-function token($conf) {
-//http://blog.martindoms.com/2009/08/15/using-the-google-reader-api-part-1/
-//https://github.com/ericmann/gReader-Library/blob/master/greader.class.php
- $user = Minz_Session::param('currentUser', '_');
- //Minz_Log::debug('token('. $user . ')', API_LOG); //TODO: Implement real token that expires
- $token = str_pad(sha1(FreshRSS_Context::$system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z'); //Must have 57 characters
- echo $token, "\n";
- exit();
-}
+ /**
+ * @return never
+ */
+ private static function token(?FreshRSS_UserConfiguration $conf) {
+ //http://blog.martindoms.com/2009/08/15/using-the-google-reader-api-part-1/
+ //https://github.com/ericmann/gReader-Library/blob/master/greader.class.php
+ if ($conf == null || FreshRSS_Context::$system_conf == null) {
+ self::unauthorized();
+ }
+ $user = Minz_Session::param('currentUser', '_');
+ //Minz_Log::debug('token('. $user . ')', API_LOG); //TODO: Implement real token that expires
+ $token = str_pad(sha1(FreshRSS_Context::$system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z'); //Must have 57 characters
+ echo $token, "\n";
+ exit();
+ }
-function checkToken($conf, $token) {
-//http://code.google.com/p/google-reader-api/wiki/ActionToken
- $user = Minz_Session::param('currentUser', '_');
- if ($user !== '_' && ( //TODO: Check security consequences
- $token == '' || //FeedMe
- $token === 'x')) { //Reeder
- return true;
+ private static function checkToken(?FreshRSS_UserConfiguration $conf, string $token): bool {
+ //http://code.google.com/p/google-reader-api/wiki/ActionToken
+ if ($conf == null || FreshRSS_Context::$system_conf == null) {
+ self::unauthorized();
+ }
+ $user = Minz_Session::param('currentUser', '_');
+ if ($user !== '_' && ( //TODO: Check security consequences
+ $token == '' || //FeedMe
+ $token === 'x')) { //Reeder
+ return true;
+ }
+ if ($token === str_pad(sha1(FreshRSS_Context::$system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z')) {
+ return true;
+ }
+ Minz_Log::warning('Invalid POST token: ' . $token, API_LOG);
+ self::unauthorized();
}
- if ($token === str_pad(sha1(FreshRSS_Context::$system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z')) {
- return true;
+
+ /** @return never */
+ private static function userInfo() {
+ //https://github.com/theoldreader/api#user-info
+ if (FreshRSS_Context::$user_conf == null) {
+ self::unauthorized();
+ }
+ $user = Minz_Session::param('currentUser', '_');
+ exit(json_encode(array(
+ 'userId' => $user,
+ 'userName' => $user,
+ 'userProfileId' => $user,
+ 'userEmail' => FreshRSS_Context::$user_conf->mail_login,
+ ), JSON_OPTIONS));
}
- Minz_Log::warning('Invalid POST token: ' . $token, API_LOG);
- unauthorized();
-}
-function userInfo() {
- //https://github.com/theoldreader/api#user-info
- $user = Minz_Session::param('currentUser', '_');
- exit(json_encode(array(
- 'userId' => $user,
- 'userName' => $user,
- 'userProfileId' => $user,
- 'userEmail' => FreshRSS_Context::$user_conf->mail_login,
- ), JSON_OPTIONS));
-}
+ /** @return never */
+ private static function tagList() {
+ header('Content-Type: application/json; charset=UTF-8');
-function tagList() {
- header('Content-Type: application/json; charset=UTF-8');
-
- $tags = array(
- array('id' => 'user/-/state/com.google/starred'),
- //array('id' => 'user/-/state/com.google/broadcast', 'sortid' => '2'),
- );
-
- $categoryDAO = FreshRSS_Factory::createCategoryDao();
- $categories = $categoryDAO->listCategories(true, false);
- foreach ($categories as $cat) {
- $tags[] = array(
- 'id' => 'user/-/label/' . htmlspecialchars_decode($cat->name(), ENT_QUOTES),
- //'sortid' => $cat->name(),
- 'type' => 'folder', //Inoreader
+ $tags = array(
+ array('id' => 'user/-/state/com.google/starred'),
+ //array('id' => 'user/-/state/com.google/broadcast', 'sortid' => '2'),
);
- }
- $tagDAO = FreshRSS_Factory::createTagDao();
- $labels = $tagDAO->listTags(true);
- foreach ($labels as $label) {
- $tags[] = array(
- 'id' => 'user/-/label/' . htmlspecialchars_decode($label->name(), ENT_QUOTES),
- //'sortid' => $label->name(),
- 'type' => 'tag', //Inoreader
- 'unread_count' => $label->nbUnread(), //Inoreader
- );
- }
+ $categoryDAO = FreshRSS_Factory::createCategoryDao();
+ $categories = $categoryDAO->listCategories(true, false);
+ foreach ($categories as $cat) {
+ $tags[] = array(
+ 'id' => 'user/-/label/' . htmlspecialchars_decode($cat->name(), ENT_QUOTES),
+ //'sortid' => $cat->name(),
+ 'type' => 'folder', //Inoreader
+ );
+ }
- echo json_encode(array('tags' => $tags), JSON_OPTIONS), "\n";
- exit();
-}
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ $labels = $tagDAO->listTags(true);
+ foreach ($labels as $label) {
+ $tags[] = array(
+ 'id' => 'user/-/label/' . htmlspecialchars_decode($label->name(), ENT_QUOTES),
+ //'sortid' => $label->name(),
+ 'type' => 'tag', //Inoreader
+ 'unread_count' => $label->nbUnread(), //Inoreader
+ );
+ }
-function subscriptionExport() {
- $user = Minz_Session::param('currentUser', '_');
- $export_service = new FreshRSS_Export_Service($user);
- list($filename, $content) = $export_service->generateOpml();
- header('Content-Type: application/xml; charset=UTF-8');
- header('Content-disposition: attachment; filename="' . $filename . '"');
- echo $content;
- exit();
-}
+ echo json_encode(array('tags' => $tags), JSON_OPTIONS), "\n";
+ exit();
+ }
-function subscriptionImport($opml) {
- $user = Minz_Session::param('currentUser', '_');
- $importService = new FreshRSS_Import_Service($user);
- $importService->importOpml($opml);
- if ($importService->lastStatus()) {
- list($nbUpdatedFeeds, $feed, $nbNewArticles) = FreshRSS_feed_Controller::actualizeFeed(0, '', true);
- invalidateHttpCache($user);
- exit('OK');
- } else {
- badRequest();
+ /** @return never */
+ private static function subscriptionExport() {
+ $user = '' . Minz_Session::param('currentUser', '_');
+ $export_service = new FreshRSS_Export_Service($user);
+ list($filename, $content) = $export_service->generateOpml();
+ header('Content-Type: application/xml; charset=UTF-8');
+ header('Content-disposition: attachment; filename="' . $filename . '"');
+ echo $content;
+ exit();
}
-}
-function subscriptionList() {
- header('Content-Type: application/json; charset=UTF-8');
-
- $salt = FreshRSS_Context::$system_conf->salt;
- $faviconsUrl = Minz_Url::display('/f.php?', '', true);
- $faviconsUrl = str_replace('/api/greader.php/reader/api/0/subscription', '', $faviconsUrl); //Security if base_url is not set properly
- $subscriptions = array();
-
- $categoryDAO = FreshRSS_Factory::createCategoryDao();
- foreach ($categoryDAO->listCategories(true, true) as $cat) {
- foreach ($cat->feeds() as $feed) {
- $subscriptions[] = [
- 'id' => 'feed/' . $feed->id(),
- 'title' => escapeToUnicodeAlternative($feed->name(), true),
- 'categories' => [
- [
- 'id' => 'user/-/label/' . htmlspecialchars_decode($cat->name(), ENT_QUOTES),
- 'label' => htmlspecialchars_decode($cat->name(), ENT_QUOTES),
- ],
- ],
- //'sortid' => $feed->name(),
- //'firstitemmsec' => 0,
- 'url' => htmlspecialchars_decode($feed->url(), ENT_QUOTES),
- 'htmlUrl' => htmlspecialchars_decode($feed->website(), ENT_QUOTES),
- 'iconUrl' => $faviconsUrl . hash('crc32b', $salt . $feed->url()),
- ];
+ /** @return never */
+ private static function subscriptionImport(string $opml) {
+ $user = '' . Minz_Session::param('currentUser', '_');
+ $importService = new FreshRSS_Import_Service($user);
+ $importService->importOpml($opml);
+ if ($importService->lastStatus()) {
+ FreshRSS_feed_Controller::actualizeFeed(0, '', true);
+ invalidateHttpCache($user);
+ exit('OK');
+ } else {
+ self::badRequest();
}
}
- echo json_encode(array('subscriptions' => $subscriptions), JSON_OPTIONS), "\n";
- exit();
-}
+ /** @return never */
+ private static function subscriptionList() {
+ if (FreshRSS_Context::$system_conf == null) {
+ self::internalServerError();
+ }
+ header('Content-Type: application/json; charset=UTF-8');
+ $salt = FreshRSS_Context::$system_conf->salt;
+ $faviconsUrl = Minz_Url::display('/f.php?', '', true);
+ $faviconsUrl = str_replace('/api/greader.php/reader/api/0/subscription', '', $faviconsUrl); //Security if base_url is not set properly
+ $subscriptions = array();
-function subscriptionEdit($streamNames, $titles, $action, $add = '', $remove = '') {
- //https://github.com/mihaip/google-reader-api/blob/master/wiki/ApiSubscriptionEdit.wiki
- switch ($action) {
- case 'subscribe':
- case 'unsubscribe':
- case 'edit':
- break;
- default:
- badRequest();
- }
- $addCatId = 0;
- $categoryDAO = null;
- if ($add != '' || $remove != '') {
$categoryDAO = FreshRSS_Factory::createCategoryDao();
- }
- $c_name = '';
- if ($add != '' && strpos($add, 'user/') === 0) { //user/-/label/Example ; user/username/label/Example
- if (strpos($add, 'user/-/label/') === 0) {
- $c_name = substr($add, 13);
- } else {
- $user = Minz_Session::param('currentUser', '_');
- $prefix = 'user/' . $user . '/label/';
- if (strpos($add, $prefix) === 0) {
- $c_name = substr($add, strlen($prefix));
- } else {
- $c_name = '';
+ foreach ($categoryDAO->listCategories(true, true) as $cat) {
+ foreach ($cat->feeds() as $feed) {
+ $subscriptions[] = [
+ 'id' => 'feed/' . $feed->id(),
+ 'title' => escapeToUnicodeAlternative($feed->name(), true),
+ 'categories' => [
+ [
+ 'id' => 'user/-/label/' . htmlspecialchars_decode($cat->name(), ENT_QUOTES),
+ 'label' => htmlspecialchars_decode($cat->name(), ENT_QUOTES),
+ ],
+ ],
+ //'sortid' => $feed->name(),
+ //'firstitemmsec' => 0,
+ 'url' => htmlspecialchars_decode($feed->url(), ENT_QUOTES),
+ 'htmlUrl' => htmlspecialchars_decode($feed->website(), ENT_QUOTES),
+ 'iconUrl' => $faviconsUrl . hash('crc32b', $salt . $feed->url()),
+ ];
}
}
- $c_name = htmlspecialchars($c_name, ENT_COMPAT, 'UTF-8');
- $cat = $categoryDAO->searchByName($c_name);
- $addCatId = $cat == null ? 0 : $cat->id();
- } elseif ($remove != '' && strpos($remove, 'user/-/label/') === 0) {
- $addCatId = 1; //Default category
- }
- $feedDAO = FreshRSS_Factory::createFeedDao();
- if (!is_array($streamNames) || count($streamNames) < 1) {
- badRequest();
+
+ echo json_encode(array('subscriptions' => $subscriptions), JSON_OPTIONS), "\n";
+ exit();
}
- for ($i = count($streamNames) - 1; $i >= 0; $i--) {
- $streamUrl = $streamNames[$i]; //feed/http://example.net/sample.xml ; feed/338
- if (strpos($streamUrl, 'feed/') === 0) {
- $streamUrl = preg_replace('%^(feed/)+%', '', $streamUrl);
- $feedId = 0;
- if (ctype_digit($streamUrl)) {
- if ($action === 'subscribe') {
- continue;
- }
- $feedId = $streamUrl;
+
+ /**
+ * @param array<string> $streamNames
+ * @param array<string> $titles
+ * @return never
+ */
+ private static function subscriptionEdit(array $streamNames, array $titles, string $action, string $add = '', string $remove = '') {
+ //https://github.com/mihaip/google-reader-api/blob/master/wiki/ApiSubscriptionEdit.wiki
+ switch ($action) {
+ case 'subscribe':
+ case 'unsubscribe':
+ case 'edit':
+ break;
+ default:
+ self::badRequest();
+ }
+ $addCatId = 0;
+ $categoryDAO = null;
+ if ($add != '' || $remove != '') {
+ $categoryDAO = FreshRSS_Factory::createCategoryDao();
+ }
+ $c_name = '';
+ if ($add != '' && strpos($add, 'user/') === 0) { //user/-/label/Example ; user/username/label/Example
+ if (strpos($add, 'user/-/label/') === 0) {
+ $c_name = substr($add, 13);
} else {
- $streamUrl = htmlspecialchars($streamUrl, ENT_COMPAT, 'UTF-8');
- $feed = $feedDAO->searchByUrl($streamUrl);
- $feedId = $feed == null ? -1 : $feed->id();
+ $user = Minz_Session::param('currentUser', '_');
+ $prefix = 'user/' . $user . '/label/';
+ if (strpos($add, $prefix) === 0) {
+ $c_name = substr($add, strlen($prefix));
+ } else {
+ $c_name = '';
+ }
}
- $title = isset($titles[$i]) ? $titles[$i] : '';
- $title = htmlspecialchars($title, ENT_COMPAT, 'UTF-8');
- switch ($action) {
- case 'subscribe':
- if ($feedId <= 0) {
- $http_auth = '';
- try {
- $feed = FreshRSS_feed_Controller::addFeed($streamUrl, $title, $addCatId, $c_name, $http_auth);
- continue 2;
- } catch (Exception $e) {
- Minz_Log::error('subscriptionEdit error subscribe: ' . $e->getMessage(), API_LOG);
- }
- }
- badRequest();
- break;
- case 'unsubscribe':
- if (!($feedId > 0 && FreshRSS_feed_Controller::deleteFeed($feedId))) {
- badRequest();
+ $c_name = htmlspecialchars($c_name, ENT_COMPAT, 'UTF-8');
+ $cat = $categoryDAO->searchByName($c_name);
+ $addCatId = $cat == null ? 0 : $cat->id();
+ } elseif ($remove != '' && strpos($remove, 'user/-/label/') === 0) {
+ $addCatId = 1; //Default category
+ }
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ if (!is_array($streamNames) || count($streamNames) < 1) {
+ self::badRequest();
+ }
+ for ($i = count($streamNames) - 1; $i >= 0; $i--) {
+ $streamUrl = $streamNames[$i]; //feed/http://example.net/sample.xml ; feed/338
+ if (strpos($streamUrl, 'feed/') === 0) {
+ $streamUrl = '' . preg_replace('%^(feed/)+%', '', $streamUrl);
+ $feedId = 0;
+ if (ctype_digit($streamUrl)) {
+ if ($action === 'subscribe') {
+ continue;
}
- break;
- case 'edit':
- if ($feedId > 0) {
- if ($addCatId > 0 || $c_name != '') {
- FreshRSS_feed_Controller::moveFeed($feedId, $addCatId, $c_name);
+ $feedId = $streamUrl;
+ } else {
+ $streamUrl = htmlspecialchars($streamUrl, ENT_COMPAT, 'UTF-8');
+ $feed = $feedDAO->searchByUrl($streamUrl);
+ $feedId = $feed == null ? -1 : $feed->id();
+ }
+ $title = isset($titles[$i]) ? $titles[$i] : '';
+ $title = htmlspecialchars($title, ENT_COMPAT, 'UTF-8');
+ switch ($action) {
+ case 'subscribe':
+ if ($feedId <= 0) {
+ $http_auth = '';
+ try {
+ $feed = FreshRSS_feed_Controller::addFeed($streamUrl, $title, $addCatId, $c_name, $http_auth);
+ continue 2;
+ } catch (Exception $e) {
+ Minz_Log::error('subscriptionEdit error subscribe: ' . $e->getMessage(), API_LOG);
+ }
}
- if ($title != '') {
- FreshRSS_feed_Controller::renameFeed($feedId, $title);
+ self::badRequest();
+ // Always exits
+ case 'unsubscribe':
+ if (!($feedId > 0 && FreshRSS_feed_Controller::deleteFeed($feedId))) {
+ self::badRequest();
}
- } else {
- badRequest();
- }
- break;
+ break;
+ case 'edit':
+ if ($feedId > 0) {
+ if ($addCatId > 0 || $c_name != '') {
+ FreshRSS_feed_Controller::moveFeed($feedId, $addCatId, $c_name);
+ }
+ if ($title != '') {
+ FreshRSS_feed_Controller::renameFeed($feedId, $title);
+ }
+ } else {
+ self::badRequest();
+ }
+ break;
+ }
}
}
+ exit('OK');
}
- exit('OK');
-}
-function quickadd($url) {
- try {
- $url = htmlspecialchars($url, ENT_COMPAT, 'UTF-8');
- if (substr($url, 0, 5) === 'feed/') {
- $url = substr($url, 5);
+ /** @return never */
+ private static function quickadd(string $url) {
+ try {
+ $url = htmlspecialchars($url, ENT_COMPAT, 'UTF-8');
+ if (substr($url, 0, 5) === 'feed/') {
+ $url = substr($url, 5);
+ }
+ $feed = FreshRSS_feed_Controller::addFeed($url);
+ exit(json_encode(array(
+ 'numResults' => 1,
+ 'query' => $feed->url(),
+ 'streamId' => 'feed/' . $feed->id(),
+ 'streamName' => $feed->name(),
+ ), JSON_OPTIONS));
+ } catch (Exception $e) {
+ Minz_Log::error('quickadd error: ' . $e->getMessage(), API_LOG);
+ die(json_encode(array(
+ 'numResults' => 0,
+ 'error' => $e->getMessage(),
+ ), JSON_OPTIONS));
}
- $feed = FreshRSS_feed_Controller::addFeed($url);
- exit(json_encode(array(
- 'numResults' => 1,
- 'query' => $feed->url(),
- 'streamId' => 'feed/' . $feed->id(),
- 'streamName' => $feed->name(),
- ), JSON_OPTIONS));
- } catch (Exception $e) {
- Minz_Log::error('quickadd error: ' . $e->getMessage(), API_LOG);
- die(json_encode(array(
- 'numResults' => 0,
- 'error' => $e->getMessage(),
- ), JSON_OPTIONS));
}
-}
-
-function unreadCount() {
- //http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#unread-count
- header('Content-Type: application/json; charset=UTF-8');
- $totalUnreads = 0;
- $totalLastUpdate = 0;
+ /** @return never */
+ private static function unreadCount() {
+ //http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#unread-count
+ header('Content-Type: application/json; charset=UTF-8');
- $categoryDAO = FreshRSS_Factory::createCategoryDao();
- $feedDAO = FreshRSS_Factory::createFeedDao();
- $feedsNewestItemUsec = $feedDAO->listFeedsNewestItemUsec();
+ $totalUnreads = 0;
+ $totalLastUpdate = 0;
- foreach ($categoryDAO->listCategories(true, true) as $cat) {
- $catLastUpdate = 0;
- foreach ($cat->feeds() as $feed) {
- $lastUpdate = isset($feedsNewestItemUsec['f_' . $feed->id()]) ? $feedsNewestItemUsec['f_' . $feed->id()] : 0;
+ $categoryDAO = FreshRSS_Factory::createCategoryDao();
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $feedsNewestItemUsec = $feedDAO->listFeedsNewestItemUsec();
+
+ foreach ($categoryDAO->listCategories(true, true) as $cat) {
+ $catLastUpdate = 0;
+ foreach ($cat->feeds() as $feed) {
+ $lastUpdate = isset($feedsNewestItemUsec['f_' . $feed->id()]) ? $feedsNewestItemUsec['f_' . $feed->id()] : 0;
+ $unreadcounts[] = array(
+ 'id' => 'feed/' . $feed->id(),
+ 'count' => $feed->nbNotRead(),
+ 'newestItemTimestampUsec' => '' . $lastUpdate,
+ );
+ if ($catLastUpdate < $lastUpdate) {
+ $catLastUpdate = $lastUpdate;
+ }
+ }
$unreadcounts[] = array(
- 'id' => 'feed/' . $feed->id(),
- 'count' => $feed->nbNotRead(),
- 'newestItemTimestampUsec' => '' . $lastUpdate,
+ 'id' => 'user/-/label/' . htmlspecialchars_decode($cat->name(), ENT_QUOTES),
+ 'count' => $cat->nbNotRead(),
+ 'newestItemTimestampUsec' => '' . $catLastUpdate,
);
- if ($catLastUpdate < $lastUpdate) {
- $catLastUpdate = $lastUpdate;
+ $totalUnreads += $cat->nbNotRead();
+ if ($totalLastUpdate < $catLastUpdate) {
+ $totalLastUpdate = $catLastUpdate;
}
}
- $unreadcounts[] = array(
- 'id' => 'user/-/label/' . htmlspecialchars_decode($cat->name(), ENT_QUOTES),
- 'count' => $cat->nbNotRead(),
- 'newestItemTimestampUsec' => '' . $catLastUpdate,
- );
- $totalUnreads += $cat->nbNotRead();
- if ($totalLastUpdate < $catLastUpdate) {
- $totalLastUpdate = $catLastUpdate;
+
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ $tagsNewestItemUsec = $tagDAO->listTagsNewestItemUsec();
+ foreach ($tagDAO->listTags(true) as $label) {
+ $lastUpdate = isset($tagsNewestItemUsec['t_' . $label->id()]) ? $tagsNewestItemUsec['t_' . $label->id()] : 0;
+ $unreadcounts[] = array(
+ 'id' => 'user/-/label/' . htmlspecialchars_decode($label->name(), ENT_QUOTES),
+ 'count' => $label->nbUnread(),
+ 'newestItemTimestampUsec' => '' . $lastUpdate,
+ );
}
- }
- $tagDAO = FreshRSS_Factory::createTagDao();
- $tagsNewestItemUsec = $tagDAO->listTagsNewestItemUsec();
- foreach ($tagDAO->listTags(true) as $label) {
- $lastUpdate = isset($tagsNewestItemUsec['t_' . $label->id()]) ? $tagsNewestItemUsec['t_' . $label->id()] : 0;
$unreadcounts[] = array(
- 'id' => 'user/-/label/' . htmlspecialchars_decode($label->name(), ENT_QUOTES),
- 'count' => $label->nbUnread(),
- 'newestItemTimestampUsec' => '' . $lastUpdate,
+ 'id' => 'user/-/state/com.google/reading-list',
+ 'count' => $totalUnreads,
+ 'newestItemTimestampUsec' => '' . $totalLastUpdate,
);
- }
- $unreadcounts[] = array(
- 'id' => 'user/-/state/com.google/reading-list',
- 'count' => $totalUnreads,
- 'newestItemTimestampUsec' => '' . $totalLastUpdate,
- );
-
- echo json_encode(array(
- 'max' => $totalUnreads,
- 'unreadcounts' => $unreadcounts,
- ), JSON_OPTIONS), "\n";
- exit();
-}
-
-function entriesToArray($entries) {
- if (empty($entries)) {
- return array();
+ echo json_encode(array(
+ 'max' => $totalUnreads,
+ 'unreadcounts' => $unreadcounts,
+ ), JSON_OPTIONS), "\n";
+ exit();
}
- $catDAO = FreshRSS_Factory::createCategoryDao();
- $categories = $catDAO->listCategories(true);
- $tagDAO = FreshRSS_Factory::createTagDao();
- $entryIdsTagNames = $tagDAO->getEntryIdsTagNames($entries);
- if ($entryIdsTagNames == false) {
- $entryIdsTagNames = array();
- }
+ /**
+ * @param array<FreshRSS_Entry> $entries
+ * @return array<array<string,mixed>>
+ */
+ private static function entriesToArray(array $entries): array {
+ if (empty($entries)) {
+ return array();
+ }
+ $catDAO = FreshRSS_Factory::createCategoryDao();
+ $categories = $catDAO->listCategories(true);
- $items = array();
- foreach ($entries as $item) {
- /** @var FreshRSS_Entry $entry */
- $entry = Minz_ExtensionManager::callHook('entry_before_display', $item);
- if ($entry == null) {
- continue;
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ $entryIdsTagNames = $tagDAO->getEntryIdsTagNames($entries);
+ if ($entryIdsTagNames == false) {
+ $entryIdsTagNames = array();
}
- $feed = FreshRSS_CategoryDAO::findFeed($categories, $entry->feedId());
- $entry->_feed($feed);
+ $items = array();
+ foreach ($entries as $item) {
+ /** @var FreshRSS_Entry $entry */
+ $entry = Minz_ExtensionManager::callHook('entry_before_display', $item);
+ if ($entry == null) {
+ continue;
+ }
- if (isset($entryIdsTagNames['e_' . $entry->id()])) {
- $entry->_tags($entryIdsTagNames['e_' . $entry->id()]);
- }
+ $feed = FreshRSS_CategoryDAO::findFeed($categories, $entry->feedId());
+ $entry->_feed($feed);
- $items[] = $entry->toGReader('compat');
+ if (isset($entryIdsTagNames['e_' . $entry->id()])) {
+ $entry->_tags($entryIdsTagNames['e_' . $entry->id()]);
+ }
+
+ $items[] = $entry->toGReader('compat');
+ }
+ return $items;
}
- return $items;
-}
-function streamContentsFilters($type, $streamId, $filter_target, $exclude_target, $start_time, $stop_time) {
- switch ($type) {
- case 'f': //feed
- if ($streamId != '' && !ctype_digit($streamId)) {
- $feedDAO = FreshRSS_Factory::createFeedDao();
+ /**
+ * @return array<string|int|FreshRSS_BooleanSearch>
+ */
+ private static function streamContentsFilters(string $type, string $streamId,
+ string $filter_target, string $exclude_target, int $start_time, int $stop_time): array {
+ switch ($type) {
+ case 'f': //feed
+ if ($streamId != '' && !ctype_digit($streamId)) {
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $streamId = htmlspecialchars($streamId, ENT_COMPAT, 'UTF-8');
+ $feed = $feedDAO->searchByUrl($streamId);
+ $streamId = $feed == null ? -1 : $feed->id();
+ }
+ break;
+ case 'c': //category or label
+ $categoryDAO = FreshRSS_Factory::createCategoryDao();
$streamId = htmlspecialchars($streamId, ENT_COMPAT, 'UTF-8');
- $feed = $feedDAO->searchByUrl($streamId);
- $streamId = $feed == null ? -1 : $feed->id();
- }
- break;
- case 'c': //category or label
- $categoryDAO = FreshRSS_Factory::createCategoryDao();
- $streamId = htmlspecialchars($streamId, ENT_COMPAT, 'UTF-8');
- $cat = $categoryDAO->searchByName($streamId);
- if ($cat != null) {
- $type = 'c';
- $streamId = $cat->id();
- } else {
- $tagDAO = FreshRSS_Factory::createTagDao();
- $tag = $tagDAO->searchByName($streamId);
- if ($tag != null) {
- $type = 't';
- $streamId = $tag->id();
+ $cat = $categoryDAO->searchByName($streamId);
+ if ($cat != null) {
+ $type = 'c';
+ $streamId = $cat->id();
} else {
- $type = 'A';
- $streamId = -1;
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ $tag = $tagDAO->searchByName($streamId);
+ if ($tag != null) {
+ $type = 't';
+ $streamId = $tag->id();
+ } else {
+ $type = 'A';
+ $streamId = -1;
+ }
}
- }
- break;
- }
+ break;
+ }
- switch ($filter_target) {
- case 'user/-/state/com.google/read':
- $state = FreshRSS_Entry::STATE_READ;
- break;
- case 'user/-/state/com.google/unread':
- $state = FreshRSS_Entry::STATE_NOT_READ;
- break;
- case 'user/-/state/com.google/starred':
- $state = FreshRSS_Entry::STATE_FAVORITE;
- break;
- default:
- $state = FreshRSS_Entry::STATE_ALL;
- break;
- }
+ switch ($filter_target) {
+ case 'user/-/state/com.google/read':
+ $state = FreshRSS_Entry::STATE_READ;
+ break;
+ case 'user/-/state/com.google/unread':
+ $state = FreshRSS_Entry::STATE_NOT_READ;
+ break;
+ case 'user/-/state/com.google/starred':
+ $state = FreshRSS_Entry::STATE_FAVORITE;
+ break;
+ default:
+ $state = FreshRSS_Entry::STATE_ALL;
+ break;
+ }
- switch ($exclude_target) {
- case 'user/-/state/com.google/read':
- $state &= FreshRSS_Entry::STATE_NOT_READ;
- break;
- case 'user/-/state/com.google/unread':
- $state &= FreshRSS_Entry::STATE_READ;
- break;
- case 'user/-/state/com.google/starred':
- $state &= FreshRSS_Entry::STATE_NOT_FAVORITE;
- break;
- }
+ switch ($exclude_target) {
+ case 'user/-/state/com.google/read':
+ $state &= FreshRSS_Entry::STATE_NOT_READ;
+ break;
+ case 'user/-/state/com.google/unread':
+ $state &= FreshRSS_Entry::STATE_READ;
+ break;
+ case 'user/-/state/com.google/starred':
+ $state &= FreshRSS_Entry::STATE_NOT_FAVORITE;
+ break;
+ }
- $searches = new FreshRSS_BooleanSearch('');
- if ($start_time != '') {
- $search = new FreshRSS_Search('');
- $search->setMinDate($start_time);
- $searches->add($search);
- }
- if ($stop_time != '') {
- $search = new FreshRSS_Search('');
- $search->setMaxDate($stop_time);
- $searches->add($search);
+ $searches = new FreshRSS_BooleanSearch('');
+ if ($start_time != '') {
+ $search = new FreshRSS_Search('');
+ $search->setMinDate($start_time);
+ $searches->add($search);
+ }
+ if ($stop_time != '') {
+ $search = new FreshRSS_Search('');
+ $search->setMaxDate($stop_time);
+ $searches->add($search);
+ }
+
+ return array($type, $streamId, $state, $searches);
}
- return array($type, $streamId, $state, $searches);
-}
+ /** @return never */
+ private static function streamContents(string $path, string $include_target, int $start_time, int $stop_time, int $count,
+ string $order, string $filter_target, string $exclude_target, string $continuation) {
+ //http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI
+ //http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#feed
+ header('Content-Type: application/json; charset=UTF-8');
+
+ switch ($path) {
+ case 'reading-list':
+ $type = 'A';
+ break;
+ case 'starred':
+ $type = 's';
+ break;
+ case 'feed':
+ $type = 'f';
+ break;
+ case 'label':
+ $type = 'c';
+ break;
+ default:
+ $type = 'A';
+ break;
+ }
-function streamContents($path, $include_target, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation) {
-//http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI
-//http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#feed
- header('Content-Type: application/json; charset=UTF-8');
+ list($type, $include_target, $state, $searches) =
+ self::streamContentsFilters($type, $include_target, $filter_target, $exclude_target, $start_time, $stop_time);
- switch ($path) {
- case 'reading-list':
- $type = 'A';
- break;
- case 'starred':
- $type = 's';
- break;
- case 'feed':
- $type = 'f';
- break;
- case 'label':
- $type = 'c';
- break;
- default:
- $type = 'A';
- break;
- }
+ if ($continuation != '') {
+ $count++; //Shift by one element
+ }
- list($type, $include_target, $state, $searches) = streamContentsFilters($type, $include_target, $filter_target, $exclude_target, $start_time, $stop_time);
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ $entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, $searches);
+ $entries = iterator_to_array($entries); //TODO: Improve
- if ($continuation != '') {
- $count++; //Shift by one element
- }
+ $items = self::entriesToArray($entries);
- $entryDAO = FreshRSS_Factory::createEntryDao();
- $entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, $searches);
- $entries = iterator_to_array($entries); //TODO: Improve
+ if ($continuation != '') {
+ array_shift($items); //Discard first element that was already sent in the previous response
+ $count--;
+ }
- $items = entriesToArray($entries);
+ $response = array(
+ 'id' => 'user/-/state/com.google/reading-list',
+ 'updated' => time(),
+ 'items' => $items,
+ );
+ if (count($entries) >= $count) {
+ $entry = end($entries);
+ if ($entry != false) {
+ $response['continuation'] = '' . $entry->id();
+ }
+ }
- if ($continuation != '') {
- array_shift($items); //Discard first element that was already sent in the previous response
- $count--;
+ echo json_encode($response, JSON_OPTIONS), "\n";
+ exit();
}
- $response = array(
- 'id' => 'user/-/state/com.google/reading-list',
- 'updated' => time(),
- 'items' => $items,
- );
- if (count($entries) >= $count) {
- $entry = end($entries);
- if ($entry != false) {
- $response['continuation'] = '' . $entry->id();
+ /** @return never */
+ private static function streamContentsItemsIds(string $streamId, int $start_time, int $stop_time, int $count,
+ string $order, string $filter_target, string $exclude_target, string $continuation) {
+ //http://code.google.com/p/google-reader-api/wiki/ApiStreamItemsIds
+ //http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI
+ //http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#feed
+ $type = 'A';
+ $id = '';
+ if ($streamId === 'user/-/state/com.google/reading-list') {
+ $type = 'A';
+ } elseif ($streamId === 'user/-/state/com.google/starred') {
+ $type = 's';
+ } elseif (strpos($streamId, 'feed/') === 0) {
+ $type = 'f';
+ $streamId = substr($streamId, 5);
+ } elseif (strpos($streamId, 'user/-/label/') === 0) {
+ $type = 'c';
+ $streamId = substr($streamId, 13);
}
- }
-
- echo json_encode($response, JSON_OPTIONS), "\n";
- exit();
-}
-function streamContentsItemsIds($streamId, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation) {
-//http://code.google.com/p/google-reader-api/wiki/ApiStreamItemsIds
-//http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI
-//http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#feed
- $type = 'A';
- $id = '';
- if ($streamId === 'user/-/state/com.google/reading-list') {
- $type = 'A';
- } elseif ($streamId === 'user/-/state/com.google/starred') {
- $type = 's';
- } elseif (strpos($streamId, 'feed/') === 0) {
- $type = 'f';
- $streamId = substr($streamId, 5);
- } elseif (strpos($streamId, 'user/-/label/') === 0) {
- $type = 'c';
- $streamId = substr($streamId, 13);
- }
+ list($type, $id, $state, $searches) = self::streamContentsFilters($type, $streamId, $filter_target, $exclude_target, $start_time, $stop_time);
- list($type, $id, $state, $searches) = streamContentsFilters($type, $streamId, $filter_target, $exclude_target, $start_time, $stop_time);
+ if ($continuation != '') {
+ $count++; //Shift by one element
+ }
- if ($continuation != '') {
- $count++; //Shift by one element
- }
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ $ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, $searches);
+ if ($ids === false) {
+ self::internalServerError();
+ }
- $entryDAO = FreshRSS_Factory::createEntryDao();
- $ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, $searches);
+ if ($continuation != '') {
+ array_shift($ids); //Discard first element that was already sent in the previous response
+ $count--;
+ }
- if ($continuation != '') {
- array_shift($ids); //Discard first element that was already sent in the previous response
- $count--;
- }
+ if (empty($ids) && isset($_GET['client']) && $_GET['client'] === 'newsplus') {
+ $ids = [ 0 ]; //For News+ bug https://github.com/noinnion/newsplus/issues/84#issuecomment-57834632
+ }
+ $itemRefs = array();
+ foreach ($ids as $id) {
+ $itemRefs[] = array(
+ 'id' => '' . $id, //64-bit decimal
+ );
+ }
- if (empty($ids) && isset($_GET['client']) && $_GET['client'] === 'newsplus') {
- $ids[] = 0; //For News+ bug https://github.com/noinnion/newsplus/issues/84#issuecomment-57834632
- }
- $itemRefs = array();
- foreach ($ids as $id) {
- $itemRefs[] = array(
- 'id' => '' . $id, //64-bit decimal
+ $response = array(
+ 'itemRefs' => $itemRefs,
);
- }
-
- $response = array(
- 'itemRefs' => $itemRefs,
- );
- if (count($ids) >= $count) {
- $id = end($ids);
- if ($id != false) {
- $response['continuation'] = '' . $id;
+ if (count($ids) >= $count) {
+ $id = end($ids);
+ if ($id != false) {
+ $response['continuation'] = '' . $id;
+ }
}
- }
- echo json_encode($response, JSON_OPTIONS), "\n";
- exit();
-}
+ echo json_encode($response, JSON_OPTIONS), "\n";
+ exit();
+ }
-function streamContentsItems($e_ids, $order) {
- header('Content-Type: application/json; charset=UTF-8');
+ /**
+ * @param array<string> $e_ids
+ * @return never
+ */
+ private static function streamContentsItems(array $e_ids, string $order) {
+ header('Content-Type: application/json; charset=UTF-8');
- foreach ($e_ids as $i => $e_id) {
- // https://feedhq.readthedocs.io/en/latest/api/terminology.html#items
- if (!ctype_digit($e_id) || $e_id[0] === '0') {
- $e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/'
+ foreach ($e_ids as $i => $e_id) {
+ // https://feedhq.readthedocs.io/en/latest/api/terminology.html#items
+ if (!ctype_digit($e_id) || $e_id[0] === '0') {
+ $e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/'
+ }
}
- }
- $entryDAO = FreshRSS_Factory::createEntryDao();
- $entries = $entryDAO->listByIds($e_ids, $order === 'o' ? 'ASC' : 'DESC');
- $entries = iterator_to_array($entries); //TODO: Improve
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ $entries = $entryDAO->listByIds($e_ids, $order === 'o' ? 'ASC' : 'DESC');
+ $entries = iterator_to_array($entries); //TODO: Improve
- $items = entriesToArray($entries);
+ $items = self::entriesToArray($entries);
- $response = array(
- 'id' => 'user/-/state/com.google/reading-list',
- 'updated' => time(),
- 'items' => $items,
- );
+ $response = array(
+ 'id' => 'user/-/state/com.google/reading-list',
+ 'updated' => time(),
+ 'items' => $items,
+ );
- echo json_encode($response, JSON_OPTIONS), "\n";
- exit();
-}
+ echo json_encode($response, JSON_OPTIONS), "\n";
+ exit();
+ }
-function editTag($e_ids, $a, $r) {
- foreach ($e_ids as $i => $e_id) {
- if (!ctype_digit($e_id) || $e_id[0] === '0') {
- $e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/'
+ /**
+ * @param array<string> $e_ids
+ * @return never
+ */
+ private static function editTag(array $e_ids, string $a, string $r): void {
+ foreach ($e_ids as $i => $e_id) {
+ if (!ctype_digit($e_id) || $e_id[0] === '0') {
+ $e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/'
+ }
}
- }
- $entryDAO = FreshRSS_Factory::createEntryDao();
- $tagDAO = FreshRSS_Factory::createTagDao();
-
- switch ($a) {
- case 'user/-/state/com.google/read':
- $entryDAO->markRead($e_ids, true);
- break;
- case 'user/-/state/com.google/starred':
- $entryDAO->markFavorite($e_ids, true);
- break;
- /*case 'user/-/state/com.google/tracking-kept-unread':
- break;
- case 'user/-/state/com.google/like':
- break;
- case 'user/-/state/com.google/broadcast':
- break;*/
- default:
- $tagName = '';
- if (strpos($a, 'user/-/label/') === 0) {
- $tagName = substr($a, 13);
- } else {
- $user = Minz_Session::param('currentUser', '_');
- $prefix = 'user/' . $user . '/label/';
- if (strpos($a, $prefix) === 0) {
- $tagName = substr($a, strlen($prefix));
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ $tagDAO = FreshRSS_Factory::createTagDao();
+
+ switch ($a) {
+ case 'user/-/state/com.google/read':
+ $entryDAO->markRead($e_ids, true);
+ break;
+ case 'user/-/state/com.google/starred':
+ $entryDAO->markFavorite($e_ids, true);
+ break;
+ /*case 'user/-/state/com.google/tracking-kept-unread':
+ break;
+ case 'user/-/state/com.google/like':
+ break;
+ case 'user/-/state/com.google/broadcast':
+ break;*/
+ default:
+ $tagName = '';
+ if (strpos($a, 'user/-/label/') === 0) {
+ $tagName = substr($a, 13);
+ } else {
+ $user = Minz_Session::param('currentUser', '_');
+ $prefix = 'user/' . $user . '/label/';
+ if (strpos($a, $prefix) === 0) {
+ $tagName = substr($a, strlen($prefix));
+ }
}
- }
- if ($tagName != '') {
- $tagName = htmlspecialchars($tagName, ENT_COMPAT, 'UTF-8');
- $tag = $tagDAO->searchByName($tagName);
- if ($tag == null) {
- $tagDAO->addTag(array('name' => $tagName));
+ if ($tagName != '') {
+ $tagName = htmlspecialchars($tagName, ENT_COMPAT, 'UTF-8');
$tag = $tagDAO->searchByName($tagName);
- }
- if ($tag != null) {
- foreach ($e_ids as $e_id) {
- $tagDAO->tagEntry($tag->id(), $e_id, true);
+ if ($tag == null) {
+ $tagDAO->addTag(array('name' => $tagName));
+ $tag = $tagDAO->searchByName($tagName);
+ }
+ if ($tag != null) {
+ foreach ($e_ids as $e_id) {
+ $tagDAO->tagEntry($tag->id(), $e_id, true);
+ }
}
}
- }
- break;
- }
- switch ($r) {
- case 'user/-/state/com.google/read':
- $entryDAO->markRead($e_ids, false);
- break;
- case 'user/-/state/com.google/starred':
- $entryDAO->markFavorite($e_ids, false);
- break;
- default:
- if (strpos($r, 'user/-/label/') === 0) {
- $tagName = substr($r, 13);
- $tagName = htmlspecialchars($tagName, ENT_COMPAT, 'UTF-8');
- $tag = $tagDAO->searchByName($tagName);
- if ($tag != null) {
- foreach ($e_ids as $e_id) {
- $tagDAO->tagEntry($tag->id(), $e_id, false);
+ break;
+ }
+ switch ($r) {
+ case 'user/-/state/com.google/read':
+ $entryDAO->markRead($e_ids, false);
+ break;
+ case 'user/-/state/com.google/starred':
+ $entryDAO->markFavorite($e_ids, false);
+ break;
+ default:
+ if (strpos($r, 'user/-/label/') === 0) {
+ $tagName = substr($r, 13);
+ $tagName = htmlspecialchars($tagName, ENT_COMPAT, 'UTF-8');
+ $tag = $tagDAO->searchByName($tagName);
+ if ($tag != null) {
+ foreach ($e_ids as $e_id) {
+ $tagDAO->tagEntry($tag->id(), $e_id, false);
+ }
}
}
- }
- break;
- }
+ break;
+ }
- exit('OK');
-}
+ exit('OK');
+ }
-function renameTag($s, $dest) {
- if ($s != '' && strpos($s, 'user/-/label/') === 0 &&
- $dest != '' && strpos($dest, 'user/-/label/') === 0) {
- $s = substr($s, 13);
- $s = htmlspecialchars($s, ENT_COMPAT, 'UTF-8');
- $dest = substr($dest, 13);
- $dest = htmlspecialchars($dest, ENT_COMPAT, 'UTF-8');
+ /** @return never */
+ private static function renameTag(string $s, string $dest) {
+ if ($s != '' && strpos($s, 'user/-/label/') === 0 &&
+ $dest != '' && strpos($dest, 'user/-/label/') === 0) {
+ $s = substr($s, 13);
+ $s = htmlspecialchars($s, ENT_COMPAT, 'UTF-8');
+ $dest = substr($dest, 13);
+ $dest = htmlspecialchars($dest, ENT_COMPAT, 'UTF-8');
- $categoryDAO = FreshRSS_Factory::createCategoryDao();
- $cat = $categoryDAO->searchByName($s);
- if ($cat != null) {
- $categoryDAO->updateCategory($cat->id(), array('name' => $dest));
- exit('OK');
- } else {
- $tagDAO = FreshRSS_Factory::createTagDao();
- $tag = $tagDAO->searchByName($s);
- if ($tag != null) {
- $tagDAO->updateTag($tag->id(), array('name' => $dest));
+ $categoryDAO = FreshRSS_Factory::createCategoryDao();
+ $cat = $categoryDAO->searchByName($s);
+ if ($cat != null) {
+ $categoryDAO->updateCategory($cat->id(), array('name' => $dest));
exit('OK');
+ } else {
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ $tag = $tagDAO->searchByName($s);
+ if ($tag != null) {
+ $tagDAO->updateTag($tag->id(), array('name' => $dest));
+ exit('OK');
+ }
}
}
+ self::badRequest();
}
- badRequest();
-}
-function disableTag($s) {
- if ($s != '' && strpos($s, 'user/-/label/') === 0) {
- $s = substr($s, 13);
- $s = htmlspecialchars($s, ENT_COMPAT, 'UTF-8');
- $categoryDAO = FreshRSS_Factory::createCategoryDao();
- $cat = $categoryDAO->searchByName($s);
- if ($cat != null) {
- $feedDAO = FreshRSS_Factory::createFeedDao();
- $feedDAO->changeCategory($cat->id(), 0);
- if ($cat->id() > 1) {
- $categoryDAO->deleteCategory($cat->id());
- }
- exit('OK');
- } else {
- $tagDAO = FreshRSS_Factory::createTagDao();
- $tag = $tagDAO->searchByName($s);
- if ($tag != null) {
- $tagDAO->deleteTag($tag->id());
+ /** @return never */
+ private static function disableTag(string $s) {
+ if ($s != '' && strpos($s, 'user/-/label/') === 0) {
+ $s = substr($s, 13);
+ $s = htmlspecialchars($s, ENT_COMPAT, 'UTF-8');
+ $categoryDAO = FreshRSS_Factory::createCategoryDao();
+ $cat = $categoryDAO->searchByName($s);
+ if ($cat != null) {
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $feedDAO->changeCategory($cat->id(), 0);
+ if ($cat->id() > 1) {
+ $categoryDAO->deleteCategory($cat->id());
+ }
exit('OK');
+ } else {
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ $tag = $tagDAO->searchByName($s);
+ if ($tag != null) {
+ $tagDAO->deleteTag($tag->id());
+ exit('OK');
+ }
}
}
+ self::badRequest();
}
- badRequest();
-}
-function markAllAsRead($streamId, $olderThanId) {
- $entryDAO = FreshRSS_Factory::createEntryDao();
- if (strpos($streamId, 'feed/') === 0) {
- $f_id = basename($streamId);
- if (!ctype_digit($f_id)) {
- badRequest();
- }
- $f_id = intval($f_id);
- $entryDAO->markReadFeed($f_id, $olderThanId);
- } elseif (strpos($streamId, 'user/-/label/') === 0) {
- $c_name = substr($streamId, 13);
- $c_name = htmlspecialchars($c_name, ENT_COMPAT, 'UTF-8');
- $categoryDAO = FreshRSS_Factory::createCategoryDao();
- $cat = $categoryDAO->searchByName($c_name);
- if ($cat != null) {
- $entryDAO->markReadCat($cat->id(), $olderThanId);
- } else {
- $tagDAO = FreshRSS_Factory::createTagDao();
- $tag = $tagDAO->searchByName($c_name);
- if ($tag != null) {
- $entryDAO->markReadTag($tag->id(), $olderThanId);
+ /** @return never */
+ private static function markAllAsRead(string $streamId, string $olderThanId) {
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ if (strpos($streamId, 'feed/') === 0) {
+ $f_id = basename($streamId);
+ if (!ctype_digit($f_id)) {
+ self::badRequest();
+ }
+ $f_id = intval($f_id);
+ $entryDAO->markReadFeed($f_id, $olderThanId);
+ } elseif (strpos($streamId, 'user/-/label/') === 0) {
+ $c_name = substr($streamId, 13);
+ $c_name = htmlspecialchars($c_name, ENT_COMPAT, 'UTF-8');
+ $categoryDAO = FreshRSS_Factory::createCategoryDao();
+ $cat = $categoryDAO->searchByName($c_name);
+ if ($cat != null) {
+ $entryDAO->markReadCat($cat->id(), $olderThanId);
} else {
- badRequest();
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ $tag = $tagDAO->searchByName($c_name);
+ if ($tag != null) {
+ $entryDAO->markReadTag($tag->id(), $olderThanId);
+ } else {
+ self::badRequest();
+ }
}
+ } elseif ($streamId === 'user/-/state/com.google/reading-list') {
+ $entryDAO->markReadEntries($olderThanId, false, -1);
+ } else {
+ self::badRequest();
}
- } elseif ($streamId === 'user/-/state/com.google/reading-list') {
- $entryDAO->markReadEntries($olderThanId, false, -1);
- } else {
- badRequest();
+ exit('OK');
}
- exit('OK');
-}
-$pathInfo = '';
-if (empty($_SERVER['PATH_INFO'])) {
- if (!empty($_SERVER['ORIG_PATH_INFO'])) {
- // Compatibility https://php.net/reserved.variables.server
- $pathInfo = $_SERVER['ORIG_PATH_INFO'];
- }
-} else {
- $pathInfo = $_SERVER['PATH_INFO'];
-}
-$pathInfo = urldecode($pathInfo);
-$pathInfo = preg_replace('%^(/api)?(/greader\.php)?%', '', $pathInfo); //Discard common errors
-if ($pathInfo == '') {
- exit('OK');
-}
-$pathInfos = explode('/', $pathInfo);
-if (count($pathInfos) < 3) {
- badRequest();
-}
+ /** @return never */
+ public static function parse() {
+ global $ORIGINAL_INPUT;
-FreshRSS_Context::initSystem();
+ $pathInfo = '';
+ if (empty($_SERVER['PATH_INFO'])) {
+ if (!empty($_SERVER['ORIG_PATH_INFO'])) {
+ // Compatibility https://php.net/reserved.variables.server
+ $pathInfo = $_SERVER['ORIG_PATH_INFO'];
+ }
+ } else {
+ $pathInfo = $_SERVER['PATH_INFO'];
+ }
+ $pathInfo = urldecode($pathInfo);
+ $pathInfo = '' . preg_replace('%^(/api)?(/greader\.php)?%', '', $pathInfo); //Discard common errors
+ if ($pathInfo == '') {
+ exit('OK');
+ }
+ $pathInfos = explode('/', $pathInfo);
+ if (count($pathInfos) < 3) {
+ self::badRequest();
+ }
-//Minz_Log::debug('----------------------------------------------------------------', API_LOG);
-//Minz_Log::debug(debugInfo(), API_LOG);
+ FreshRSS_Context::initSystem();
-if (!FreshRSS_Context::$system_conf->api_enabled) {
- serviceUnavailable();
-} elseif ($pathInfos[1] === 'check' && $pathInfos[2] === 'compatibility') {
- checkCompatibility();
-}
+ //Minz_Log::debug('----------------------------------------------------------------', API_LOG);
+ //Minz_Log::debug(debugInfo(), API_LOG);
-Minz_Session::init('FreshRSS', true);
+ if (FreshRSS_Context::$system_conf == null || !FreshRSS_Context::$system_conf->api_enabled) {
+ self::serviceUnavailable();
+ } elseif ($pathInfos[1] === 'check' && $pathInfos[2] === 'compatibility') {
+ self::checkCompatibility();
+ }
-if ($pathInfos[1] !== 'accounts') {
- authorizationToUser();
-}
-if (FreshRSS_Context::$user_conf != null) {
- Minz_Translate::init(FreshRSS_Context::$user_conf->language);
- Minz_ExtensionManager::init();
- Minz_ExtensionManager::enableByList(FreshRSS_Context::$user_conf->extensions_enabled);
-} else {
- Minz_Translate::init();
-}
+ Minz_Session::init('FreshRSS', true);
-if ($pathInfos[1] === 'accounts') {
- if (($pathInfos[2] === 'ClientLogin') && isset($_REQUEST['Email']) && isset($_REQUEST['Passwd'])) {
- clientLogin($_REQUEST['Email'], $_REQUEST['Passwd']);
- }
-} elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfos[3]) && $pathInfos[3] === '0' && isset($pathInfos[4])) {
- if (Minz_Session::param('currentUser', '') == '') {
- unauthorized();
- }
- $timestamp = isset($_GET['ck']) ? intval($_GET['ck']) : 0; //ck=[unix timestamp] : Use the current Unix time here, helps Google with caching.
- switch ($pathInfos[4]) {
- case 'stream':
- /* xt=[exclude target] : Used to exclude certain items from the feed.
- * For example, using xt=user/-/state/com.google/read will exclude items
- * that the current user has marked as read, or xt=feed/[feedurl] will
- * exclude items from a particular feed (obviously not useful in this
- * request, but xt appears in other listing requests). */
- $exclude_target = isset($_GET['xt']) ? $_GET['xt'] : '';
- $filter_target = isset($_GET['it']) ? $_GET['it'] : '';
- //n=[integer] : The maximum number of results to return.
- $count = isset($_GET['n']) ? intval($_GET['n']) : 20;
- //r=[d|n|o] : Sort order of item results. d or n gives items in descending date order, o in ascending order.
- $order = isset($_GET['r']) ? $_GET['r'] : 'd';
- /* ot=[unix timestamp] : The time from which you want to retrieve
- * items. Only items that have been crawled by Google Reader after
- * this time will be returned. */
- $start_time = isset($_GET['ot']) ? intval($_GET['ot']) : 0;
- $stop_time = isset($_GET['nt']) ? intval($_GET['nt']) : 0;
- /* Continuation token. If a StreamContents response does not represent
- * all items in a timestamp range, it will have a continuation attribute.
- * The same request can be re-issued with the value of that attribute put
- * in this parameter to get more items */
- $continuation = isset($_GET['c']) ? trim($_GET['c']) : '';
- if (!ctype_digit($continuation)) {
- $continuation = '';
+ if ($pathInfos[1] !== 'accounts') {
+ self::authorizationToUser();
+ }
+ if (FreshRSS_Context::$user_conf != null) {
+ Minz_Translate::init(FreshRSS_Context::$user_conf->language);
+ Minz_ExtensionManager::init();
+ Minz_ExtensionManager::enableByList(FreshRSS_Context::$user_conf->extensions_enabled);
+ } else {
+ Minz_Translate::init();
+ }
+
+ if ($pathInfos[1] === 'accounts') {
+ if (($pathInfos[2] === 'ClientLogin') && isset($_REQUEST['Email']) && isset($_REQUEST['Passwd'])) {
+ self::clientLogin($_REQUEST['Email'], $_REQUEST['Passwd']);
+ }
+ } elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfos[3]) && $pathInfos[3] === '0' && isset($pathInfos[4])) {
+ if (Minz_Session::param('currentUser', '') == '') {
+ self::unauthorized();
}
- if (isset($pathInfos[5]) && $pathInfos[5] === 'contents') {
- if (!isset($pathInfos[6]) && isset($_GET['s'])) {
- // Compatibility BazQux API https://github.com/bazqux/bazqux-api#fetching-streams
- $streamIdInfos = explode('/', $_GET['s']);
- foreach ($streamIdInfos as $streamIdInfo) {
- $pathInfos[] = $streamIdInfo;
+ $timestamp = isset($_GET['ck']) ? intval($_GET['ck']) : 0; //ck=[unix timestamp] : Use the current Unix time here, helps Google with caching.
+ switch ($pathInfos[4]) {
+ case 'stream':
+ /* xt=[exclude target] : Used to exclude certain items from the feed.
+ * For example, using xt=user/-/state/com.google/read will exclude items
+ * that the current user has marked as read, or xt=feed/[feedurl] will
+ * exclude items from a particular feed (obviously not useful in this
+ * request, but xt appears in other listing requests). */
+ $exclude_target = isset($_GET['xt']) ? $_GET['xt'] : '';
+ $filter_target = isset($_GET['it']) ? $_GET['it'] : '';
+ //n=[integer] : The maximum number of results to return.
+ $count = isset($_GET['n']) ? intval($_GET['n']) : 20;
+ //r=[d|n|o] : Sort order of item results. d or n gives items in descending date order, o in ascending order.
+ $order = isset($_GET['r']) ? $_GET['r'] : 'd';
+ /* ot=[unix timestamp] : The time from which you want to retrieve
+ * items. Only items that have been crawled by Google Reader after
+ * this time will be returned. */
+ $start_time = isset($_GET['ot']) ? intval($_GET['ot']) : 0;
+ $stop_time = isset($_GET['nt']) ? intval($_GET['nt']) : 0;
+ /* Continuation token. If a StreamContents response does not represent
+ * all items in a timestamp range, it will have a continuation attribute.
+ * The same request can be re-issued with the value of that attribute put
+ * in this parameter to get more items */
+ $continuation = isset($_GET['c']) ? trim($_GET['c']) : '';
+ if (!ctype_digit($continuation)) {
+ $continuation = '';
}
- }
- if (isset($pathInfos[6]) && isset($pathInfos[7])) {
- if ($pathInfos[6] === 'feed') {
- $include_target = $pathInfos[7];
- if ($include_target != '' && !ctype_digit($include_target)) {
- $include_target = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI'];
- if (preg_match('#/reader/api/0/stream/contents/feed/([A-Za-z0-9\'!*()%$_.~+-]+)#', $include_target, $matches) && isset($matches[1])) {
- $include_target = urldecode($matches[1]);
- } else {
- $include_target = '';
+ if (isset($pathInfos[5]) && $pathInfos[5] === 'contents') {
+ if (!isset($pathInfos[6]) && isset($_GET['s'])) {
+ // Compatibility BazQux API https://github.com/bazqux/bazqux-api#fetching-streams
+ $streamIdInfos = explode('/', $_GET['s']);
+ foreach ($streamIdInfos as $streamIdInfo) {
+ $pathInfos[] = $streamIdInfo;
}
}
- streamContents($pathInfos[6], $include_target, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
- } elseif ($pathInfos[6] === 'user' && isset($pathInfos[8]) && isset($pathInfos[9])) {
- if ($pathInfos[8] === 'state') {
- if ($pathInfos[9] === 'com.google' && isset($pathInfos[10])) {
- if ($pathInfos[10] === 'reading-list' || $pathInfos[10] === 'starred') {
- $include_target = '';
- streamContents($pathInfos[10], $include_target, $start_time, $stop_time, $count, $order,
- $filter_target, $exclude_target, $continuation);
+ if (isset($pathInfos[6]) && isset($pathInfos[7])) {
+ if ($pathInfos[6] === 'feed') {
+ $include_target = $pathInfos[7];
+ if ($include_target != '' && !ctype_digit($include_target)) {
+ $include_target = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI'];
+ if (preg_match('#/reader/api/0/stream/contents/feed/([A-Za-z0-9\'!*()%$_.~+-]+)#', $include_target, $matches)) {
+ $include_target = urldecode($matches[1]);
+ } else {
+ $include_target = '';
+ }
+ }
+ self::streamContents($pathInfos[6], $include_target, $start_time, $stop_time,
+ $count, $order, $filter_target, $exclude_target, $continuation);
+ } elseif ($pathInfos[6] === 'user' && isset($pathInfos[8]) && isset($pathInfos[9])) {
+ if ($pathInfos[8] === 'state') {
+ if ($pathInfos[9] === 'com.google' && isset($pathInfos[10])) {
+ if ($pathInfos[10] === 'reading-list' || $pathInfos[10] === 'starred') {
+ $include_target = '';
+ self::streamContents($pathInfos[10], $include_target, $start_time, $stop_time, $count, $order,
+ $filter_target, $exclude_target, $continuation);
+ }
+ }
+ } elseif ($pathInfos[8] === 'label') {
+ $include_target = $pathInfos[9];
+ self::streamContents($pathInfos[8], $include_target, $start_time, $stop_time,
+ $count, $order, $filter_target, $exclude_target, $continuation);
}
}
- } elseif ($pathInfos[8] === 'label') {
- $include_target = $pathInfos[9];
- streamContents($pathInfos[8], $include_target, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
+ } else { //EasyRSS, FeedMe
+ $include_target = '';
+ self::streamContents('reading-list', $include_target, $start_time, $stop_time,
+ $count, $order, $filter_target, $exclude_target, $continuation);
}
- }
- } else { //EasyRSS, FeedMe
- $include_target = '';
- streamContents('reading-list', $include_target, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
- }
- } elseif ($pathInfos[5] === 'items') {
- if ($pathInfos[6] === 'ids' && isset($_GET['s'])) {
- /* StreamId for which to fetch the item IDs. The parameter may
- * be repeated to fetch the item IDs from multiple streams at once
- * (more efficient from a backend perspective than multiple requests). */
- $streamId = $_GET['s'];
- streamContentsItemsIds($streamId, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
- } elseif ($pathInfos[6] === 'contents' && isset($_POST['i'])) { //FeedMe
- $e_ids = multiplePosts('i'); //item IDs
- streamContentsItems($e_ids, $order);
- }
- }
- break;
- case 'tag':
- if (isset($pathInfos[5]) && $pathInfos[5] === 'list') {
- $output = isset($_GET['output']) ? $_GET['output'] : '';
- if ($output !== 'json') notImplemented();
- tagList();
- }
- break;
- case 'subscription':
- if (isset($pathInfos[5])) {
- switch ($pathInfos[5]) {
- case 'export':
- subscriptionExport();
- break;
- case 'import':
- if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && $ORIGINAL_INPUT != '') {
- subscriptionImport($ORIGINAL_INPUT);
+ } elseif ($pathInfos[5] === 'items') {
+ if ($pathInfos[6] === 'ids' && isset($_GET['s'])) {
+ /* StreamId for which to fetch the item IDs. The parameter may
+ * be repeated to fetch the item IDs from multiple streams at once
+ * (more efficient from a backend perspective than multiple requests). */
+ $streamId = $_GET['s'];
+ self::streamContentsItemsIds($streamId, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
+ } elseif ($pathInfos[6] === 'contents' && isset($_POST['i'])) { //FeedMe
+ $e_ids = multiplePosts('i'); //item IDs
+ self::streamContentsItems($e_ids, $order);
}
- break;
- case 'list':
+ }
+ break;
+ case 'tag':
+ if (isset($pathInfos[5]) && $pathInfos[5] === 'list') {
$output = isset($_GET['output']) ? $_GET['output'] : '';
- if ($output !== 'json') notImplemented();
- subscriptionList();
- break;
- case 'edit':
- if (isset($_REQUEST['s']) && isset($_REQUEST['ac'])) {
- //StreamId to operate on. The parameter may be repeated to edit multiple subscriptions at once
- $streamNames = empty($_POST['s']) && isset($_GET['s']) ? array($_GET['s']) : multiplePosts('s');
- /* Title to use for the subscription. For the `subscribe` action,
- * if not specified then the feed’s current title will be used. Can
- * be used with the `edit` action to rename a subscription */
- $titles = empty($_POST['t']) && isset($_GET['t']) ? array($_GET['t']) : multiplePosts('t');
- $action = $_REQUEST['ac']; //Action to perform on the given StreamId. Possible values are `subscribe`, `unsubscribe` and `edit`
- $add = isset($_REQUEST['a']) ? $_REQUEST['a'] : ''; //StreamId to add the subscription to (generally a user label)
- $remove = isset($_REQUEST['r']) ? $_REQUEST['r'] : ''; //StreamId to remove the subscription from (generally a user label)
- subscriptionEdit($streamNames, $titles, $action, $add, $remove);
- }
- break;
- case 'quickadd': //https://github.com/theoldreader/api
- if (isset($_REQUEST['quickadd'])) {
- quickadd($_REQUEST['quickadd']);
+ if ($output !== 'json') self::notImplemented();
+ self::tagList();
+ }
+ break;
+ case 'subscription':
+ if (isset($pathInfos[5])) {
+ switch ($pathInfos[5]) {
+ case 'export':
+ self::subscriptionExport();
+ // Always exits
+ case 'import':
+ if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && $ORIGINAL_INPUT != '') {
+ self::subscriptionImport($ORIGINAL_INPUT);
+ }
+ break;
+ case 'list':
+ $output = isset($_GET['output']) ? $_GET['output'] : '';
+ if ($output !== 'json') self::notImplemented();
+ self::subscriptionList();
+ // Always exits
+ case 'edit':
+ if (isset($_REQUEST['s']) && isset($_REQUEST['ac'])) {
+ //StreamId to operate on. The parameter may be repeated to edit multiple subscriptions at once
+ $streamNames = empty($_POST['s']) && isset($_GET['s']) ? array($_GET['s']) : multiplePosts('s');
+ /* Title to use for the subscription. For the `subscribe` action,
+ * if not specified then the feed’s current title will be used. Can
+ * be used with the `edit` action to rename a subscription */
+ $titles = empty($_POST['t']) && isset($_GET['t']) ? array($_GET['t']) : multiplePosts('t');
+ $action = $_REQUEST['ac']; //Action to perform on the given StreamId. Possible values are `subscribe`, `unsubscribe` and `edit`
+ $add = isset($_REQUEST['a']) ? $_REQUEST['a'] : ''; //StreamId to add the subscription to (generally a user label)
+ $remove = isset($_REQUEST['r']) ? $_REQUEST['r'] : ''; //StreamId to remove the subscription from (generally a user label)
+ self::subscriptionEdit($streamNames, $titles, $action, $add, $remove);
+ }
+ break;
+ case 'quickadd': //https://github.com/theoldreader/api
+ if (isset($_REQUEST['quickadd'])) {
+ self::quickadd($_REQUEST['quickadd']);
+ }
+ break;
}
- break;
- }
- }
- break;
- case 'unread-count':
- $output = isset($_GET['output']) ? $_GET['output'] : '';
- if ($output !== 'json') notImplemented();
- unreadCount();
- break;
- case 'edit-tag': //http://blog.martindoms.com/2010/01/20/using-the-google-reader-api-part-3/
- $token = isset($_POST['T']) ? trim($_POST['T']) : '';
- checkToken(FreshRSS_Context::$user_conf, $token);
- $a = isset($_POST['a']) ? $_POST['a'] : ''; //Add: user/-/state/com.google/read user/-/state/com.google/starred
- $r = isset($_POST['r']) ? $_POST['r'] : ''; //Remove: user/-/state/com.google/read user/-/state/com.google/starred
- $e_ids = multiplePosts('i'); //item IDs
- editTag($e_ids, $a, $r);
- break;
- case 'rename-tag': //https://github.com/theoldreader/api
- $token = isset($_POST['T']) ? trim($_POST['T']) : '';
- checkToken(FreshRSS_Context::$user_conf, $token);
- $s = isset($_POST['s']) ? $_POST['s'] : ''; //user/-/label/Folder
- $dest = isset($_POST['dest']) ? $_POST['dest'] : ''; //user/-/label/NewFolder
- renameTag($s, $dest);
- break;
- case 'disable-tag': //https://github.com/theoldreader/api
- $token = isset($_POST['T']) ? trim($_POST['T']) : '';
- checkToken(FreshRSS_Context::$user_conf, $token);
- $s_s = multiplePosts('s');
- foreach ($s_s as $s) {
- disableTag($s); //user/-/label/Folder
- }
- break;
- case 'mark-all-as-read':
- $token = isset($_POST['T']) ? trim($_POST['T']) : '';
- checkToken(FreshRSS_Context::$user_conf, $token);
- $streamId = $_POST['s'] ?? '';
- $ts = isset($_POST['ts']) ? $_POST['ts'] : '0'; //Older than timestamp in nanoseconds
- if (!ctype_digit($ts)) {
- badRequest();
+ }
+ break;
+ case 'unread-count':
+ $output = isset($_GET['output']) ? $_GET['output'] : '';
+ if ($output !== 'json') self::notImplemented();
+ self::unreadCount();
+ // Always exits
+ case 'edit-tag': //http://blog.martindoms.com/2010/01/20/using-the-google-reader-api-part-3/
+ $token = isset($_POST['T']) ? trim($_POST['T']) : '';
+ self::checkToken(FreshRSS_Context::$user_conf, $token);
+ $a = isset($_POST['a']) ? $_POST['a'] : ''; //Add: user/-/state/com.google/read user/-/state/com.google/starred
+ $r = isset($_POST['r']) ? $_POST['r'] : ''; //Remove: user/-/state/com.google/read user/-/state/com.google/starred
+ $e_ids = multiplePosts('i'); //item IDs
+ self::editTag($e_ids, $a, $r);
+ // Always exits
+ case 'rename-tag': //https://github.com/theoldreader/api
+ $token = isset($_POST['T']) ? trim($_POST['T']) : '';
+ self::checkToken(FreshRSS_Context::$user_conf, $token);
+ $s = isset($_POST['s']) ? $_POST['s'] : ''; //user/-/label/Folder
+ $dest = isset($_POST['dest']) ? $_POST['dest'] : ''; //user/-/label/NewFolder
+ self::renameTag($s, $dest);
+ // Always exits
+ case 'disable-tag': //https://github.com/theoldreader/api
+ $token = isset($_POST['T']) ? trim($_POST['T']) : '';
+ self::checkToken(FreshRSS_Context::$user_conf, $token);
+ $s_s = multiplePosts('s');
+ foreach ($s_s as $s) {
+ self::disableTag($s); //user/-/label/Folder
+ }
+ // Always exits
+ case 'mark-all-as-read':
+ $token = isset($_POST['T']) ? trim($_POST['T']) : '';
+ self::checkToken(FreshRSS_Context::$user_conf, $token);
+ $streamId = trim($_POST['s'] ?? '');
+ $ts = trim($_POST['ts'] ?? '0'); //Older than timestamp in nanoseconds
+ if (!ctype_digit($ts)) {
+ self::badRequest();
+ }
+ self::markAllAsRead($streamId, $ts);
+ // Always exits
+ case 'token':
+ self::token(FreshRSS_Context::$user_conf);
+ // Always exits
+ case 'user-info':
+ self::userInfo();
+ // Always exits
}
- markAllAsRead($streamId, $ts);
- break;
- case 'token':
- token(FreshRSS_Context::$user_conf);
- break;
- case 'user-info':
- userInfo();
- break;
+ }
+
+ self::badRequest();
}
}
-badRequest();
+GReaderAPI::parse();
diff --git a/p/api/pshb.php b/p/api/pshb.php
index 26d1e125b..b3e3f400f 100644
--- a/p/api/pshb.php
+++ b/p/api/pshb.php
@@ -7,9 +7,13 @@ const MAX_PAYLOAD = 3145728;
header('Content-Type: text/plain; charset=UTF-8');
header('X-Content-Type-Options: nosniff');
-$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, MAX_PAYLOAD);
+$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, MAX_PAYLOAD) ?: '';
FreshRSS_Context::initSystem();
+if (FreshRSS_Context::$system_conf == null) {
+ header('HTTP/1.1 500 Internal Server Error');
+ die('Invalid system init!');
+}
FreshRSS_Context::$system_conf->auth_type = 'none'; // avoid necessity to be logged in (not saved!)
//Minz_Log::debug(print_r(array('_SERVER' => $_SERVER, '_GET' => $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT), true), PSHB_LOG);
@@ -41,7 +45,7 @@ if ($hubFile === false) {
die('Feed info not found!');
}
$hubJson = json_decode($hubFile, true);
-if (!$hubJson || empty($hubJson['key']) || $hubJson['key'] !== $key) {
+if (!is_array($hubJson) || empty($hubJson['key']) || $hubJson['key'] !== $key) {
header('HTTP/1.1 500 Internal Server Error');
Minz_Log::error('Error: Invalid key cross-check!: ' . $key, PSHB_LOG);
die('Invalid key cross-check!');
@@ -120,15 +124,12 @@ foreach ($users as $userFilename) {
try {
FreshRSS_Context::initUser($username);
- if (FreshRSS_Context::$user_conf != null) {
- Minz_ExtensionManager::enableByList(FreshRSS_Context::$user_conf->extensions_enabled);
- Minz_Translate::reset(FreshRSS_Context::$user_conf->language);
- }
-
- if (!FreshRSS_Context::$user_conf->enabled) {
+ if (FreshRSS_Context::$user_conf == null || !FreshRSS_Context::$user_conf->enabled) {
Minz_Log::warning('FreshRSS skip disabled user ' . $username);
continue;
}
+ Minz_ExtensionManager::enableByList(FreshRSS_Context::$user_conf->extensions_enabled);
+ Minz_Translate::reset(FreshRSS_Context::$user_conf->language);
list($updated_feeds, $feed, $nb_new_articles) = FreshRSS_feed_Controller::actualizeFeed(0, $self, false, $simplePie);
if ($updated_feeds > 0 || $feed != false) {
diff --git a/p/ext.php b/p/ext.php
index 2979e2365..abc07ad12 100644
--- a/p/ext.php
+++ b/p/ext.php
@@ -13,10 +13,7 @@ const SUPPORTED_TYPES = [
'svg' => 'image/svg+xml',
];
-/**
- * @return string
- */
-function get_absolute_filename(string $file_name) {
+function get_absolute_filename(string $file_name): string {
$core_extension = realpath(CORE_EXTENSIONS_PATH . '/' . $file_name);
if (false !== $core_extension) {
return $core_extension;
@@ -40,9 +37,12 @@ function get_absolute_filename(string $file_name) {
return '';
}
-function is_valid_path_extension($path, $extensionPath, $isStatic = true) {
+function is_valid_path_extension(string $path, string $extensionPath, bool $isStatic = true): bool {
// It must be under the extension path.
$real_ext_path = realpath($extensionPath);
+ if ($real_ext_path == false) {
+ return false;
+ }
//Windows compatibility
$real_ext_path = str_replace('\\', '/', $real_ext_path);
@@ -60,8 +60,7 @@ function is_valid_path_extension($path, $extensionPath, $isStatic = true) {
// Static files to serve must be under a `ext_dir/static/` directory.
$path_relative_to_ext = substr($path, strlen($real_ext_path) + 1);
- // @phpstan-ignore-next-line
- list(,$static,$file) = sscanf($path_relative_to_ext, '%[^/]/%[^/]/%s');
+ list(, $static, $file) = sscanf($path_relative_to_ext, '%[^/]/%[^/]/%s') ?? [null, null, null];
if (null === $file || 'static' !== $static) {
return false;
}
@@ -79,16 +78,18 @@ function is_valid_path_extension($path, $extensionPath, $isStatic = true) {
* @return bool true if it can be served, false otherwise.
*
*/
-function is_valid_path($path) {
+function is_valid_path(string $path): bool {
return is_valid_path_extension($path, CORE_EXTENSIONS_PATH) || is_valid_path_extension($path, THIRDPARTY_EXTENSIONS_PATH)
|| is_valid_path_extension($path, USERS_PATH, false);
}
+/** @return never */
function sendBadRequestResponse(string $message = null) {
header('HTTP/1.1 400 Bad Request');
die($message);
}
+/** @return never */
function sendNotFoundResponse() {
header('HTTP/1.1 404 Not Found');
die();
diff --git a/p/f.php b/p/f.php
index d856256aa..7837407e2 100644
--- a/p/f.php
+++ b/p/f.php
@@ -4,7 +4,7 @@ require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
require(LIB_PATH . '/favicons.php');
require(LIB_PATH . '/http-conditional.php');
-function show_default_favicon($cacheSeconds = 3600) {
+function show_default_favicon(int $cacheSeconds = 3600): void {
$default_mtime = @filemtime(DEFAULT_FAVICON);
if (!httpConditional($default_mtime, $cacheSeconds, 2)) {
header('Content-Type: image/x-icon');
diff --git a/p/i/index.php b/p/i/index.php
index 48cedfc92..360a858ca 100755..100644
--- a/p/i/index.php
+++ b/p/i/index.php
@@ -35,8 +35,8 @@ if (!file_exists($applied_migrations_path)) {
require(LIB_PATH . '/http-conditional.php');
$currentUser = Minz_Session::param('currentUser', '');
$dateLastModification = $currentUser === '' ? time() : max(
- @filemtime(join_path(USERS_PATH, $currentUser, LOG_FILENAME)),
- @filemtime(join_path(DATA_PATH, 'config.php'))
+ @filemtime(USERS_PATH . '/' . $currentUser . '/' . LOG_FILENAME),
+ @filemtime(DATA_PATH . '/config.php')
);
if (httpConditional($dateLastModification, 0, 0, false, PHP_COMPRESSION, true)) {
Minz_Session::init('FreshRSS');
diff --git a/p/index.html b/p/index.html
index 3f4284eb5..86cd81c84 100644
--- a/p/index.html
+++ b/p/index.html
@@ -14,6 +14,6 @@
<body>
<h1><a href="i/">FreshRSS</a></h1>
-<p><a href="i/"><img class="logo" width="25%" src="themes/icons/icon.svg" alt="⊚" /></a></p>
+<p><a href="i/"><img class="logo" width="25%" src="themes/icons/icon.svg" alt="⊚" loading="lazy" /></a></p>
</body>
</html>
diff --git a/p/scripts/extra.js b/p/scripts/extra.js
index 52a480c9a..707719430 100644
--- a/p/scripts/extra.js
+++ b/p/scripts/extra.js
@@ -145,6 +145,9 @@ function init_archiving(parent) {
const freshrssSliderLoadEvent = new Event('freshrss:slider-load');
function open_slider_listener(ev) {
+ if (ev.ctrlKey || ev.shiftKey) {
+ return;
+ }
const a = ev.target.closest('.open-slider');
if (a) {
if (!context.ajax_loading) {
@@ -175,21 +178,24 @@ function open_slider_listener(ev) {
function init_slider(slider) {
window.onclick = open_slider_listener;
- const closer = document.getElementById('close-slider');
- closer.addEventListener('click', function (ev) {
- if (data_leave_validation(slider) || confirm(context.i18n.confirmation_default)) {
- slider.querySelectorAll('form').forEach(function (f) { f.reset(); });
- document.documentElement.classList.remove('slider-active');
- return true;
- } else {
- return false;
- }
- });
+ document.getElementById('close-slider').addEventListener('click', close_slider_listener);
+ document.querySelector('#slider .toggle_aside').addEventListener('click', close_slider_listener);
if (slider.children.length > 0) {
slider.dispatchEvent(freshrssSliderLoadEvent);
}
}
+
+function close_slider_listener(ev) {
+ const slider = document.getElementById('slider');
+ if (data_leave_validation(slider) || confirm(context.i18n.confirmation_default)) {
+ slider.querySelectorAll('form').forEach(function (f) { f.reset(); });
+ document.documentElement.classList.remove('slider-active');
+ return true;
+ } else {
+ return false;
+ }
+}
// </slider>
// overwrites the href attribute from the url input
diff --git a/p/scripts/feed.js b/p/scripts/feed.js
index 1a6833db6..29af2a3ea 100644
--- a/p/scripts/feed.js
+++ b/p/scripts/feed.js
@@ -88,10 +88,17 @@ function init_disable_elements_on_update(parent) {
function init_select_show(parent) {
const listener = (select) => {
const options = select.querySelectorAll('option[data-show]');
+ const shows = {}; // To allow multiple options to show the same element
for (const option of options) {
- const elem = document.getElementById(option.dataset.show);
+ if (!shows[option.dataset.show]) {
+ shows[option.dataset.show] = option.selected;
+ }
+ }
+
+ for (const show in shows) {
+ const elem = document.getElementById(show);
if (elem) {
- elem.style.display = option.selected ? 'block' : 'none';
+ elem.style.display = shows[show] ? 'block' : 'none';
}
}
};
diff --git a/p/scripts/main.js b/p/scripts/main.js
index 268fd2858..bca6c2407 100644
--- a/p/scripts/main.js
+++ b/p/scripts/main.js
@@ -264,6 +264,7 @@ function send_mark_read_queue(queue, asRead, callback) {
incUnreadsTag(tagId, (asRead ? -1 : 1) * json.tags[tagId].length);
}
}
+ toggle_bigMarkAsRead_button();
onScroll();
if (callback) {
callback();
@@ -1012,11 +1013,13 @@ function init_shortcuts() {
}
const link_go_website = document.querySelector('.flux.current a.go_website');
- const newWindow = window.open();
- if (link_go_website && newWindow) {
- newWindow.opener = null;
- newWindow.location = link_go_website.href;
- ev.preventDefault();
+ if (link_go_website) {
+ const newWindow = window.open();
+ if (newWindow) {
+ newWindow.opener = null;
+ newWindow.location = link_go_website.href;
+ ev.preventDefault();
+ }
}
return;
}
@@ -1101,6 +1104,12 @@ function init_stream(stream) {
return false;
}
+ el = ev.target.closest('.item.share > a[data-type="email-webmail-firefox-fix"]');
+ if (el) {
+ window.open(el.href);
+ return false;
+ }
+
el = ev.target.closest('.item.share > a[href="POST"]');
if (el) { // Share by POST
const f = el.parentElement.querySelector('form');
@@ -1562,7 +1571,7 @@ function notifs_html5_show(nb, nb_new) {
const notification = new window.Notification(context.i18n.notif_title_articles, {
icon: '../themes/icons/favicon-256-padding.png',
- body: context.i18n.notif_body_new_articles.replace('%d', nb_new) + ' ' + context.i18n.notif_body_unread_articles.replace('%d', nb),
+ body: context.i18n.notif_body_new_articles.replace('%%d', nb_new) + ' ' + context.i18n.notif_body_unread_articles.replace('%%d', nb),
tag: 'freshRssNewArticles',
});
@@ -1655,6 +1664,23 @@ function refreshUnreads() {
req.send();
}
+function toggle_bigMarkAsRead_button() {
+ const bigMarkAsRead_button = document.getElementById('bigMarkAsRead');
+ if (bigMarkAsRead_button) {
+ if (document.querySelector('.flux.not_read') != null) {
+ bigMarkAsRead_button.style = '';
+ bigMarkAsRead_button.querySelector('.markAllRead').style.visibility = '';
+ } else {
+ if (bigMarkAsRead_button.querySelector('.jumpNext')) {
+ bigMarkAsRead_button.querySelector('.markAllRead').style.visibility = 'hidden';
+ } else {
+ bigMarkAsRead_button.querySelector('.markAllRead').style.visibility = '';
+ bigMarkAsRead_button.style.visibility = 'hidden';
+ }
+ }
+ }
+}
+
// <endless_mode>
let url_load_more = '';
let load_more = false;
@@ -1691,6 +1717,7 @@ function load_more_posts() {
} else {
bigMarkAsRead.formAction = readAll.formAction;
}
+ toggle_bigMarkAsRead_button();
}
document.querySelectorAll('[id^=day_]').forEach(function (div) {
@@ -1843,6 +1870,7 @@ function init_main_afterDOM() {
init_posts();
init_nav_entries();
init_notifs_html5();
+ toggle_bigMarkAsRead_button();
setTimeout(faviconNbUnread, 1000);
setInterval(refreshUnreads, 120000);
}
diff --git a/p/themes/.htaccess b/p/themes/.htaccess
index 796c55ae0..0e78aab3a 100644
--- a/p/themes/.htaccess
+++ b/p/themes/.htaccess
@@ -1,6 +1,6 @@
<IfModule mod_mime.c>
- AddType application/font-woff .woff
- AddType application/font-woff2 .woff2
+ AddType font/woff .woff
+ AddType font/woff2 .woff2
AddCharset UTF-8 .css
AddCharset UTF-8 .svg
@@ -8,8 +8,9 @@
<IfModule mod_expires.c>
ExpiresActive on
- ExpiresByType application/font-woff "access plus 1 month"
ExpiresByType application/json "access plus 1 month"
+ ExpiresByType font/woff "access plus 1 month"
+ ExpiresByType font/woff2 "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
diff --git a/p/themes/Alternative-Dark/adark.css b/p/themes/Alternative-Dark/adark.css
index 3b5a9d337..0f600fa2e 100644
--- a/p/themes/Alternative-Dark/adark.css
+++ b/p/themes/Alternative-Dark/adark.css
@@ -114,17 +114,12 @@ input:disabled, select:disabled {
border-color: var(--border-color-dark);
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid var(--border-color-dark);
}
@@ -153,7 +148,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: right;
}
.form-group .group-controls {
@@ -282,13 +276,17 @@ a.btn {
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- line-height: 2.5;
+.nav-list {
color: var(--font-color-light);
font-size: 0.9rem;
}
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item a {
color: var(--font-color-middle);
}
@@ -304,33 +302,15 @@ a.btn {
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
}
.nav-list a:hover {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: var(--empty-feed-color);
-}
-
-.nav-list .item.active.empty a {
- background: var(--empty-feed-color);
- color: var(--font-color-light);
-}
-
-.nav-list .item.error a {
- color: var(--font-color-error);
-}
-
-.nav-list .item.active.error a {
- background: var(--contrast-attention-background-color);
- color: var(--font-color-light);
-}
-
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
color: var(--font-color-middle);
font-weight: bold;
}
@@ -343,7 +323,7 @@ a.btn {
/*=== Dropdown */
.dropdown-menu {
margin: 5px 0 0;
- padding: 5px 0;
+ padding: 0.5rem 0 0.25rem 0;
background: var(--background-color-active);
font-size: 0.8rem;
border: 2px solid var(--background-color-light);
@@ -355,23 +335,35 @@ a.btn {
border-color: var(--border-color-dark);
}
-.dropdown-header {
- padding: 0 5px 5px;
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
+ padding: 0.25rem 0.5rem 0.25rem 1rem;
color: var(--font-color-middle);
font-weight: bold;
text-align: left;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5;
- font-size: 0.8rem;
+ font-size: inherit;
+}
+
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-left: 2rem;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background: var(--background-color-hover);
color: var(--font-color-light);
}
@@ -389,14 +381,13 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: var(--border-color-dark);
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background-color: var(--background-color-dark);
color: var(--font-color-middle);
font-size: 0.9em;
@@ -524,8 +515,6 @@ kbd {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 3.1;
font-size: 0.8rem;
}
@@ -549,26 +538,20 @@ kbd {
}
.header > .item {
- padding: 10px;
border-bottom: 1px solid var(--border-color-dark);
vertical-align: middle;
text-align: center;
}
-.header > .item.title {
- width: 230px;
-}
-
.header > .item.title .logo {
- margin: 0.5em 0;
filter: grayscale(100%) brightness(2.5);
}
-.header > .item.search input {
- width: 230px;
+.header > .item.title a:hover .logo {
+ filter: grayscale(100%) brightness(2.8);
}
-.header .item.search input:focus {
+.header > .item.search input {
width: 350px;
}
@@ -595,7 +578,8 @@ kbd {
.aside.aside_feed .category .title:not([data-unread="0"])::after,
.aside.aside_feed .feed .item-title:not([data-unread="0"])::after,
-.global .box.category .title:not([data-unread="0"])::after {
+.global .box.category .title:not([data-unread="0"])::after,
+.global .feed .item-title:not([data-unread="0"])::after {
background-color: var(--background-color-dark);
color: var(--font-color-middle);
}
@@ -742,7 +726,7 @@ kbd {
border-left-style: solid;
}
-.flux:hover {
+.flux .flux_header:hover {
background: var(--background-color-hover) !important;
}
@@ -915,15 +899,6 @@ kbd {
color: var(--font-color-contrast);
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3;
-}
-
-.notification#actualizeProgress {
- line-height: 2;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
background: var(--background-color-dark);
@@ -1013,6 +988,10 @@ kbd {
text-shadow: none;
}
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ margin: 1rem 0px 0px;
+}
+
/*=== Slider */
#slider {
background: var(--background-color-dark);
@@ -1071,7 +1050,6 @@ kbd {
@media (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: left;
}
.aside {
@@ -1127,21 +1105,6 @@ kbd {
margin: 5px 0;
}
- .nav_menu .search {
- display: inline-block;
- max-width: 97%;
- }
-
- .nav_menu .search input {
- padding: 3px 5px;
- max-width: 97%;
- width: 90px;
- }
-
- .nav_menu .search input:focus {
- width: 400px;
- }
-
.dropdown-target:target ~ .dropdown-toggle::after {
background-color: var(--background-color-active);
border-top: 2px solid var(--background-color-light);
diff --git a/p/themes/Alternative-Dark/adark.rtl.css b/p/themes/Alternative-Dark/adark.rtl.css
index 64ddd4403..a42fa2a71 100644
--- a/p/themes/Alternative-Dark/adark.rtl.css
+++ b/p/themes/Alternative-Dark/adark.rtl.css
@@ -114,17 +114,12 @@ input:disabled, select:disabled {
border-color: var(--border-color-dark);
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid var(--border-color-dark);
}
@@ -153,7 +148,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: left;
}
.form-group .group-controls {
@@ -282,13 +276,17 @@ a.btn {
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- line-height: 2.5;
+.nav-list {
color: var(--font-color-light);
font-size: 0.9rem;
}
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item a {
color: var(--font-color-middle);
}
@@ -304,33 +302,15 @@ a.btn {
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
}
.nav-list a:hover {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: var(--empty-feed-color);
-}
-
-.nav-list .item.active.empty a {
- background: var(--empty-feed-color);
- color: var(--font-color-light);
-}
-
-.nav-list .item.error a {
- color: var(--font-color-error);
-}
-
-.nav-list .item.active.error a {
- background: var(--contrast-attention-background-color);
- color: var(--font-color-light);
-}
-
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
color: var(--font-color-middle);
font-weight: bold;
}
@@ -343,7 +323,7 @@ a.btn {
/*=== Dropdown */
.dropdown-menu {
margin: 5px 0 0;
- padding: 5px 0;
+ padding: 0.5rem 0 0.25rem 0;
background: var(--background-color-active);
font-size: 0.8rem;
border: 2px solid var(--background-color-light);
@@ -355,23 +335,35 @@ a.btn {
border-color: var(--border-color-dark);
}
-.dropdown-header {
- padding: 0 5px 5px;
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
+ padding: 0.25rem 1rem 0.25rem 0.5rem;
color: var(--font-color-middle);
font-weight: bold;
text-align: right;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5;
- font-size: 0.8rem;
+ font-size: inherit;
+}
+
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-right: 2rem;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background: var(--background-color-hover);
color: var(--font-color-light);
}
@@ -389,14 +381,13 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: var(--border-color-dark);
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background-color: var(--background-color-dark);
color: var(--font-color-middle);
font-size: 0.9em;
@@ -524,8 +515,6 @@ kbd {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 3.1;
font-size: 0.8rem;
}
@@ -549,26 +538,20 @@ kbd {
}
.header > .item {
- padding: 10px;
border-bottom: 1px solid var(--border-color-dark);
vertical-align: middle;
text-align: center;
}
-.header > .item.title {
- width: 230px;
-}
-
.header > .item.title .logo {
- margin: 0.5em 0;
filter: grayscale(100%) brightness(2.5);
}
-.header > .item.search input {
- width: 230px;
+.header > .item.title a:hover .logo {
+ filter: grayscale(100%) brightness(2.8);
}
-.header .item.search input:focus {
+.header > .item.search input {
width: 350px;
}
@@ -595,7 +578,8 @@ kbd {
.aside.aside_feed .category .title:not([data-unread="0"])::after,
.aside.aside_feed .feed .item-title:not([data-unread="0"])::after,
-.global .box.category .title:not([data-unread="0"])::after {
+.global .box.category .title:not([data-unread="0"])::after,
+.global .feed .item-title:not([data-unread="0"])::after {
background-color: var(--background-color-dark);
color: var(--font-color-middle);
}
@@ -742,7 +726,7 @@ kbd {
border-right-style: solid;
}
-.flux:hover {
+.flux .flux_header:hover {
background: var(--background-color-hover) !important;
}
@@ -915,15 +899,6 @@ kbd {
color: var(--font-color-contrast);
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3;
-}
-
-.notification#actualizeProgress {
- line-height: 2;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
background: var(--background-color-dark);
@@ -1013,6 +988,10 @@ kbd {
text-shadow: none;
}
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ margin: 1rem 0px 0px;
+}
+
/*=== Slider */
#slider {
background: var(--background-color-dark);
@@ -1071,7 +1050,6 @@ kbd {
@media (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: right;
}
.aside {
@@ -1127,21 +1105,6 @@ kbd {
margin: 5px 0;
}
- .nav_menu .search {
- display: inline-block;
- max-width: 97%;
- }
-
- .nav_menu .search input {
- padding: 3px 5px;
- max-width: 97%;
- width: 90px;
- }
-
- .nav_menu .search input:focus {
- width: 400px;
- }
-
.dropdown-target:target ~ .dropdown-toggle::after {
background-color: var(--background-color-active);
border-top: 2px solid var(--background-color-light);
diff --git a/p/themes/Ansum/_components.scss b/p/themes/Ansum/_components.scss
index 35298b4de..88ab37296 100644
--- a/p/themes/Ansum/_components.scss
+++ b/p/themes/Ansum/_components.scss
@@ -22,6 +22,12 @@
}
/*=== Dropdown */
+.dropdown {
+ .dropdown-target:target + .btn {
+ background-color: variables.$grey-medium-light;
+ }
+}
+
.dropdown-menu {
margin: 9px 0 0 0;
padding: 0.5rem 0 1rem 0;
@@ -37,44 +43,61 @@
right: 17px;
}
- .dropdown-header {
- padding: 1rem 0.5rem 1rem 1rem;
+ .dropdown-header,
+ .dropdown-section .dropdown-section-title {
+ padding: 1rem 1.5rem;
font-weight: bold;
text-align: left;
color: variables.$grey-dark;
text-transform: uppercase;
letter-spacing: 1px;
-
-
}
.item {
@include mixins.transition(all, 0.075s, ease-in-out);
- a, span, .as-link {
+ > a,
+ > span,
+ > .as-link {
padding: 0 2rem;
color: variables.$main-font-color;
- font-size: 1rem;
+ font-size: inherit;
line-height: 2.5em;
- }
- &:not(.addItem):hover {
- background: variables.$main-first;
- color: variables.$white;
+ span.icon {
+ padding: 0 0.25rem !important;
+ }
+ }
- a, button, label {
- text-decoration: none;
+ > a,
+ > .as-link {
+ &:not(.addItem):hover {
+ background: variables.$main-first;
color: variables.$white;
+
+ .icon {
+ filter: grayscale(100%) brightness(2.5);
+ }
}
+ }
- .icon {
- filter: grayscale(100%) brightness(2.5);
+ &.dropdown-section {
+ margin-top: 0.75rem;
+
+ ~ .dropdown-section {
+ border-top-color: variables.$grey-light;
+ }
+
+ .item {
+ a, .as-link {
+ padding-left: 2rem;
+ }
}
}
&:not(.addItem) {
- a:hover,
+ > a:hover,
button:hover {
background: variables.$main-first;
color: variables.$white;
@@ -139,9 +162,6 @@
/*=== Alerts */
.alert {
- margin: 1rem 0;
- // width: 100%;
- padding: 1rem;
background: variables.$grey-lighter;
color: variables.$grey-dark;
font-size: 1rem;
diff --git a/p/themes/Ansum/_forms.scss b/p/themes/Ansum/_forms.scss
index 77c9e3552..3bdb30664 100644
--- a/p/themes/Ansum/_forms.scss
+++ b/p/themes/Ansum/_forms.scss
@@ -120,24 +120,17 @@ input:disabled, select:disabled {
border-color: variables.$grey-medium-dark;
}
-input.extend {
- transition: width 200ms linear;
-}
-
-
.form-group {
padding: 5px;
border-radius: 3px;
&::after {
- content: "";
display: block;
clear: both;
}
.group-name {
padding: 10px 0;
- text-align: right;
}
.group-controls {
diff --git a/p/themes/Ansum/_layout.scss b/p/themes/Ansum/_layout.scss
index 4d099ab4a..78431f462 100644
--- a/p/themes/Ansum/_layout.scss
+++ b/p/themes/Ansum/_layout.scss
@@ -6,31 +6,30 @@
/*===============*/
/*=== Header */
.header {
- padding: 0.5rem 1.35rem;
background: variables.$sid-bg;
- display: block;
- width: auto;
- height: 3.5rem;
- table-layout: none;
.item {
vertical-align: middle;
&.title {
- width: 280px;
- font-weight: 400;
-
a {
- img {
- margin: 0.6em 0 0.3em;
+ padding: 0.5rem 1rem;
+
+ .logo {
filter: invert(80%);
}
+
+ &:hover {
+ .logo {
+ filter: invert(80%) opacity(80%);
+ }
+ }
}
}
&.search {
input {
- width: 230px;
+ width: 350px;
color: variables.$sid-font-color;
border: none;
border-radius: 2px 0 0 2px;
@@ -43,9 +42,7 @@
}
&:focus {
- width: 350px;
color: variables.$grey-dark;
-
background-color: variables.$white;
}
}
@@ -81,10 +78,6 @@
}
&.configure {
- width: 3rem;
- position: absolute;
- right: 1rem;
- top: 1rem;
text-align: center;
.btn {
@@ -97,7 +90,7 @@
/*=== Body */
#global {
- height: calc(100% - 3.5rem);
+ height: calc(100vh - (calc(3rem + 2 * var(--frss-padding-top-bottom))));
}
/*=== Prompt (centered) */
@@ -214,7 +207,7 @@ main.prompt {
}
}
- .dropdown {
+ .dropdown:not(#dropdown-search-wrapper) {
a.dropdown-toggle {
border-left-width: 0;
background-image: url(icons/more.svg);
@@ -224,6 +217,12 @@ main.prompt {
}
}
}
+
+ #dropdown-search-wrapper.dropdown {
+ a.dropdown-toggle {
+ border-left-width: 0;
+ }
+ }
}
}
@@ -348,9 +347,7 @@ main.prompt {
}
a.close {
- padding: 0 15px;
border-radius: 0 3px 3px 0;
- line-height: 3em;
}
&.good a.close:hover {
@@ -362,11 +359,13 @@ main.prompt {
}
&#actualizeProgress {
- line-height: 2em;
-
br {
display: none;
}
+
+ .title {
+ margin: 0 2rem;
+ }
}
}
diff --git a/p/themes/Ansum/_list-view.scss b/p/themes/Ansum/_list-view.scss
index c85c6f8fb..1ffd68dc7 100644
--- a/p/themes/Ansum/_list-view.scss
+++ b/p/themes/Ansum/_list-view.scss
@@ -11,11 +11,13 @@
@include mixins.transition(all, 0.15s, ease-in-out);
- &:hover {
- background: variables.$grey-lighter;
-
- &:not(.current):hover .item.title {
+ .flux_header {
+ &:hover {
background: variables.$grey-lighter;
+
+ &:not(.current):hover .item.title {
+ background: variables.$grey-lighter;
+ }
}
}
diff --git a/p/themes/Ansum/_mobile.scss b/p/themes/Ansum/_mobile.scss
index 9e90010c6..6fbca4a52 100644
--- a/p/themes/Ansum/_mobile.scss
+++ b/p/themes/Ansum/_mobile.scss
@@ -6,10 +6,6 @@
/*===========*/
@media (max-width: 840px) {
- .form-group .group-name {
- text-align: left;
- }
-
.aside {
@include mixins.transition(all, 0.2s, ease-in-out);
@@ -39,35 +35,15 @@
}
.header {
- padding: 0.5rem;
- height: 8rem;
-
.item {
&.search {
- display: block;
-
- form {
- display: inherit;
- }
-
- .stick {
- display: flex;
- }
-
- input {
- width: 90%;
- height: 3.5rem;
-
- &:focus {
- width: 100%;
-
- }
- }
+ display: none;
+ }
- .btn {
- min-height: 49px;
- padding: 0.5rem 2rem;
- }
+ &.configure {
+ position: absolute;
+ top: 0;
+ right: 0;
}
}
}
@@ -101,11 +77,7 @@
}
.search {
- display: none;
- max-width: 97%;
-
.input {
-
max-width: 97%;
width: 90px;
diff --git a/p/themes/Ansum/_sidebar.scss b/p/themes/Ansum/_sidebar.scss
index 7aaa6b0a8..7c21d159d 100644
--- a/p/themes/Ansum/_sidebar.scss
+++ b/p/themes/Ansum/_sidebar.scss
@@ -93,7 +93,7 @@
border-radius: 5px 0 0 5px;
}
- .btn:last-child, input:last-child, .btn + .dropdown > .btn {
+ .btn:last-child, input:last-child, .dropdown:last-child > .btn {
border-radius: 0 5px 5px 0;
}
@@ -135,15 +135,23 @@
/*=== Navigation */
.nav-list {
- .nav-header,
+ font-size: 1rem;
+
+ .item.nav-header,
.item {
- height: 2.5em;
+ min-height: 2.5em;
line-height: 2.5em;
- font-size: 1rem;
}
.item {
background: variables.$sid-bg;
+ min-height: 2.5em;
+ line-height: 2.5em;
+
+ &.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5em;
+ }
a {
padding: 0 1rem;
@@ -152,51 +160,15 @@
@include mixins.transition(all, 0.15s, ease-in-out);
}
- .error {
- a {
- color: variables.$alert-bg;
- }
- }
-
- &:hover {
- .error {
- a {
- background: variables.$main-first;
- color: variables.$sid-font-color;
- }
- }
-
- .empty {
- a {
- background: variables.$warning-bg;
- color: variables.$sid-font-color;
- }
- }
-
- a {
- background: variables.$sid-bg-dark;
- text-decoration: none;
- }
+ a:hover {
+ background: variables.$sid-bg-dark;
+ text-decoration: none;
}
&.active {
background: variables.$main-first;
color: variables.$white;
- .error {
- a {
- background: variables.$main-first;
- color: variables.$white;
- }
- }
-
- .empty {
- a {
- background: variables.$warning-bg;
- color: variables.$white;
- }
- }
-
a {
background: variables.$main-first;
color: variables.$white;
@@ -205,14 +177,8 @@
}
}
- &.empty {
- a {
- color: variables.$warning-bg;
- }
- }
-
.nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
color: variables.$grey-dark;
text-transform: uppercase;
diff --git a/p/themes/Ansum/_tables.scss b/p/themes/Ansum/_tables.scss
index 4955fff6a..7376279e0 100644
--- a/p/themes/Ansum/_tables.scss
+++ b/p/themes/Ansum/_tables.scss
@@ -5,8 +5,7 @@ table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid variables.$grey-medium-light;
}
diff --git a/p/themes/Ansum/ansum.css b/p/themes/Ansum/ansum.css
index 21d718949..709861e18 100644
--- a/p/themes/Ansum/ansum.css
+++ b/p/themes/Ansum/ansum.css
@@ -164,22 +164,16 @@ input:disabled, select:disabled {
border-color: #ba9;
}
-input.extend {
- transition: width 200ms linear;
-}
-
.form-group {
padding: 5px;
border-radius: 3px;
}
.form-group::after {
- content: "";
display: block;
clear: both;
}
.form-group .group-name {
padding: 10px 0;
- text-align: right;
}
.form-group .group-controls {
min-height: 25px;
@@ -201,8 +195,7 @@ table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #e4d8cc;
}
@@ -228,6 +221,10 @@ form th {
}
/*=== Dropdown */
+.dropdown .dropdown-target:target + .btn {
+ background-color: #e4d8cc;
+}
+
.dropdown-menu {
margin: 9px 0 0 0;
padding: 0.5rem 0 1rem 0;
@@ -242,8 +239,9 @@ form th {
border: none;
right: 17px;
}
-.dropdown-menu .dropdown-header {
- padding: 1rem 0.5rem 1rem 1rem;
+.dropdown-menu .dropdown-header,
+.dropdown-menu .dropdown-section .dropdown-section-title {
+ padding: 1rem 1.5rem;
font-weight: bold;
text-align: left;
color: #766556;
@@ -253,29 +251,43 @@ form th {
.dropdown-menu .item {
transition: all 0.075s ease-in-out;
}
-.dropdown-menu .item a, .dropdown-menu .item span, .dropdown-menu .item .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 2rem;
color: #363330;
- font-size: 1rem;
+ font-size: inherit;
line-height: 2.5em;
}
-.dropdown-menu .item:not(.addItem):hover {
- background: #ca7227;
- color: #fff;
+.dropdown-menu .item > a span.icon,
+.dropdown-menu .item > span span.icon,
+.dropdown-menu .item > .as-link span.icon {
+ padding: 0 0.25rem !important;
}
-.dropdown-menu .item:not(.addItem):hover a, .dropdown-menu .item:not(.addItem):hover button, .dropdown-menu .item:not(.addItem):hover label {
- text-decoration: none;
+.dropdown-menu .item > a:not(.addItem):hover,
+.dropdown-menu .item > .as-link:not(.addItem):hover {
+ background: #ca7227;
color: #fff;
}
-.dropdown-menu .item:not(.addItem):hover .icon {
+.dropdown-menu .item > a:not(.addItem):hover .icon,
+.dropdown-menu .item > .as-link:not(.addItem):hover .icon {
filter: grayscale(100%) brightness(2.5);
}
-.dropdown-menu .item:not(.addItem) a:hover,
+.dropdown-menu .item.dropdown-section {
+ margin-top: 0.75rem;
+}
+.dropdown-menu .item.dropdown-section ~ .dropdown-section {
+ border-top-color: #f5f0ec;
+}
+.dropdown-menu .item.dropdown-section .item a, .dropdown-menu .item.dropdown-section .item .as-link {
+ padding-left: 2rem;
+}
+.dropdown-menu .item:not(.addItem) > a:hover,
.dropdown-menu .item:not(.addItem) button:hover {
background: #ca7227;
color: #fff;
}
-.dropdown-menu .item:not(.addItem) a:hover .icon,
+.dropdown-menu .item:not(.addItem) > a:hover .icon,
.dropdown-menu .item:not(.addItem) button:hover .icon {
filter: brightness(3);
}
@@ -315,8 +327,6 @@ form th {
/*=== Alerts */
.alert {
- margin: 1rem 0;
- padding: 1rem;
background: #fcfaf8;
color: #766556;
font-size: 1rem;
@@ -518,7 +528,7 @@ form th {
.stick .btn:first-child {
border-radius: 5px 0 0 5px;
}
-.stick .btn:last-child, .stick input:last-child, .stick .btn + .dropdown > .btn {
+.stick .btn:last-child, .stick input:last-child, .stick .dropdown:last-child > .btn {
border-radius: 0 5px 5px 0;
}
.stick .btn + .btn,
@@ -548,32 +558,29 @@ form th {
/* Sidebar des pages de configuration */
/*=== Navigation */
-.nav-list .nav-header,
+.nav-list {
+ font-size: 1rem;
+}
+.nav-list .item.nav-header,
.nav-list .item {
- height: 2.5em;
+ min-height: 2.5em;
line-height: 2.5em;
- font-size: 1rem;
}
.nav-list .item {
background: #fbf9f6;
+ min-height: 2.5em;
+ line-height: 2.5em;
+}
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5em;
}
.nav-list .item a {
padding: 0 1rem;
color: #363330;
transition: all 0.15s ease-in-out;
}
-.nav-list .item .error a {
- color: #f5633e;
-}
-.nav-list .item:hover .error a {
- background: #ca7227;
- color: #363330;
-}
-.nav-list .item:hover .empty a {
- background: #f4f762;
- color: #363330;
-}
-.nav-list .item:hover a {
+.nav-list .item a:hover {
background: #efe3d3;
text-decoration: none;
}
@@ -581,24 +588,13 @@ form th {
background: #ca7227;
color: #fff;
}
-.nav-list .item.active .error a {
- background: #ca7227;
- color: #fff;
-}
-.nav-list .item.active .empty a {
- background: #f4f762;
- color: #fff;
-}
.nav-list .item.active a {
background: #ca7227;
color: #fff;
text-decoration: none;
}
-.nav-list.empty a {
- color: #f4f762;
-}
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
color: #766556;
text-transform: uppercase;
@@ -668,26 +664,22 @@ form th {
/*===============*/
/*=== Header */
.header {
- padding: 0.5rem 1.35rem;
background: #fbf9f6;
- display: block;
- width: auto;
- height: 3.5rem;
- table-layout: none;
}
.header .item {
vertical-align: middle;
}
-.header .item.title {
- width: 280px;
- font-weight: 400;
+.header .item.title a {
+ padding: 0.5rem 1rem;
}
-.header .item.title a img {
- margin: 0.6em 0 0.3em;
+.header .item.title a .logo {
filter: invert(80%);
}
+.header .item.title a:hover .logo {
+ filter: invert(80%) opacity(80%);
+}
.header .item.search input {
- width: 230px;
+ width: 350px;
color: #363330;
border: none;
border-radius: 2px 0 0 2px;
@@ -698,7 +690,6 @@ form th {
background-color: #efe3d3;
}
.header .item.search input:focus {
- width: 350px;
color: #766556;
background-color: #fff;
}
@@ -727,10 +718,6 @@ form th {
filter: brightness(3);
}
.header .item.configure {
- width: 3rem;
- position: absolute;
- right: 1rem;
- top: 1rem;
text-align: center;
}
.header .item.configure .btn {
@@ -740,7 +727,7 @@ form th {
/*=== Body */
#global {
- height: calc(100% - 3.5rem);
+ height: calc(100vh - (3rem + 2 * var(--frss-padding-top-bottom)));
}
/*=== Prompt (centered) */
@@ -842,13 +829,16 @@ main.prompt {
.nav_menu .stick .btn.read_all:hover {
background-color: #e4d8cc;
}
-.nav_menu .stick .dropdown a.dropdown-toggle {
+.nav_menu .stick .dropdown:not(#dropdown-search-wrapper) a.dropdown-toggle {
border-left-width: 0;
background-image: url(icons/more.svg);
}
-.nav_menu .stick .dropdown a.dropdown-toggle .icon {
+.nav_menu .stick .dropdown:not(#dropdown-search-wrapper) a.dropdown-toggle .icon {
display: none;
}
+.nav_menu .stick #dropdown-search-wrapper.dropdown a.dropdown-toggle {
+ border-left-width: 0;
+}
#dropdown-query ~ .dropdown-menu .dropdown-header .icon {
vertical-align: middle;
@@ -951,9 +941,7 @@ main.prompt {
color: #fff;
}
.notification a.close {
- padding: 0 15px;
border-radius: 0 3px 3px 0;
- line-height: 3em;
}
.notification.good a.close:hover {
background: #0c7556;
@@ -961,12 +949,12 @@ main.prompt {
.notification.bad a.close:hover {
background: #73341f;
}
-.notification#actualizeProgress {
- line-height: 2em;
-}
.notification#actualizeProgress br {
display: none;
}
+.notification#actualizeProgress .title {
+ margin: 0 2rem;
+}
/*=== Navigation menu (for articles) */
#nav_entries {
@@ -982,10 +970,10 @@ main.prompt {
background: #fff;
transition: all 0.15s ease-in-out;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #fcfaf8;
}
-.flux:hover:not(.current):hover .item.title {
+.flux .flux_header:hover:not(.current):hover .item.title {
background: #fcfaf8;
}
.flux.current {
@@ -1194,9 +1182,6 @@ main.prompt {
/*=== MOBILE */
/*===========*/
@media (max-width: 840px) {
- .form-group .group-name {
- text-align: left;
- }
.aside {
transition: all 0.2s ease-in-out;
}
@@ -1224,29 +1209,13 @@ main.prompt {
#slider .toggle_aside .icon {
filter: grayscale(100%) brightness(2.5);
}
- .header {
- padding: 0.5rem;
- height: 8rem;
- }
.header .item.search {
- display: block;
- }
- .header .item.search form {
- display: inherit;
- }
- .header .item.search .stick {
- display: flex;
- }
- .header .item.search input {
- width: 90%;
- height: 3.5rem;
- }
- .header .item.search input:focus {
- width: 100%;
+ display: none;
}
- .header .item.search .btn {
- min-height: 49px;
- padding: 0.5rem 2rem;
+ .header .item.configure {
+ position: absolute;
+ top: 0;
+ right: 0;
}
#global {
height: calc(100% - 8rem);
@@ -1269,10 +1238,6 @@ main.prompt {
.nav_menu .stick .btn.read_all {
padding: 0.85rem 1.25rem;
}
- .nav_menu .search {
- display: none;
- max-width: 97%;
- }
.nav_menu .search .input {
max-width: 97%;
width: 90px;
diff --git a/p/themes/Ansum/ansum.rtl.css b/p/themes/Ansum/ansum.rtl.css
index a4d0e32b5..ad480c952 100644
--- a/p/themes/Ansum/ansum.rtl.css
+++ b/p/themes/Ansum/ansum.rtl.css
@@ -164,22 +164,16 @@ input:disabled, select:disabled {
border-color: #ba9;
}
-input.extend {
- transition: width 200ms linear;
-}
-
.form-group {
padding: 5px;
border-radius: 3px;
}
.form-group::after {
- content: "";
display: block;
clear: both;
}
.form-group .group-name {
padding: 10px 0;
- text-align: left;
}
.form-group .group-controls {
min-height: 25px;
@@ -201,8 +195,7 @@ table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #e4d8cc;
}
@@ -228,6 +221,10 @@ form th {
}
/*=== Dropdown */
+.dropdown .dropdown-target:target + .btn {
+ background-color: #e4d8cc;
+}
+
.dropdown-menu {
margin: 9px 0 0 0;
padding: 0.5rem 0 1rem 0;
@@ -242,8 +239,9 @@ form th {
border: none;
left: 17px;
}
-.dropdown-menu .dropdown-header {
- padding: 1rem 1rem 1rem 0.5rem;
+.dropdown-menu .dropdown-header,
+.dropdown-menu .dropdown-section .dropdown-section-title {
+ padding: 1rem 1.5rem;
font-weight: bold;
text-align: right;
color: #766556;
@@ -253,29 +251,43 @@ form th {
.dropdown-menu .item {
transition: all 0.075s ease-in-out;
}
-.dropdown-menu .item a, .dropdown-menu .item span, .dropdown-menu .item .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 2rem;
color: #363330;
- font-size: 1rem;
+ font-size: inherit;
line-height: 2.5em;
}
-.dropdown-menu .item:not(.addItem):hover {
- background: #ca7227;
- color: #fff;
+.dropdown-menu .item > a span.icon,
+.dropdown-menu .item > span span.icon,
+.dropdown-menu .item > .as-link span.icon {
+ padding: 0 0.25rem !important;
}
-.dropdown-menu .item:not(.addItem):hover a, .dropdown-menu .item:not(.addItem):hover button, .dropdown-menu .item:not(.addItem):hover label {
- text-decoration: none;
+.dropdown-menu .item > a:not(.addItem):hover,
+.dropdown-menu .item > .as-link:not(.addItem):hover {
+ background: #ca7227;
color: #fff;
}
-.dropdown-menu .item:not(.addItem):hover .icon {
+.dropdown-menu .item > a:not(.addItem):hover .icon,
+.dropdown-menu .item > .as-link:not(.addItem):hover .icon {
filter: grayscale(100%) brightness(2.5);
}
-.dropdown-menu .item:not(.addItem) a:hover,
+.dropdown-menu .item.dropdown-section {
+ margin-top: 0.75rem;
+}
+.dropdown-menu .item.dropdown-section ~ .dropdown-section {
+ border-top-color: #f5f0ec;
+}
+.dropdown-menu .item.dropdown-section .item a, .dropdown-menu .item.dropdown-section .item .as-link {
+ padding-right: 2rem;
+}
+.dropdown-menu .item:not(.addItem) > a:hover,
.dropdown-menu .item:not(.addItem) button:hover {
background: #ca7227;
color: #fff;
}
-.dropdown-menu .item:not(.addItem) a:hover .icon,
+.dropdown-menu .item:not(.addItem) > a:hover .icon,
.dropdown-menu .item:not(.addItem) button:hover .icon {
filter: brightness(3);
}
@@ -315,8 +327,6 @@ form th {
/*=== Alerts */
.alert {
- margin: 1rem 0;
- padding: 1rem;
background: #fcfaf8;
color: #766556;
font-size: 1rem;
@@ -518,7 +528,7 @@ form th {
.stick .btn:first-child {
border-radius: 0 5px 5px 0;
}
-.stick .btn:last-child, .stick input:last-child, .stick .btn + .dropdown > .btn {
+.stick .btn:last-child, .stick input:last-child, .stick .dropdown:last-child > .btn {
border-radius: 5px 0 0 5px;
}
.stick .btn + .btn,
@@ -548,32 +558,29 @@ form th {
/* Sidebar des pages de configuration */
/*=== Navigation */
-.nav-list .nav-header,
+.nav-list {
+ font-size: 1rem;
+}
+.nav-list .item.nav-header,
.nav-list .item {
- height: 2.5em;
+ min-height: 2.5em;
line-height: 2.5em;
- font-size: 1rem;
}
.nav-list .item {
background: #fbf9f6;
+ min-height: 2.5em;
+ line-height: 2.5em;
+}
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5em;
}
.nav-list .item a {
padding: 0 1rem;
color: #363330;
transition: all 0.15s ease-in-out;
}
-.nav-list .item .error a {
- color: #f5633e;
-}
-.nav-list .item:hover .error a {
- background: #ca7227;
- color: #363330;
-}
-.nav-list .item:hover .empty a {
- background: #f4f762;
- color: #363330;
-}
-.nav-list .item:hover a {
+.nav-list .item a:hover {
background: #efe3d3;
text-decoration: none;
}
@@ -581,24 +588,13 @@ form th {
background: #ca7227;
color: #fff;
}
-.nav-list .item.active .error a {
- background: #ca7227;
- color: #fff;
-}
-.nav-list .item.active .empty a {
- background: #f4f762;
- color: #fff;
-}
.nav-list .item.active a {
background: #ca7227;
color: #fff;
text-decoration: none;
}
-.nav-list.empty a {
- color: #f4f762;
-}
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
color: #766556;
text-transform: uppercase;
@@ -668,26 +664,22 @@ form th {
/*===============*/
/*=== Header */
.header {
- padding: 0.5rem 1.35rem;
background: #fbf9f6;
- display: block;
- width: auto;
- height: 3.5rem;
- table-layout: none;
}
.header .item {
vertical-align: middle;
}
-.header .item.title {
- width: 280px;
- font-weight: 400;
+.header .item.title a {
+ padding: 0.5rem 1rem;
}
-.header .item.title a img {
- margin: 0.6em 0 0.3em;
+.header .item.title a .logo {
filter: invert(80%);
}
+.header .item.title a:hover .logo {
+ filter: invert(80%) opacity(80%);
+}
.header .item.search input {
- width: 230px;
+ width: 350px;
color: #363330;
border: none;
border-radius: 0 2px 2px 0;
@@ -698,7 +690,6 @@ form th {
background-color: #efe3d3;
}
.header .item.search input:focus {
- width: 350px;
color: #766556;
background-color: #fff;
}
@@ -727,10 +718,6 @@ form th {
filter: brightness(3);
}
.header .item.configure {
- width: 3rem;
- position: absolute;
- left: 1rem;
- top: 1rem;
text-align: center;
}
.header .item.configure .btn {
@@ -740,7 +727,7 @@ form th {
/*=== Body */
#global {
- height: calc(100% - 3.5rem);
+ height: calc(100vh - (3rem + 2 * var(--frss-padding-top-bottom)));
}
/*=== Prompt (centered) */
@@ -842,13 +829,16 @@ main.prompt {
.nav_menu .stick .btn.read_all:hover {
background-color: #e4d8cc;
}
-.nav_menu .stick .dropdown a.dropdown-toggle {
+.nav_menu .stick .dropdown:not(#dropdown-search-wrapper) a.dropdown-toggle {
border-right-width: 0;
background-image: url(icons/more.svg);
}
-.nav_menu .stick .dropdown a.dropdown-toggle .icon {
+.nav_menu .stick .dropdown:not(#dropdown-search-wrapper) a.dropdown-toggle .icon {
display: none;
}
+.nav_menu .stick #dropdown-search-wrapper.dropdown a.dropdown-toggle {
+ border-right-width: 0;
+}
#dropdown-query ~ .dropdown-menu .dropdown-header .icon {
vertical-align: middle;
@@ -951,9 +941,7 @@ main.prompt {
color: #fff;
}
.notification a.close {
- padding: 0 15px;
border-radius: 3px 0 0 3px;
- line-height: 3em;
}
.notification.good a.close:hover {
background: #0c7556;
@@ -961,12 +949,12 @@ main.prompt {
.notification.bad a.close:hover {
background: #73341f;
}
-.notification#actualizeProgress {
- line-height: 2em;
-}
.notification#actualizeProgress br {
display: none;
}
+.notification#actualizeProgress .title {
+ margin: 0 2rem;
+}
/*=== Navigation menu (for articles) */
#nav_entries {
@@ -982,10 +970,10 @@ main.prompt {
background: #fff;
transition: all 0.15s ease-in-out;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #fcfaf8;
}
-.flux:hover:not(.current):hover .item.title {
+.flux .flux_header:hover:not(.current):hover .item.title {
background: #fcfaf8;
}
.flux.current {
@@ -1194,9 +1182,6 @@ main.prompt {
/*=== MOBILE */
/*===========*/
@media (max-width: 840px) {
- .form-group .group-name {
- text-align: right;
- }
.aside {
transition: all 0.2s ease-in-out;
}
@@ -1224,29 +1209,13 @@ main.prompt {
#slider .toggle_aside .icon {
filter: grayscale(100%) brightness(2.5);
}
- .header {
- padding: 0.5rem;
- height: 8rem;
- }
.header .item.search {
- display: block;
- }
- .header .item.search form {
- display: inherit;
- }
- .header .item.search .stick {
- display: flex;
- }
- .header .item.search input {
- width: 90%;
- height: 3.5rem;
- }
- .header .item.search input:focus {
- width: 100%;
+ display: none;
}
- .header .item.search .btn {
- min-height: 49px;
- padding: 0.5rem 2rem;
+ .header .item.configure {
+ position: absolute;
+ top: 0;
+ left: 0;
}
#global {
height: calc(100% - 8rem);
@@ -1269,10 +1238,6 @@ main.prompt {
.nav_menu .stick .btn.read_all {
padding: 0.85rem 1.25rem;
}
- .nav_menu .search {
- display: none;
- max-width: 97%;
- }
.nav_menu .search .input {
max-width: 97%;
width: 90px;
diff --git a/p/themes/Ansum/icons/up.svg b/p/themes/Ansum/icons/up.svg
index 306b076e9..631ef9fdb 100644
--- a/p/themes/Ansum/icons/up.svg
+++ b/p/themes/Ansum/icons/up.svg
@@ -1,59 +1,3 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="16"
- height="10"
- viewBox="0 0 16 10"
- version="1.1"
- id="svg5"
- sodipodi:docname="up.svg"
- inkscape:version="0.92.3 (unknown)">
- <metadata
- id="metadata11">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <defs
- id="defs9" />
- <sodipodi:namedview
- pagecolor="#ffffff"
- bordercolor="#666666"
- borderopacity="1"
- objecttolerance="10"
- gridtolerance="10"
- guidetolerance="10"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:window-width="1920"
- inkscape:window-height="1026"
- id="namedview7"
- showgrid="false"
- inkscape:zoom="26.222222"
- inkscape:cx="7.5"
- inkscape:cy="4.5"
- inkscape:window-x="0"
- inkscape:window-y="27"
- inkscape:window-maximized="1"
- inkscape:current-layer="svg5" />
- <g
- id="surface2">
- <path
- style="fill:#515151;fill-opacity:1;fill-rule:nonzero;stroke:none"
- d="M 14.386719,9.46875 15.800781,8.054688 8.09375,0.347656 0.386719,8.054688 1.800781,9.46875 8.09375,3.175781 Z m 0,0"
- id="path2"
- inkscape:connector-curvature="0" />
- </g>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+ <path d="M14.387 9.469 15.8 8.055 8.094.348.387 8.055 1.8 9.469l6.293-6.293Zm0 0" style="fill:#515151;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(-.094 3.092)"/>
</svg>
diff --git a/p/themes/BlueLagoon/BlueLagoon.css b/p/themes/BlueLagoon/BlueLagoon.css
index caf81bb51..14a369a2e 100644
--- a/p/themes/BlueLagoon/BlueLagoon.css
+++ b/p/themes/BlueLagoon/BlueLagoon.css
@@ -66,17 +66,12 @@ input:disabled, select:disabled {
border-color: #ccc;
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #ddd;
}
@@ -107,7 +102,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: right;
}
.form-group .group-controls {
@@ -267,13 +261,16 @@ a.btn {
box-shadow: 0 -1px rgba(255,255,255,0.08) inset;
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5em;
+.nav-list {
font-size: 0.9rem;
}
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item a:hover {
text-shadow: 0 0 2px rgba(255,255,255,0.28);
color: #fff;
@@ -288,7 +285,7 @@ a.btn {
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
color: #ccc;
}
@@ -296,27 +293,8 @@ a.btn {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: #f39c12;
-}
-
-.nav-list .item.active.empty a {
- background: linear-gradient(180deg, #e4992c 0%, #d18114 100%) #e4992c;
- background: -webkit-linear-gradient(180deg, #e4992c 0%, #d18114 100%);
- color: #fff;
-}
-
-.nav-list .item.error a {
- color: #bd362f;
-}
-
-.nav-list .item.active.error a {
- background: #bd362f;
- color: #fff;
-}
-
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
background: transparent;
color: #222;
}
@@ -344,7 +322,6 @@ a.btn {
height: 10px;
border-top: 1px solid #171717;
border-left: 1px solid #171717;
- content: "";
position: absolute;
top: -6px;
right: 13px;
@@ -352,7 +329,8 @@ a.btn {
transform: rotate(45deg);
}
-.dropdown-header {
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
padding: 0 5px 5px;
color: #ccc;
font-weight: bold;
@@ -362,9 +340,9 @@ a.btn {
filter: grayscale(100%) brightness(2.5);
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5em;
color: #ccc;
@@ -375,8 +353,19 @@ a.btn {
color: #ccc;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-left: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background: linear-gradient(180deg, #0090ff 0%, #0062be 100%) #e4992c;
background: -webkit-linear-gradient(top, #0090ff 0%, #0062be 100%);
color: #fff;
@@ -394,8 +383,6 @@ a.btn {
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background: #f4f4f4;
color: #aaa;
font-size: 0.9em;
@@ -528,8 +515,6 @@ a.btn {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 2.5rem;
font-size: 0.8rem;
}
@@ -604,10 +589,6 @@ a.btn {
}
.header > .item.search input {
- width: 230px;
-}
-
-.header .item.search input:focus {
width: 350px;
}
@@ -802,7 +783,7 @@ a.btn {
background: #f9f7f4;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #f9f7f4;
}
@@ -950,11 +931,6 @@ a.btn {
color: #eb2901;
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3em;
-}
-
.notification a.close:hover {
background: rgba(255,255,255,0.2);
}
@@ -963,10 +939,6 @@ a.btn {
filter: brightness(2);
}
-.notification#actualizeProgress {
- line-height: 2em;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
background: #f9f7f4;
@@ -1061,6 +1033,10 @@ a.btn {
color: #222;
}
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ margin-top: 1rem;
+}
+
/*=== PANEL */
/*===========*/
#panel {
@@ -1116,7 +1092,6 @@ a.btn {
@media screen and (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: left;
}
.header {
diff --git a/p/themes/BlueLagoon/BlueLagoon.rtl.css b/p/themes/BlueLagoon/BlueLagoon.rtl.css
index f4c4e0d84..7c16ee958 100644
--- a/p/themes/BlueLagoon/BlueLagoon.rtl.css
+++ b/p/themes/BlueLagoon/BlueLagoon.rtl.css
@@ -66,17 +66,12 @@ input:disabled, select:disabled {
border-color: #ccc;
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #ddd;
}
@@ -107,7 +102,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: left;
}
.form-group .group-controls {
@@ -267,13 +261,16 @@ a.btn {
box-shadow: 0 -1px rgba(255,255,255,0.08) inset;
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5em;
+.nav-list {
font-size: 0.9rem;
}
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item a:hover {
text-shadow: 0 0 2px rgba(255,255,255,0.28);
color: #fff;
@@ -288,7 +285,7 @@ a.btn {
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
color: #ccc;
}
@@ -296,27 +293,8 @@ a.btn {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: #f39c12;
-}
-
-.nav-list .item.active.empty a {
- background: linear-gradient(-180deg, #e4992c 0%, #d18114 100%) #e4992c;
- background: -webkit-linear-gradient(-180deg, #e4992c 0%, #d18114 100%);
- color: #fff;
-}
-
-.nav-list .item.error a {
- color: #bd362f;
-}
-
-.nav-list .item.active.error a {
- background: #bd362f;
- color: #fff;
-}
-
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
background: transparent;
color: #222;
}
@@ -344,7 +322,6 @@ a.btn {
height: 10px;
border-top: 1px solid #171717;
border-right: 1px solid #171717;
- content: "";
position: absolute;
top: -6px;
left: 13px;
@@ -352,7 +329,8 @@ a.btn {
transform: rotate(-45deg);
}
-.dropdown-header {
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
padding: 0 5px 5px;
color: #ccc;
font-weight: bold;
@@ -362,9 +340,9 @@ a.btn {
filter: grayscale(100%) brightness(2.5);
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5em;
color: #ccc;
@@ -375,8 +353,19 @@ a.btn {
color: #ccc;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-right: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background: linear-gradient(-180deg, #0090ff 0%, #0062be 100%) #e4992c;
background: -webkit-linear-gradient(top, #0090ff 0%, #0062be 100%);
color: #fff;
@@ -394,8 +383,6 @@ a.btn {
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background: #f4f4f4;
color: #aaa;
font-size: 0.9em;
@@ -528,8 +515,6 @@ a.btn {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 2.5rem;
font-size: 0.8rem;
}
@@ -604,10 +589,6 @@ a.btn {
}
.header > .item.search input {
- width: 230px;
-}
-
-.header .item.search input:focus {
width: 350px;
}
@@ -802,7 +783,7 @@ a.btn {
background: #f9f7f4;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #f9f7f4;
}
@@ -950,11 +931,6 @@ a.btn {
color: #eb2901;
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3em;
-}
-
.notification a.close:hover {
background: rgba(255,255,255,0.2);
}
@@ -963,10 +939,6 @@ a.btn {
filter: brightness(2);
}
-.notification#actualizeProgress {
- line-height: 2em;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
background: #f9f7f4;
@@ -1061,6 +1033,10 @@ a.btn {
color: #222;
}
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ margin-top: 1rem;
+}
+
/*=== PANEL */
/*===========*/
#panel {
@@ -1116,7 +1092,6 @@ a.btn {
@media screen and (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: right;
}
.header {
diff --git a/p/themes/BlueLagoon/icons/bookmark.svg b/p/themes/BlueLagoon/icons/bookmark.svg
deleted file mode 100644
index 898488038..000000000
--- a/p/themes/BlueLagoon/icons/bookmark.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14.031" height="12.989">
- <path fill="#666" d="M69.003 398.01a1.03 1.03 0 0 0-.665.24c-.166.138-.277.32-.39.5-.224.357-.392.768-.565 1.203-.173.435-.347.888-.504 1.232a2.606 2.606 0 0 1-.36.588 2.621 2.621 0 0 1-.695.152c-.388.035-.881.05-1.363.07-.482.02-.97.036-1.393.128-.211.038-.415.11-.604.21a.987.987 0 0 0-.45.53.994.994 0 0 0 .036.7c.085.196.227.366.368.524.283.317.633.606 1.008.899.375.293.777.559 1.068.81.29.251.454.508.459.523.018.235 0 .472-.055.702-.085.367-.223.812-.352 1.26-.13.449-.252.902-.292 1.319-.02.208-.043.407.005.614a.987.987 0 0 0 1.067.752c.22-.018.436-.07.636-.152.4-.162.801-.428 1.206-.682.405-.254.808-.522 1.146-.711.337-.19.648-.269.664-.269.016 0 .303.095.639.288.336.192.725.465 1.128.722.402.257.82.497 1.218.663.199.082.389.15.607.17.219.02.48-.03.696-.18a.947.947 0 0 0 .389-.56c.05-.206.043-.435.025-.643-.037-.417-.16-.864-.284-1.314-.126-.45-.262-.887-.345-1.255a2.64 2.64 0 0 1-.066-.701c.005-.015.187-.253.48-.501.293-.249.676-.538 1.054-.827.378-.29.767-.572 1.053-.886a1.93 1.93 0 0 0 .329-.53.916.916 0 0 0 .055-.672c-.082-.245-.271-.413-.46-.523a1.965 1.965 0 0 0-.577-.23c-.422-.095-.914-.14-1.396-.164-.482-.024-.946-.039-1.334-.077a3.698 3.698 0 0 1-.729-.14 2.713 2.713 0 0 1-.338-.582 28.41 28.41 0 0 1-.496-1.224c-.168-.437-.334-.865-.556-1.224a2.01 2.01 0 0 0-.398-.494 1.061 1.061 0 0 0-.669-.258z" style="color:#000;text-indent:0;text-align:start;text-transform:none;direction:ltr;baseline-shift:baseline;enable-background:accumulate;fill:#0062bf;fill-opacity:1" transform="translate(-61.966 -398.01)"/>
-</svg>
diff --git a/p/themes/BlueLagoon/metadata.json b/p/themes/BlueLagoon/metadata.json
index 7822d7346..37750c631 100644
--- a/p/themes/BlueLagoon/metadata.json
+++ b/p/themes/BlueLagoon/metadata.json
@@ -3,5 +3,6 @@
"author": "Mister aiR",
"description": "C’est un cocktail (bis)! C’est la version plus fresh de Screwdriver. C’est… c’est… un thème pour l’agrégateur de flux RSS FreshRSS. En toute modestie, ce thème tue du Nyan Cat.",
"version": 1.0,
- "files": ["_frss.css","BlueLagoon.css"]
+ "files": ["_frss.css","BlueLagoon.css"],
+ "deprecated": true
}
diff --git a/p/themes/Dark-pink/pinkdark.css b/p/themes/Dark-pink/pinkdark.css
index 4ccfbbb95..1d8e6e855 100644
--- a/p/themes/Dark-pink/pinkdark.css
+++ b/p/themes/Dark-pink/pinkdark.css
@@ -1,3 +1,7 @@
+:root {
+ --background-color-hover: #2f1d22;
+}
+
.btn:hover {
background: unset;
border-color: #ff449a;
@@ -87,6 +91,10 @@ input:focus {
filter: sepia(62%) brightness(107%) hue-rotate(315deg) saturate(248%) contrast(104%) invert(100%);
}
+.header > .item.title a:hover .logo {
+ filter: sepia(62%) brightness(70%) hue-rotate(315deg) saturate(248%) contrast(104%) invert(100%);
+}
+
.icon[src*="/all"],
.icon[src*="/down"],
.icon[src*="/help"],
diff --git a/p/themes/Dark-pink/pinkdark.rtl.css b/p/themes/Dark-pink/pinkdark.rtl.css
index 9c88010e5..03161d0b0 100644
--- a/p/themes/Dark-pink/pinkdark.rtl.css
+++ b/p/themes/Dark-pink/pinkdark.rtl.css
@@ -1,3 +1,7 @@
+:root {
+ --background-color-hover: #2f1d22;
+}
+
.btn:hover {
background: unset;
border-color: #ff449a;
@@ -87,6 +91,10 @@ input:focus {
filter: sepia(62%) brightness(107%) hue-rotate(315deg) saturate(248%) contrast(104%) invert(100%);
}
+.header > .item.title a:hover .logo {
+ filter: sepia(62%) brightness(70%) hue-rotate(315deg) saturate(248%) contrast(104%) invert(100%);
+}
+
.icon[src*="/all"],
.icon[src*="/down"],
.icon[src*="/help"],
diff --git a/p/themes/Dark-pink/thumbs/original.png b/p/themes/Dark-pink/thumbs/original.png
index 4bae257b5..835496eac 100644
--- a/p/themes/Dark-pink/thumbs/original.png
+++ b/p/themes/Dark-pink/thumbs/original.png
Binary files differ
diff --git a/p/themes/Dark/dark.css b/p/themes/Dark/dark.css
index c0a5fa507..fa194bb9e 100644
--- a/p/themes/Dark/dark.css
+++ b/p/themes/Dark/dark.css
@@ -78,17 +78,12 @@ input:disabled, select:disabled {
border-color: #000;
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #333;
}
@@ -117,7 +112,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: right;
}
.form-group .group-controls {
@@ -263,13 +257,16 @@ a.btn {
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5em;
+.nav-list {
font-size: 0.9rem;
}
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item a:hover {
background: #26303f;
}
@@ -284,35 +281,15 @@ a.btn {
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
}
.nav-list a:hover {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: #c95;
-}
-
-.nav-list .item:hover.empty a,
-.nav-list .item.active.empty a {
- background: #c95;
- color: #fff;
-}
-
-.nav-list .item.error a {
- color: #a44;
-}
-
-.nav-list .item:hover.error a,
-.nav-list .item.active.error a {
- background: #a44;
- color: #fff;
-}
-
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
background: #111;
border-bottom: 1px solid #333;
@@ -338,23 +315,35 @@ a.btn {
border-color: #888;
}
-.dropdown-header {
- padding: 0 5px 5px;
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
+ padding: 0.25rem 0.5rem 0.25rem 1rem;
font-weight: bold;
text-align: left;
color: #888;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5em;
- font-size: 0.8rem;
+ font-size: inherit;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-left: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background: #26303f;
color: #888;
}
@@ -372,14 +361,13 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: #333;
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background: #111;
color: #aaa;
font-size: 0.9em;
@@ -497,8 +485,6 @@ a.btn {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 2.5rem;
font-size: 0.8rem;
}
@@ -540,25 +526,20 @@ a.btn {
/*===============*/
/*=== Header */
.header > .item {
- padding: 10px;
vertical-align: middle;
text-align: center;
border-bottom: 1px solid #333;
}
-.header > .item.title {
- width: 230px;
-}
-
.header > .item.title .logo {
filter: grayscale(60%) brightness(1.1);
}
-.header > .item.search input {
- width: 230px;
+.header > .item.title a:hover .logo {
+ filter: grayscale(60%) brightness(1.5);
}
-.header .item.search input:focus {
+.header > .item.search input {
width: 350px;
}
@@ -717,7 +698,7 @@ a.btn {
border-left: 2px solid #2f2f2f;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #111;
}
@@ -841,11 +822,6 @@ a.btn {
color: #a44;
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3em;
-}
-
.notification a.close:hover {
background: #222;
border-radius: 0 3px 3px 0;
@@ -859,10 +835,6 @@ a.btn {
background: #a44;
}
-.notification#actualizeProgress {
- line-height: 2em;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
text-align: center;
@@ -934,6 +906,11 @@ a.btn {
text-shadow: none;
}
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ background-color: #171717;
+ margin-top: 1rem;
+}
+
/*=== Panel */
#panel {
background: #1c1c1c;
@@ -989,7 +966,6 @@ a.btn {
@media (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: left;
}
.aside {
@@ -1019,20 +995,6 @@ a.btn {
margin: 5px 0;
}
- .nav_menu .search {
- display: inline-block;
- max-width: 97%;
- }
-
- .nav_menu .search input {
- max-width: 97%;
- width: 90px;
- }
-
- .nav_menu .search input:focus {
- width: 400px;
- }
-
.dropdown-target:target ~ .dropdown-toggle::after {
background-color: #1a1a1a;
border-top: 1px solid #888;
diff --git a/p/themes/Dark/dark.rtl.css b/p/themes/Dark/dark.rtl.css
index 82ec53464..2610e49d5 100644
--- a/p/themes/Dark/dark.rtl.css
+++ b/p/themes/Dark/dark.rtl.css
@@ -78,17 +78,12 @@ input:disabled, select:disabled {
border-color: #000;
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #333;
}
@@ -117,7 +112,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: left;
}
.form-group .group-controls {
@@ -263,13 +257,16 @@ a.btn {
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5em;
+.nav-list {
font-size: 0.9rem;
}
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item a:hover {
background: #26303f;
}
@@ -284,35 +281,15 @@ a.btn {
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
}
.nav-list a:hover {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: #c95;
-}
-
-.nav-list .item:hover.empty a,
-.nav-list .item.active.empty a {
- background: #c95;
- color: #fff;
-}
-
-.nav-list .item.error a {
- color: #a44;
-}
-
-.nav-list .item:hover.error a,
-.nav-list .item.active.error a {
- background: #a44;
- color: #fff;
-}
-
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
background: #111;
border-bottom: 1px solid #333;
@@ -338,23 +315,35 @@ a.btn {
border-color: #888;
}
-.dropdown-header {
- padding: 0 5px 5px;
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
+ padding: 0.25rem 1rem 0.25rem 0.5rem;
font-weight: bold;
text-align: right;
color: #888;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5em;
- font-size: 0.8rem;
+ font-size: inherit;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-right: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background: #26303f;
color: #888;
}
@@ -372,14 +361,13 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: #333;
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background: #111;
color: #aaa;
font-size: 0.9em;
@@ -497,8 +485,6 @@ a.btn {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 2.5rem;
font-size: 0.8rem;
}
@@ -540,25 +526,20 @@ a.btn {
/*===============*/
/*=== Header */
.header > .item {
- padding: 10px;
vertical-align: middle;
text-align: center;
border-bottom: 1px solid #333;
}
-.header > .item.title {
- width: 230px;
-}
-
.header > .item.title .logo {
filter: grayscale(60%) brightness(1.1);
}
-.header > .item.search input {
- width: 230px;
+.header > .item.title a:hover .logo {
+ filter: grayscale(60%) brightness(1.5);
}
-.header .item.search input:focus {
+.header > .item.search input {
width: 350px;
}
@@ -717,7 +698,7 @@ a.btn {
border-right: 2px solid #2f2f2f;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #111;
}
@@ -841,11 +822,6 @@ a.btn {
color: #a44;
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3em;
-}
-
.notification a.close:hover {
background: #222;
border-radius: 3px 0 0 3px;
@@ -859,10 +835,6 @@ a.btn {
background: #a44;
}
-.notification#actualizeProgress {
- line-height: 2em;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
text-align: center;
@@ -934,6 +906,11 @@ a.btn {
text-shadow: none;
}
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ background-color: #171717;
+ margin-top: 1rem;
+}
+
/*=== Panel */
#panel {
background: #1c1c1c;
@@ -989,7 +966,6 @@ a.btn {
@media (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: right;
}
.aside {
@@ -1019,20 +995,6 @@ a.btn {
margin: 5px 0;
}
- .nav_menu .search {
- display: inline-block;
- max-width: 97%;
- }
-
- .nav_menu .search input {
- max-width: 97%;
- width: 90px;
- }
-
- .nav_menu .search input:focus {
- width: 400px;
- }
-
.dropdown-target:target ~ .dropdown-toggle::after {
background-color: #1a1a1a;
border-top: 1px solid #888;
diff --git a/p/themes/Flat/flat.css b/p/themes/Flat/flat.css
index 2f0362b9d..8fd09bb35 100644
--- a/p/themes/Flat/flat.css
+++ b/p/themes/Flat/flat.css
@@ -70,17 +70,12 @@ input:disabled, select:disabled {
background: #eee;
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #ddd;
}
@@ -104,7 +99,6 @@ form th {
}
.form-group::after {
- content: "";
display: block;
clear: both;
}
@@ -129,7 +123,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: right;
}
.form-group .group-controls {
@@ -154,7 +147,7 @@ form th {
.stick .btn:last-child,
.stick input:last-child,
-.stick .btn + .dropdown > .btn {
+.stick .dropdown:last-child > .btn {
border-radius: 0 5px 5px 0;
}
@@ -253,13 +246,16 @@ a.btn {
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5em;
+.nav-list {
font-size: 0.9rem;
}
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item a:hover,
.nav-list .item.active {
background: #2980b9;
@@ -271,35 +267,15 @@ a.btn {
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
}
.nav-list a:hover {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: #f39c12;
-}
-
-.nav-list .item:hover.empty a,
-.nav-list .item.active.empty a {
- background: #f39c12;
- color: #fff;
-}
-
-.nav-list .item.error a {
- color: #bd362f;
-}
-
-.nav-list .item:hover.error a,
-.nav-list .item.active.error a {
- background: #bd362f;
- color: #fff;
-}
-
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
background: #34495e;
color: #fff;
@@ -313,7 +289,7 @@ a.btn {
/*=== Dropdown */
.dropdown-menu {
margin: 0.5rem 0 0;
- padding: 0.5rem 0;
+ padding: 0.5rem 0 0.25rem 0;
background: #fafafa;
font-size: 0.8rem;
border: 1px solid #95a5a6;
@@ -326,7 +302,8 @@ a.btn {
right: 12px;
}
-.dropdown-header {
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
padding: 0 0.5rem 0.5rem;
font-weight: bold;
text-align: left;
@@ -338,16 +315,27 @@ a.btn {
right: 0.5rem;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5em;
- font-size: 0.8rem;
+ font-size: inherit;
+}
+
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-left: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background: #2980b9;
color: #fff;
}
@@ -370,14 +358,13 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: #ddd;
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background: #f4f4f4;
color: #aaa;
font-size: 0.9em;
@@ -456,7 +443,6 @@ a.btn {
.box .box-content .item {
font-size: 0.9rem;
- line-height: 2.5em;
}
.box .box-title .configure .icon,
@@ -498,8 +484,6 @@ a.btn {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 2.5rem;
font-size: 0.8rem;
}
@@ -542,13 +526,12 @@ a.btn {
}
.header > .item {
- padding: 10px;
vertical-align: middle;
text-align: center;
}
-.header > .item.title {
- width: 230px;
+.header > .item.title a:hover .logo {
+ filter: brightness(1.5);
}
.header > .item.title h1 {
@@ -560,10 +543,6 @@ a.btn {
}
.header > .item.search input {
- width: 230px;
-}
-
-.header .item.search input:focus {
width: 350px;
}
@@ -728,7 +707,7 @@ a.btn {
border-left: 2px solid #ecf0f1;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #fff;
}
@@ -745,10 +724,6 @@ a.btn {
background: #fff3ed;
}
-.flux.not_read:not(.current):hover .item.title {
- background: inherit;
-}
-
.flux.favorite {
border-left-color: #ffc300;
}
@@ -757,10 +732,6 @@ a.btn {
background: #fff6da;
}
-.flux.favorite:not(.current):hover .item.title {
- background: #fff6da;
-}
-
.flux_header {
font-size: 0.8rem;
cursor: pointer;
@@ -863,8 +834,6 @@ a.btn {
}
.notification a.close {
- padding: 0 15px;
- line-height: 3em;
border-radius: 0 3px 3px 0;
}
@@ -876,10 +845,6 @@ a.btn {
background: #c0392b;
}
-.notification#actualizeProgress {
- line-height: 2em;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
text-align: center;
@@ -989,7 +954,6 @@ a.btn {
@media (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: left;
}
.aside {
diff --git a/p/themes/Flat/flat.rtl.css b/p/themes/Flat/flat.rtl.css
index 63d0103c4..43327ddeb 100644
--- a/p/themes/Flat/flat.rtl.css
+++ b/p/themes/Flat/flat.rtl.css
@@ -70,17 +70,12 @@ input:disabled, select:disabled {
background: #eee;
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #ddd;
}
@@ -104,7 +99,6 @@ form th {
}
.form-group::after {
- content: "";
display: block;
clear: both;
}
@@ -129,7 +123,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: left;
}
.form-group .group-controls {
@@ -154,7 +147,7 @@ form th {
.stick .btn:last-child,
.stick input:last-child,
-.stick .btn + .dropdown > .btn {
+.stick .dropdown:last-child > .btn {
border-radius: 5px 0 0 5px;
}
@@ -253,13 +246,16 @@ a.btn {
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5em;
+.nav-list {
font-size: 0.9rem;
}
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item a:hover,
.nav-list .item.active {
background: #2980b9;
@@ -271,35 +267,15 @@ a.btn {
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
}
.nav-list a:hover {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: #f39c12;
-}
-
-.nav-list .item:hover.empty a,
-.nav-list .item.active.empty a {
- background: #f39c12;
- color: #fff;
-}
-
-.nav-list .item.error a {
- color: #bd362f;
-}
-
-.nav-list .item:hover.error a,
-.nav-list .item.active.error a {
- background: #bd362f;
- color: #fff;
-}
-
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
background: #34495e;
color: #fff;
@@ -313,7 +289,7 @@ a.btn {
/*=== Dropdown */
.dropdown-menu {
margin: 0.5rem 0 0;
- padding: 0.5rem 0;
+ padding: 0.5rem 0 0.25rem 0;
background: #fafafa;
font-size: 0.8rem;
border: 1px solid #95a5a6;
@@ -326,7 +302,8 @@ a.btn {
left: 12px;
}
-.dropdown-header {
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
padding: 0 0.5rem 0.5rem;
font-weight: bold;
text-align: right;
@@ -338,16 +315,27 @@ a.btn {
left: 0.5rem;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5em;
- font-size: 0.8rem;
+ font-size: inherit;
+}
+
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-right: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background: #2980b9;
color: #fff;
}
@@ -370,14 +358,13 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: #ddd;
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background: #f4f4f4;
color: #aaa;
font-size: 0.9em;
@@ -456,7 +443,6 @@ a.btn {
.box .box-content .item {
font-size: 0.9rem;
- line-height: 2.5em;
}
.box .box-title .configure .icon,
@@ -498,8 +484,6 @@ a.btn {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 2.5rem;
font-size: 0.8rem;
}
@@ -542,13 +526,12 @@ a.btn {
}
.header > .item {
- padding: 10px;
vertical-align: middle;
text-align: center;
}
-.header > .item.title {
- width: 230px;
+.header > .item.title a:hover .logo {
+ filter: brightness(1.5);
}
.header > .item.title h1 {
@@ -560,10 +543,6 @@ a.btn {
}
.header > .item.search input {
- width: 230px;
-}
-
-.header .item.search input:focus {
width: 350px;
}
@@ -728,7 +707,7 @@ a.btn {
border-right: 2px solid #ecf0f1;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #fff;
}
@@ -745,10 +724,6 @@ a.btn {
background: #fff3ed;
}
-.flux.not_read:not(.current):hover .item.title {
- background: inherit;
-}
-
.flux.favorite {
border-right-color: #ffc300;
}
@@ -757,10 +732,6 @@ a.btn {
background: #fff6da;
}
-.flux.favorite:not(.current):hover .item.title {
- background: #fff6da;
-}
-
.flux_header {
font-size: 0.8rem;
cursor: pointer;
@@ -863,8 +834,6 @@ a.btn {
}
.notification a.close {
- padding: 0 15px;
- line-height: 3em;
border-radius: 3px 0 0 3px;
}
@@ -876,10 +845,6 @@ a.btn {
background: #c0392b;
}
-.notification#actualizeProgress {
- line-height: 2em;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
text-align: center;
@@ -989,7 +954,6 @@ a.btn {
@media (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: right;
}
.aside {
diff --git a/p/themes/Flat/metadata.json b/p/themes/Flat/metadata.json
index 27a83fe83..bd7ce6157 100644
--- a/p/themes/Flat/metadata.json
+++ b/p/themes/Flat/metadata.json
@@ -3,5 +3,6 @@
"author": "Marien Fressinaud",
"description": "Thème plat pour FreshRSS",
"version": 0.2,
- "files": ["_frss.css", "flat.css"]
+ "files": ["_frss.css", "flat.css"],
+ "deprecated": true
}
diff --git a/p/themes/Mapco/_components.scss b/p/themes/Mapco/_components.scss
index cf7aca73d..5f7e04e56 100644
--- a/p/themes/Mapco/_components.scss
+++ b/p/themes/Mapco/_components.scss
@@ -22,6 +22,12 @@
}
/*=== Dropdown */
+.dropdown {
+ .dropdown-target:target + .btn {
+ background-color: variables.$grey-medium-light;
+ }
+}
+
.dropdown-menu {
margin: 9px 0 0 0;
padding: 0.5rem 0 1rem 0;
@@ -37,8 +43,9 @@
right: 18px;
}
- .dropdown-header {
- padding: 1rem 0.5rem 1rem 1rem;
+ .dropdown-header,
+ .dropdown-section .dropdown-section-title {
+ padding: 1rem 1.5rem;
font-weight: bold;
text-align: left;
color: variables.$grey-dark;
@@ -50,16 +57,22 @@
@include mixins.transition(all, 0.075s, ease-in-out);
- a, span, .as-link {
+ > a,
+ > span,
+ > .as-link {
padding: 0 2rem;
color: variables.$main-font-color;
- font-size: 1rem;
+ font-size: inherit;
line-height: 2.5em;
+
+ span.icon {
+ padding: 0 0.25rem !important;
+ }
}
- &:not(.addItem) {
- a:hover,
- button:hover {
+ > a,
+ > .as-link {
+ &:not(.addItem):hover {
background: variables.$main-first;
color: variables.$white;
@@ -69,6 +82,20 @@
}
}
+ &.dropdown-section {
+ margin-top: 0.75rem;
+
+ ~ .dropdown-section {
+ border-top-color: variables.$grey-light;
+ }
+
+ .item {
+ a, .as-link {
+ padding-left: 2rem;
+ }
+ }
+ }
+
&[aria-checked="true"] {
a::before {
margin: 0 0 0 -14px;
@@ -122,8 +149,6 @@
/*=== Alerts */
.alert {
- margin: 1rem 0;
- padding: 1rem;
background: variables.$grey-lighter;
color: variables.$grey-dark;
font-size: 1rem;
diff --git a/p/themes/Mapco/_fonts.scss b/p/themes/Mapco/_fonts.scss
index 687525743..57c9c9bf2 100644
--- a/p/themes/Mapco/_fonts.scss
+++ b/p/themes/Mapco/_fonts.scss
@@ -19,7 +19,7 @@
font-style: normal;
font-stretch: normal;
font-weight: 700;
- src: url("../fonts/LatoLatin-Regular.woff") format("woff");
+ src: url("../fonts/LatoLatin-Bold.woff") format("woff");
}
@font-face {
diff --git a/p/themes/Mapco/_forms.scss b/p/themes/Mapco/_forms.scss
index 0f5f96681..977a70346 100644
--- a/p/themes/Mapco/_forms.scss
+++ b/p/themes/Mapco/_forms.scss
@@ -119,24 +119,17 @@ input:disabled, select:disabled {
background: variables.$grey-light;
}
-input.extend {
- transition: width 200ms linear;
-}
-
-
.form-group {
padding: 5px;
border-radius: 3px;
&::after {
- content: "";
display: block;
clear: both;
}
.group-name {
padding: 10px 0;
- text-align: right;
}
.group-controls {
diff --git a/p/themes/Mapco/_layout.scss b/p/themes/Mapco/_layout.scss
index a4aaf6fa8..b57a48fd0 100644
--- a/p/themes/Mapco/_layout.scss
+++ b/p/themes/Mapco/_layout.scss
@@ -6,36 +6,30 @@
/*===============*/
/*=== Header */
.header {
- padding: 0.5rem 1.35rem;
background: variables.$sid-bg;
- display: block;
- width: auto;
- height: 3.5rem;
- table-layout: none;
-
- .logo {
- margin: 11px 0 5px;
- filter: grayscale(100%) brightness(100);
- }
.item {
vertical-align: middle;
&.title {
- width: 280px;
+ a {
+ padding: 0.5rem 1rem;
- font-weight: 400;
+ .logo {
+ filter: grayscale(100%) brightness(3);
+ }
- a {
- img {
- margin-right: 0.5rem;
+ &:hover {
+ .logo {
+ filter: grayscale(100%) brightness(2.2);
+ }
}
}
}
&.search {
input {
- width: 230px;
+ width: 350px;
color: variables.$sid-font-color;
border: none;
border-radius: 2px 0 0 2px;
@@ -48,9 +42,7 @@
}
&:focus {
- width: 350px;
color: variables.$grey-dark;
-
background-color: variables.$white;
}
}
@@ -82,10 +74,6 @@
}
&.configure {
- width: 3rem;
- position: absolute;
- right: 1rem;
- top: 1rem;
text-align: center;
.btn .icon,
@@ -107,7 +95,7 @@
/*=== Body */
#global {
- height: calc(100% - 3.5rem);
+ height: calc(100vh - (calc(3rem + 2 * var(--frss-padding-top-bottom))));
}
/*=== Prompt (centered) */
@@ -238,7 +226,7 @@ main.prompt {
}
}
- .dropdown {
+ .dropdown:not(#dropdown-search-wrapper) {
a.dropdown-toggle {
border-left-width: 0;
background-image: url(icons/more.svg);
@@ -248,6 +236,12 @@ main.prompt {
}
}
}
+
+ #dropdown-search-wrapper.dropdown {
+ a.dropdown-toggle {
+ border-left-width: 0;
+ }
+ }
}
}
@@ -366,9 +360,7 @@ main.prompt {
}
a.close {
- padding: 0 15px;
border-radius: 0 3px 3px 0;
- line-height: 3em;
}
&.good a.close:hover {
@@ -380,11 +372,13 @@ main.prompt {
}
&#actualizeProgress {
- line-height: 2em;
-
br {
display: none;
}
+
+ .title {
+ margin: 0 2rem;
+ }
}
}
diff --git a/p/themes/Mapco/_list-view.scss b/p/themes/Mapco/_list-view.scss
index 40fab3c1c..7d0fb87f4 100644
--- a/p/themes/Mapco/_list-view.scss
+++ b/p/themes/Mapco/_list-view.scss
@@ -10,11 +10,13 @@
@include mixins.transition(all, 0.15s, ease-in-out);
- &:hover {
- background: variables.$grey-lighter;
-
- &:not(.current):hover .item.title {
+ .flux_header {
+ &:hover {
background: variables.$grey-lighter;
+
+ &:not(.current):hover .item.title {
+ background: variables.$grey-lighter;
+ }
}
}
diff --git a/p/themes/Mapco/_mobile.scss b/p/themes/Mapco/_mobile.scss
index 1c11448f0..3faea3d7e 100644
--- a/p/themes/Mapco/_mobile.scss
+++ b/p/themes/Mapco/_mobile.scss
@@ -6,10 +6,6 @@
/*===========*/
@media (max-width: 840px) {
- .form-group .group-name {
- text-align: left;
- }
-
.aside {
@include mixins.transition(all, 0.2s, ease-in-out);
@@ -39,30 +35,15 @@
}
.header {
- padding: 0.5rem;
- height: 8rem;
-
.item {
&.search {
- display: block;
-
- form {
- display: inherit;
- }
-
- .stick {
- display: flex;
- }
-
- input {
- width: 90%;
- height: 3.5rem;
-
- &:focus {
- width: 100%;
+ display: none;
+ }
- }
- }
+ &.configure {
+ position: absolute;
+ top: 0;
+ right: 0;
}
}
@@ -103,11 +84,7 @@
}
.search {
- display: none;
- max-width: 97%;
-
.input {
-
max-width: 97%;
width: 90px;
diff --git a/p/themes/Mapco/_sidebar.scss b/p/themes/Mapco/_sidebar.scss
index 77438c836..090a69580 100644
--- a/p/themes/Mapco/_sidebar.scss
+++ b/p/themes/Mapco/_sidebar.scss
@@ -93,7 +93,7 @@
border-radius: 5px 0 0 5px;
}
- .btn:last-child, input:last-child, .btn + .dropdown > .btn {
+ .btn:last-child, input:last-child, .dropdown:last-child > .btn {
border-radius: 0 5px 5px 0;
}
@@ -133,16 +133,24 @@
/*=== Navigation */
.nav-list {
- .nav-header,
+ font-size: 1rem;
+
+ .item.nav-header,
.item {
- height: 2.5em;
+ min-height: 2.5em;
line-height: 2.5em;
- font-size: 1rem;
}
.item {
background: variables.$sid-bg;
color: variables.$white;
+ min-height: 2.5em;
+ line-height: 2.5em;
+
+ &.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5em;
+ }
a {
padding: 0 1rem;
@@ -151,51 +159,15 @@
@include mixins.transition(all, 0.15s, ease-in-out);
}
- .error {
- a {
- color: variables.$alert-bg;
- }
- }
-
- &:hover {
- .error {
- a {
- background: variables.$main-first;
- color: variables.$sid-font-color;
- }
- }
-
- .empty {
- a {
- background: variables.$warning-bg;
- color: variables.$sid-font-color;
- }
- }
-
- a {
- background: variables.$sid-bg-dark;
- text-decoration: none;
- }
+ a:hover {
+ background: variables.$sid-bg-dark;
+ text-decoration: none;
}
&.active {
background: variables.$main-first;
color: variables.$white;
- .error {
- a {
- background: variables.$main-first;
- color: variables.$white;
- }
- }
-
- .empty {
- a {
- background: variables.$warning-bg;
- color: variables.$white;
- }
- }
-
a {
background: variables.$main-first;
color: variables.$white;
@@ -204,14 +176,8 @@
}
}
- &.empty {
- a {
- color: variables.$warning-bg;
- }
- }
-
.nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
color: variables.$grey-dark;
text-transform: uppercase;
diff --git a/p/themes/Mapco/_tables.scss b/p/themes/Mapco/_tables.scss
index 4955fff6a..7376279e0 100644
--- a/p/themes/Mapco/_tables.scss
+++ b/p/themes/Mapco/_tables.scss
@@ -5,8 +5,7 @@ table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid variables.$grey-medium-light;
}
diff --git a/p/themes/Mapco/icons/up.svg b/p/themes/Mapco/icons/up.svg
index 65da6b1a7..b5baff62f 100644
--- a/p/themes/Mapco/icons/up.svg
+++ b/p/themes/Mapco/icons/up.svg
@@ -1,3 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="10">
- <path fill="#666" d="M14.387 9.469 15.8 8.055 8.094.348.387 8.055 1.8 9.469l6.293-6.293Zm0 0"/>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+ <path fill="#666" d="m14.293 12.56 1.414-1.414L8 3.44.293 11.146l1.413 1.414L8 6.267zm0 0"/>
</svg>
diff --git a/p/themes/Mapco/mapco.css b/p/themes/Mapco/mapco.css
index 028c12e12..967accc00 100644
--- a/p/themes/Mapco/mapco.css
+++ b/p/themes/Mapco/mapco.css
@@ -17,7 +17,7 @@
font-style: normal;
font-stretch: normal;
font-weight: 700;
- src: url("../fonts/LatoLatin-Regular.woff") format("woff");
+ src: url("../fonts/LatoLatin-Bold.woff") format("woff");
}
@font-face {
font-family: "lato";
@@ -163,22 +163,16 @@ input:disabled, select:disabled {
background: #eff0f2;
}
-input.extend {
- transition: width 200ms linear;
-}
-
.form-group {
padding: 5px;
border-radius: 3px;
}
.form-group::after {
- content: "";
display: block;
clear: both;
}
.form-group .group-name {
padding: 10px 0;
- text-align: right;
}
.form-group .group-controls {
min-height: 25px;
@@ -200,8 +194,7 @@ table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #d5d8db;
}
@@ -227,6 +220,10 @@ form th {
}
/*=== Dropdown */
+.dropdown .dropdown-target:target + .btn {
+ background-color: #d5d8db;
+}
+
.dropdown-menu {
margin: 9px 0 0 0;
padding: 0.5rem 0 1rem 0;
@@ -241,8 +238,9 @@ form th {
border: none;
right: 18px;
}
-.dropdown-menu .dropdown-header {
- padding: 1rem 0.5rem 1rem 1rem;
+.dropdown-menu .dropdown-header,
+.dropdown-menu .dropdown-section .dropdown-section-title {
+ padding: 1rem 1.5rem;
font-weight: bold;
text-align: left;
color: #5b6871;
@@ -252,21 +250,37 @@ form th {
.dropdown-menu .item {
transition: all 0.075s ease-in-out;
}
-.dropdown-menu .item a, .dropdown-menu .item span, .dropdown-menu .item .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 2rem;
color: #303136;
- font-size: 1rem;
+ font-size: inherit;
line-height: 2.5em;
}
-.dropdown-menu .item:not(.addItem) a:hover,
-.dropdown-menu .item:not(.addItem) button:hover {
+.dropdown-menu .item > a span.icon,
+.dropdown-menu .item > span span.icon,
+.dropdown-menu .item > .as-link span.icon {
+ padding: 0 0.25rem !important;
+}
+.dropdown-menu .item > a:not(.addItem):hover,
+.dropdown-menu .item > .as-link:not(.addItem):hover {
background: #36c;
color: #fff;
}
-.dropdown-menu .item:not(.addItem) a:hover .icon,
-.dropdown-menu .item:not(.addItem) button:hover .icon {
+.dropdown-menu .item > a:not(.addItem):hover .icon,
+.dropdown-menu .item > .as-link:not(.addItem):hover .icon {
filter: brightness(3);
}
+.dropdown-menu .item.dropdown-section {
+ margin-top: 0.75rem;
+}
+.dropdown-menu .item.dropdown-section ~ .dropdown-section {
+ border-top-color: #eff0f2;
+}
+.dropdown-menu .item.dropdown-section .item a, .dropdown-menu .item.dropdown-section .item .as-link {
+ padding-left: 2rem;
+}
.dropdown-menu .item[aria-checked=true] a::before {
margin: 0 0 0 -14px;
font-weight: bold;
@@ -303,8 +317,6 @@ form th {
/*=== Alerts */
.alert {
- margin: 1rem 0;
- padding: 1rem;
background: #f9fafb;
color: #5b6871;
font-size: 1rem;
@@ -530,7 +542,7 @@ form th {
.stick .btn:first-child {
border-radius: 5px 0 0 5px;
}
-.stick .btn:last-child, .stick input:last-child, .stick .btn + .dropdown > .btn {
+.stick .btn:last-child, .stick input:last-child, .stick .dropdown:last-child > .btn {
border-radius: 0 5px 5px 0;
}
.stick .btn + .btn,
@@ -559,33 +571,30 @@ form th {
/* Sidebar des pages de configuration */
/*=== Navigation */
-.nav-list .nav-header,
+.nav-list {
+ font-size: 1rem;
+}
+.nav-list .item.nav-header,
.nav-list .item {
- height: 2.5em;
+ min-height: 2.5em;
line-height: 2.5em;
- font-size: 1rem;
}
.nav-list .item {
background: #303136;
color: #fff;
+ min-height: 2.5em;
+ line-height: 2.5em;
+}
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5em;
}
.nav-list .item a {
padding: 0 1rem;
color: #ffffff;
transition: all 0.15s ease-in-out;
}
-.nav-list .item .error a {
- color: #f5633e;
-}
-.nav-list .item:hover .error a {
- background: #36c;
- color: #ffffff;
-}
-.nav-list .item:hover .empty a {
- background: #f4f762;
- color: #ffffff;
-}
-.nav-list .item:hover a {
+.nav-list .item a:hover {
background: #17181a;
text-decoration: none;
}
@@ -593,24 +602,13 @@ form th {
background: #36c;
color: #fff;
}
-.nav-list .item.active .error a {
- background: #36c;
- color: #fff;
-}
-.nav-list .item.active .empty a {
- background: #f4f762;
- color: #fff;
-}
.nav-list .item.active a {
background: #36c;
color: #fff;
text-decoration: none;
}
-.nav-list.empty a {
- color: #f4f762;
-}
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
color: #5b6871;
text-transform: uppercase;
@@ -680,29 +678,22 @@ form th {
/*===============*/
/*=== Header */
.header {
- padding: 0.5rem 1.35rem;
background: #303136;
- display: block;
- width: auto;
- height: 3.5rem;
- table-layout: none;
-}
-.header .logo {
- margin: 11px 0 5px;
- filter: grayscale(100%) brightness(100);
}
.header .item {
vertical-align: middle;
}
-.header .item.title {
- width: 280px;
- font-weight: 400;
+.header .item.title a {
+ padding: 0.5rem 1rem;
}
-.header .item.title a img {
- margin-right: 0.5rem;
+.header .item.title a .logo {
+ filter: grayscale(100%) brightness(3);
+}
+.header .item.title a:hover .logo {
+ filter: grayscale(100%) brightness(2.2);
}
.header .item.search input {
- width: 230px;
+ width: 350px;
color: #ffffff;
border: none;
border-radius: 2px 0 0 2px;
@@ -713,7 +704,6 @@ form th {
background-color: #17181a;
}
.header .item.search input:focus {
- width: 350px;
color: #5b6871;
background-color: #fff;
}
@@ -739,10 +729,6 @@ form th {
filter: brightness(3);
}
.header .item.configure {
- width: 3rem;
- position: absolute;
- right: 1rem;
- top: 1rem;
text-align: center;
}
.header .item.configure .btn .icon,
@@ -759,7 +745,7 @@ form th {
/*=== Body */
#global {
- height: calc(100% - 3.5rem);
+ height: calc(100vh - (3rem + 2 * var(--frss-padding-top-bottom)));
}
/*=== Prompt (centered) */
@@ -867,13 +853,16 @@ main.prompt {
.nav_menu .stick .btn.read_all:hover {
background-color: #d5d8db;
}
-.nav_menu .stick .dropdown a.dropdown-toggle {
+.nav_menu .stick .dropdown:not(#dropdown-search-wrapper) a.dropdown-toggle {
border-left-width: 0;
background-image: url(icons/more.svg);
}
-.nav_menu .stick .dropdown a.dropdown-toggle .icon {
+.nav_menu .stick .dropdown:not(#dropdown-search-wrapper) a.dropdown-toggle .icon {
display: none;
}
+.nav_menu .stick #dropdown-search-wrapper.dropdown a.dropdown-toggle {
+ border-left-width: 0;
+}
/*=== Content of feed articles */
.content, .content.thin {
@@ -969,9 +958,7 @@ main.prompt {
color: #fff;
}
.notification a.close {
- padding: 0 15px;
border-radius: 0 3px 3px 0;
- line-height: 3em;
}
.notification.good a.close:hover {
background: #0c7540;
@@ -979,12 +966,12 @@ main.prompt {
.notification.bad a.close:hover {
background: #73341f;
}
-.notification#actualizeProgress {
- line-height: 2em;
-}
.notification#actualizeProgress br {
display: none;
}
+.notification#actualizeProgress .title {
+ margin: 0 2rem;
+}
/*=== Navigation menu (for articles) */
#nav_entries {
@@ -1000,10 +987,10 @@ main.prompt {
background: #fff;
transition: all 0.15s ease-in-out;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #f9fafb;
}
-.flux:hover:not(.current):hover .item.title {
+.flux .flux_header:hover:not(.current):hover .item.title {
background: #f9fafb;
}
.flux.current {
@@ -1212,9 +1199,6 @@ main.prompt {
/*=== MOBILE */
/*===========*/
@media (max-width: 840px) {
- .form-group .group-name {
- text-align: left;
- }
.aside {
transition: all 0.2s ease-in-out;
}
@@ -1242,25 +1226,13 @@ main.prompt {
#slider .toggle_aside .icon {
filter: grayscale(100%) brightness(2.5);
}
- .header {
- padding: 0.5rem;
- height: 8rem;
- }
.header .item.search {
- display: block;
- }
- .header .item.search form {
- display: inherit;
- }
- .header .item.search .stick {
- display: flex;
- }
- .header .item.search input {
- width: 90%;
- height: 3.5rem;
+ display: none;
}
- .header .item.search input:focus {
- width: 100%;
+ .header .item.configure {
+ position: absolute;
+ top: 0;
+ right: 0;
}
#global {
height: calc(100% - 8rem);
@@ -1288,10 +1260,6 @@ main.prompt {
.nav_menu .stick .btn.read_all {
padding: 0.85rem 1.25rem;
}
- .nav_menu .search {
- display: none;
- max-width: 97%;
- }
.nav_menu .search .input {
max-width: 97%;
width: 90px;
diff --git a/p/themes/Mapco/mapco.rtl.css b/p/themes/Mapco/mapco.rtl.css
index 21cc44222..b962e5fe0 100644
--- a/p/themes/Mapco/mapco.rtl.css
+++ b/p/themes/Mapco/mapco.rtl.css
@@ -17,7 +17,7 @@
font-style: normal;
font-stretch: normal;
font-weight: 700;
- src: url("../fonts/LatoLatin-Regular.woff") format("woff");
+ src: url("../fonts/LatoLatin-Bold.woff") format("woff");
}
@font-face {
font-family: "lato";
@@ -163,22 +163,16 @@ input:disabled, select:disabled {
background: #eff0f2;
}
-input.extend {
- transition: width 200ms linear;
-}
-
.form-group {
padding: 5px;
border-radius: 3px;
}
.form-group::after {
- content: "";
display: block;
clear: both;
}
.form-group .group-name {
padding: 10px 0;
- text-align: left;
}
.form-group .group-controls {
min-height: 25px;
@@ -200,8 +194,7 @@ table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #d5d8db;
}
@@ -227,6 +220,10 @@ form th {
}
/*=== Dropdown */
+.dropdown .dropdown-target:target + .btn {
+ background-color: #d5d8db;
+}
+
.dropdown-menu {
margin: 9px 0 0 0;
padding: 0.5rem 0 1rem 0;
@@ -241,8 +238,9 @@ form th {
border: none;
left: 18px;
}
-.dropdown-menu .dropdown-header {
- padding: 1rem 1rem 1rem 0.5rem;
+.dropdown-menu .dropdown-header,
+.dropdown-menu .dropdown-section .dropdown-section-title {
+ padding: 1rem 1.5rem;
font-weight: bold;
text-align: right;
color: #5b6871;
@@ -252,21 +250,37 @@ form th {
.dropdown-menu .item {
transition: all 0.075s ease-in-out;
}
-.dropdown-menu .item a, .dropdown-menu .item span, .dropdown-menu .item .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 2rem;
color: #303136;
- font-size: 1rem;
+ font-size: inherit;
line-height: 2.5em;
}
-.dropdown-menu .item:not(.addItem) a:hover,
-.dropdown-menu .item:not(.addItem) button:hover {
+.dropdown-menu .item > a span.icon,
+.dropdown-menu .item > span span.icon,
+.dropdown-menu .item > .as-link span.icon {
+ padding: 0 0.25rem !important;
+}
+.dropdown-menu .item > a:not(.addItem):hover,
+.dropdown-menu .item > .as-link:not(.addItem):hover {
background: #36c;
color: #fff;
}
-.dropdown-menu .item:not(.addItem) a:hover .icon,
-.dropdown-menu .item:not(.addItem) button:hover .icon {
+.dropdown-menu .item > a:not(.addItem):hover .icon,
+.dropdown-menu .item > .as-link:not(.addItem):hover .icon {
filter: brightness(3);
}
+.dropdown-menu .item.dropdown-section {
+ margin-top: 0.75rem;
+}
+.dropdown-menu .item.dropdown-section ~ .dropdown-section {
+ border-top-color: #eff0f2;
+}
+.dropdown-menu .item.dropdown-section .item a, .dropdown-menu .item.dropdown-section .item .as-link {
+ padding-right: 2rem;
+}
.dropdown-menu .item[aria-checked=true] a::before {
margin: 0 -14px 0 0;
font-weight: bold;
@@ -303,8 +317,6 @@ form th {
/*=== Alerts */
.alert {
- margin: 1rem 0;
- padding: 1rem;
background: #f9fafb;
color: #5b6871;
font-size: 1rem;
@@ -530,7 +542,7 @@ form th {
.stick .btn:first-child {
border-radius: 0 5px 5px 0;
}
-.stick .btn:last-child, .stick input:last-child, .stick .btn + .dropdown > .btn {
+.stick .btn:last-child, .stick input:last-child, .stick .dropdown:last-child > .btn {
border-radius: 5px 0 0 5px;
}
.stick .btn + .btn,
@@ -559,33 +571,30 @@ form th {
/* Sidebar des pages de configuration */
/*=== Navigation */
-.nav-list .nav-header,
+.nav-list {
+ font-size: 1rem;
+}
+.nav-list .item.nav-header,
.nav-list .item {
- height: 2.5em;
+ min-height: 2.5em;
line-height: 2.5em;
- font-size: 1rem;
}
.nav-list .item {
background: #303136;
color: #fff;
+ min-height: 2.5em;
+ line-height: 2.5em;
+}
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5em;
}
.nav-list .item a {
padding: 0 1rem;
color: #ffffff;
transition: all 0.15s ease-in-out;
}
-.nav-list .item .error a {
- color: #f5633e;
-}
-.nav-list .item:hover .error a {
- background: #36c;
- color: #ffffff;
-}
-.nav-list .item:hover .empty a {
- background: #f4f762;
- color: #ffffff;
-}
-.nav-list .item:hover a {
+.nav-list .item a:hover {
background: #17181a;
text-decoration: none;
}
@@ -593,24 +602,13 @@ form th {
background: #36c;
color: #fff;
}
-.nav-list .item.active .error a {
- background: #36c;
- color: #fff;
-}
-.nav-list .item.active .empty a {
- background: #f4f762;
- color: #fff;
-}
.nav-list .item.active a {
background: #36c;
color: #fff;
text-decoration: none;
}
-.nav-list.empty a {
- color: #f4f762;
-}
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
color: #5b6871;
text-transform: uppercase;
@@ -680,29 +678,22 @@ form th {
/*===============*/
/*=== Header */
.header {
- padding: 0.5rem 1.35rem;
background: #303136;
- display: block;
- width: auto;
- height: 3.5rem;
- table-layout: none;
-}
-.header .logo {
- margin: 11px 0 5px;
- filter: grayscale(100%) brightness(100);
}
.header .item {
vertical-align: middle;
}
-.header .item.title {
- width: 280px;
- font-weight: 400;
+.header .item.title a {
+ padding: 0.5rem 1rem;
}
-.header .item.title a img {
- margin-left: 0.5rem;
+.header .item.title a .logo {
+ filter: grayscale(100%) brightness(3);
+}
+.header .item.title a:hover .logo {
+ filter: grayscale(100%) brightness(2.2);
}
.header .item.search input {
- width: 230px;
+ width: 350px;
color: #ffffff;
border: none;
border-radius: 0 2px 2px 0;
@@ -713,7 +704,6 @@ form th {
background-color: #17181a;
}
.header .item.search input:focus {
- width: 350px;
color: #5b6871;
background-color: #fff;
}
@@ -739,10 +729,6 @@ form th {
filter: brightness(3);
}
.header .item.configure {
- width: 3rem;
- position: absolute;
- left: 1rem;
- top: 1rem;
text-align: center;
}
.header .item.configure .btn .icon,
@@ -759,7 +745,7 @@ form th {
/*=== Body */
#global {
- height: calc(100% - 3.5rem);
+ height: calc(100vh - (3rem + 2 * var(--frss-padding-top-bottom)));
}
/*=== Prompt (centered) */
@@ -867,13 +853,16 @@ main.prompt {
.nav_menu .stick .btn.read_all:hover {
background-color: #d5d8db;
}
-.nav_menu .stick .dropdown a.dropdown-toggle {
+.nav_menu .stick .dropdown:not(#dropdown-search-wrapper) a.dropdown-toggle {
border-right-width: 0;
background-image: url(icons/more.svg);
}
-.nav_menu .stick .dropdown a.dropdown-toggle .icon {
+.nav_menu .stick .dropdown:not(#dropdown-search-wrapper) a.dropdown-toggle .icon {
display: none;
}
+.nav_menu .stick #dropdown-search-wrapper.dropdown a.dropdown-toggle {
+ border-right-width: 0;
+}
/*=== Content of feed articles */
.content, .content.thin {
@@ -969,9 +958,7 @@ main.prompt {
color: #fff;
}
.notification a.close {
- padding: 0 15px;
border-radius: 3px 0 0 3px;
- line-height: 3em;
}
.notification.good a.close:hover {
background: #0c7540;
@@ -979,12 +966,12 @@ main.prompt {
.notification.bad a.close:hover {
background: #73341f;
}
-.notification#actualizeProgress {
- line-height: 2em;
-}
.notification#actualizeProgress br {
display: none;
}
+.notification#actualizeProgress .title {
+ margin: 0 2rem;
+}
/*=== Navigation menu (for articles) */
#nav_entries {
@@ -1000,10 +987,10 @@ main.prompt {
background: #fff;
transition: all 0.15s ease-in-out;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #f9fafb;
}
-.flux:hover:not(.current):hover .item.title {
+.flux .flux_header:hover:not(.current):hover .item.title {
background: #f9fafb;
}
.flux.current {
@@ -1212,9 +1199,6 @@ main.prompt {
/*=== MOBILE */
/*===========*/
@media (max-width: 840px) {
- .form-group .group-name {
- text-align: right;
- }
.aside {
transition: all 0.2s ease-in-out;
}
@@ -1242,25 +1226,13 @@ main.prompt {
#slider .toggle_aside .icon {
filter: grayscale(100%) brightness(2.5);
}
- .header {
- padding: 0.5rem;
- height: 8rem;
- }
.header .item.search {
- display: block;
- }
- .header .item.search form {
- display: inherit;
- }
- .header .item.search .stick {
- display: flex;
- }
- .header .item.search input {
- width: 90%;
- height: 3.5rem;
+ display: none;
}
- .header .item.search input:focus {
- width: 100%;
+ .header .item.configure {
+ position: absolute;
+ top: 0;
+ left: 0;
}
#global {
height: calc(100% - 8rem);
@@ -1288,10 +1260,6 @@ main.prompt {
.nav_menu .stick .btn.read_all {
padding: 0.85rem 1.25rem;
}
- .nav_menu .search {
- display: none;
- max-width: 97%;
- }
.nav_menu .search .input {
max-width: 97%;
width: 90px;
diff --git a/p/themes/Nord/nord.css b/p/themes/Nord/nord.css
index 9de1f0189..403a27dd0 100644
--- a/p/themes/Nord/nord.css
+++ b/p/themes/Nord/nord.css
@@ -16,6 +16,8 @@
--alert: #a3be8c;
--alert-bg: #8fbcbb;
--code-bg: #2e3440;
+
+ --frss-background-color-transparent: #2e34407f;
}
@@ -95,10 +97,6 @@ button.as-link[disabled] {
}
/*=== Tables */
-tr, th, td {
- padding: 0.5em;
-}
-
form td,
form th {
font-weight: normal;
@@ -138,7 +136,6 @@ table td span {
.form-group .group-name {
padding: 10px 0;
- text-align: right;
}
.form-group .group-controls {
@@ -177,7 +174,7 @@ a.btn {
line-height: 25px;
}
-.btn-important, .read_all, #actualize {
+.btn-important, #nav_menu_read_all .read_all, #actualize {
font-weight: bold !important;
background-color: var(--accent) !important;
color: var(--bg) !important;
@@ -191,17 +188,29 @@ a.btn {
opacity: .8;
cursor: pointer;
}
-/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- font-size: 0.9rem;
- line-height: 2.5em;
+
+.stick input {
+ margin: 0 5px 0 0;
}
+.stick .btn {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.stick {
+ margin: 0 5px;
+}
+
+.header .stick,
+.header .btn {
+ margin: 0
+}
+
+/*=== Navigation */
.dropdown-menu {
margin: 5px 0 0;
- padding: 5px 0;
+ padding: 0.5rem 0 0.25rem 0;
background: var(--accent-bg);
font-size: 0.8rem;
border: 1px solid var(--border);
@@ -209,21 +218,39 @@ a.btn {
text-align: left;
}
-.dropdown-header {
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
padding: 0 5px 5px;
font-weight: bold;
text-align: left;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
color: var(--text);
line-height: 2.5em;
+ font-size: inherit;
min-width: 200px;
}
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-left: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
+ /* no hover color */
+}
+
.dropdown-menu > .item[aria-checked="true"] > a::before {
font-weight: bold;
margin: 0 0 0 -14px;
@@ -279,14 +306,13 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: var(--border);
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
font-size: 0.9em;
border-radius: 6px;
}
@@ -355,7 +381,6 @@ img.favicon {
.box .box-content .item {
padding: 0 10px;
font-size: 0.9rem;
- line-height: 2.5em;
}
/*=== Draggable */
@@ -383,10 +408,8 @@ img.favicon {
}
.tree-folder-items > .item {
- padding: 0 10px;
color: var(--text);
font-size: 0.8rem;
- line-height: 2.5rem;
}
.tree-folder-items > .item > a {
@@ -412,16 +435,10 @@ img.favicon {
/*===============*/
/*=== Header */
.header > .item {
- padding: 10px;
vertical-align: middle;
text-align: center;
}
-
-.header > .item.title {
- width: 230px;
-}
-
.header > .item.title h1 {
margin: 0.5em 0;
}
@@ -431,10 +448,6 @@ img.favicon {
}
.header > .item.search input {
- width: 230px;
-}
-
-.header .item.search input:focus {
width: 350px;
}
@@ -442,6 +455,10 @@ img.favicon {
filter: grayscale(100%) brightness(2.5);
}
+.header > .item.title a:hover .logo {
+ filter: grayscale(85%) brightness(2.5);
+}
+
/*=== Body */
.aside {
background-color: var(--accent-bg);
@@ -495,10 +512,6 @@ img.favicon {
}
-.category .title:not([data-unread="0"])::after {
- content: attr(data-unread);
-}
-
li.item.active {
background-color: var(--bg);
font-weight: bold;
@@ -664,16 +677,6 @@ li.item.active {
vertical-align: middle;
}
-
-.notification a.close {
- padding: 0 15px;
- line-height: 3em;
-}
-
-.notification#actualizeProgress {
- line-height: 2em;
-}
-
.notification.closed {
opacity: 0;
visibility: hidden;
@@ -735,7 +738,6 @@ li.item.active {
/*===========*/
.category .title.error::before {
color: var(--accent-light);
- content: "⚠ ";
}
@@ -744,105 +746,6 @@ li.item.active {
text-align: center;
}
-/*=== MOBILE */
-/*===========*/
-
-@media (max-width: 840px) {
- .aside:target + .close-aside {
- background: rgba(0, 0, 0, 0.2);
- display: block;
- font-size: 0;
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- cursor: pointer;
- z-index: 99;
- }
-
- .nav_mobile {
- display: block;
- }
-
- .aside {
- position: fixed;
- top: 0; bottom: 0;
- left: 0;
- width: 0;
- overflow: hidden;
- z-index: 100;
- }
-
- .aside:target,
- .reader .aside:target {
- width: 90%;
- height: 100vh;
- }
-
- .aside_feed .configure-feeds {
- margin-top: 10px;
- }
-
- .flux_header .item.website {
- width: 40px;
- }
-
- .flux:not(.current):hover .item.title {
- position: relative;
- width: auto;
- white-space: nowrap;
- }
-
- .notification {
- top: 0;
- left: 0;
- right: 0;
- }
-
- #nav_entries {
- width: 100%;
- }
-
- #panel {
- top: 25px; bottom: 30px;
- left: 0; right: 0;
- }
-
- #panel .close {
- top: 0; right: 0;
- left: auto; bottom: auto;
- display: inline-block;
- width: 30px;
- height: 30px;
- }
-
- #slider.active {
- left: 0;
- top: 50px;
- background-color: var(--bg);
- }
-
- #close-slider img {
- display: initial;
- }
-
- #close-slider.active {
- background: var(--bg);
- display: block;
- width: 100%;
- height: 50px;
- z-index: 10;
- text-align: center;
- line-height: 50px;
- border-bottom: 1px solid #ddd;
- }
-
- .stat.half {
- grid-column: 1 / span 2;
- }
-}
-
/*=== PRINTER */
/*============*/
@@ -874,11 +777,6 @@ li.item.active {
.flux_content .content a {
color: #000;
}
-
- .flux_content .content a::after {
- content: " [" attr(href) "] ";
- font-style: italic;
- }
}
/*=== PREVIEW */
@@ -914,15 +812,6 @@ li.item.active {
margin: -1px;
}
-.feed .item-title:not([data-unread="0"])::before {
- content: "(" attr(data-unread) ") ";
- display: none
-}
-
-.feed .item-title:not([data-unread="0"])::after {
- content: " (" attr(data-unread) ")";
-}
-
/*BEGINS BASE.CSS*/
@@ -940,17 +829,10 @@ textarea {
height: 100px;
}
-
option {
padding: 0 .5em;
}
-
-input.extend {
- transition: width 200ms linear;
-}
-
-
/*=== COMPONENTS */
/*===============*/
/*=== Forms */
@@ -959,8 +841,18 @@ input.extend {
}
/*=== Navigation */
+.nav-list {
+ font-size: 0.9rem;
+}
+
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
}
.nav-list a:hover {
@@ -968,7 +860,7 @@ input.extend {
}
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
}
@@ -979,7 +871,6 @@ input.extend {
/*=== Dropdown */
.dropdown-menu::after {
- content: "";
position: absolute;
top: -6px;
right: 13px;
@@ -1088,7 +979,30 @@ input.extend {
/*===========*/
@media (max-width: 840px) {
+ .aside:target + .close-aside {
+ background: rgba(0, 0, 0, 0.2);
+ display: block;
+ font-size: 0;
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ cursor: pointer;
+ z-index: 99;
+ }
+
+ .nav_mobile {
+ display: block;
+ }
+
.aside {
+ position: fixed;
+ top: 0; bottom: 0;
+ left: 0;
+ width: 0;
+ overflow: hidden;
+ z-index: 100;
transition: width 200ms linear;
}
@@ -1096,6 +1010,78 @@ input.extend {
padding: 0;
}
+ .aside:target,
+ .reader .aside:target {
+ width: 90%;
+ height: 100vh;
+ }
+
+ .aside_feed .configure-feeds {
+ margin-top: 10px;
+ }
+
+ .flux_header .item.website {
+ width: 40px;
+ }
+
+ .flux:not(.current):hover .item.title {
+ position: relative;
+ width: auto;
+ white-space: nowrap;
+ }
+
+ .flux .website .favicon {
+ position: relative;
+ }
+
+ .notification {
+ top: 0;
+ left: 0;
+ right: 0;
+ }
+
+ #nav_entries {
+ width: 100%;
+ }
+
+ #panel {
+ top: 25px; bottom: 30px;
+ left: 0; right: 0;
+ }
+
+ #panel .close {
+ top: 0; right: 0;
+ left: auto; bottom: auto;
+ display: inline-block;
+ width: 30px;
+ height: 30px;
+ }
+
+ #slider.active {
+ left: 0;
+ top: 50px;
+ background-color: var(--bg);
+ }
+
+ #close-slider img {
+ display: initial;
+ }
+
+ #close-slider.active {
+ background: var(--bg);
+ display: block;
+ width: 100%;
+ height: 50px;
+ z-index: 10;
+ text-align: center;
+ line-height: 50px;
+ border-bottom: 1px solid #ddd;
+ }
+
+ .stat.half {
+ grid-column: 1 / span 2;
+ }
+
.nav_menu .btn {
margin: 5px 10px;
}
@@ -1122,6 +1108,10 @@ input.extend {
width: 400px;
}
+ .post {
+ padding: 1rem;
+ }
+
.day .name {
font-size: 1.1rem;
}
diff --git a/p/themes/Nord/nord.rtl.css b/p/themes/Nord/nord.rtl.css
index 52bdd752d..2ae7464fa 100644
--- a/p/themes/Nord/nord.rtl.css
+++ b/p/themes/Nord/nord.rtl.css
@@ -16,6 +16,8 @@
--alert: #a3be8c;
--alert-bg: #8fbcbb;
--code-bg: #2e3440;
+
+ --frss-background-color-transparent: #2e34407f;
}
@@ -95,10 +97,6 @@ button.as-link[disabled] {
}
/*=== Tables */
-tr, th, td {
- padding: 0.5em;
-}
-
form td,
form th {
font-weight: normal;
@@ -138,7 +136,6 @@ table td span {
.form-group .group-name {
padding: 10px 0;
- text-align: left;
}
.form-group .group-controls {
@@ -177,7 +174,7 @@ a.btn {
line-height: 25px;
}
-.btn-important, .read_all, #actualize {
+.btn-important, #nav_menu_read_all .read_all, #actualize {
font-weight: bold !important;
background-color: var(--accent) !important;
color: var(--bg) !important;
@@ -191,17 +188,29 @@ a.btn {
opacity: .8;
cursor: pointer;
}
-/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- font-size: 0.9rem;
- line-height: 2.5em;
+
+.stick input {
+ margin: 0 0 0 5px;
}
+.stick .btn {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.stick {
+ margin: 0 5px;
+}
+
+.header .stick,
+.header .btn {
+ margin: 0
+}
+
+/*=== Navigation */
.dropdown-menu {
margin: 5px 0 0;
- padding: 5px 0;
+ padding: 0.5rem 0 0.25rem 0;
background: var(--accent-bg);
font-size: 0.8rem;
border: 1px solid var(--border);
@@ -209,21 +218,39 @@ a.btn {
text-align: right;
}
-.dropdown-header {
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
padding: 0 5px 5px;
font-weight: bold;
text-align: right;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
color: var(--text);
line-height: 2.5em;
+ font-size: inherit;
min-width: 200px;
}
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-right: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
+ /* no hover color */
+}
+
.dropdown-menu > .item[aria-checked="true"] > a::before {
font-weight: bold;
margin: 0 -14px 0 0;
@@ -279,14 +306,13 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: var(--border);
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
font-size: 0.9em;
border-radius: 6px;
}
@@ -355,7 +381,6 @@ img.favicon {
.box .box-content .item {
padding: 0 10px;
font-size: 0.9rem;
- line-height: 2.5em;
}
/*=== Draggable */
@@ -383,10 +408,8 @@ img.favicon {
}
.tree-folder-items > .item {
- padding: 0 10px;
color: var(--text);
font-size: 0.8rem;
- line-height: 2.5rem;
}
.tree-folder-items > .item > a {
@@ -412,16 +435,10 @@ img.favicon {
/*===============*/
/*=== Header */
.header > .item {
- padding: 10px;
vertical-align: middle;
text-align: center;
}
-
-.header > .item.title {
- width: 230px;
-}
-
.header > .item.title h1 {
margin: 0.5em 0;
}
@@ -431,10 +448,6 @@ img.favicon {
}
.header > .item.search input {
- width: 230px;
-}
-
-.header .item.search input:focus {
width: 350px;
}
@@ -442,6 +455,10 @@ img.favicon {
filter: grayscale(100%) brightness(2.5);
}
+.header > .item.title a:hover .logo {
+ filter: grayscale(85%) brightness(2.5);
+}
+
/*=== Body */
.aside {
background-color: var(--accent-bg);
@@ -495,10 +512,6 @@ img.favicon {
}
-.category .title:not([data-unread="0"])::after {
- content: attr(data-unread);
-}
-
li.item.active {
background-color: var(--bg);
font-weight: bold;
@@ -664,16 +677,6 @@ li.item.active {
vertical-align: middle;
}
-
-.notification a.close {
- padding: 0 15px;
- line-height: 3em;
-}
-
-.notification#actualizeProgress {
- line-height: 2em;
-}
-
.notification.closed {
opacity: 0;
visibility: hidden;
@@ -735,7 +738,6 @@ li.item.active {
/*===========*/
.category .title.error::before {
color: var(--accent-light);
- content: "⚠ ";
}
@@ -744,105 +746,6 @@ li.item.active {
text-align: center;
}
-/*=== MOBILE */
-/*===========*/
-
-@media (max-width: 840px) {
- .aside:target + .close-aside {
- background: rgba(0, 0, 0, 0.2);
- display: block;
- font-size: 0;
- position: fixed;
- top: 0;
- bottom: 0;
- right: 0;
- left: 0;
- cursor: pointer;
- z-index: 99;
- }
-
- .nav_mobile {
- display: block;
- }
-
- .aside {
- position: fixed;
- top: 0; bottom: 0;
- right: 0;
- width: 0;
- overflow: hidden;
- z-index: 100;
- }
-
- .aside:target,
- .reader .aside:target {
- width: 90%;
- height: 100vh;
- }
-
- .aside_feed .configure-feeds {
- margin-top: 10px;
- }
-
- .flux_header .item.website {
- width: 40px;
- }
-
- .flux:not(.current):hover .item.title {
- position: relative;
- width: auto;
- white-space: nowrap;
- }
-
- .notification {
- top: 0;
- right: 0;
- left: 0;
- }
-
- #nav_entries {
- width: 100%;
- }
-
- #panel {
- top: 25px; bottom: 30px;
- right: 0; left: 0;
- }
-
- #panel .close {
- top: 0; left: 0;
- right: auto; bottom: auto;
- display: inline-block;
- width: 30px;
- height: 30px;
- }
-
- #slider.active {
- right: 0;
- top: 50px;
- background-color: var(--bg);
- }
-
- #close-slider img {
- display: initial;
- }
-
- #close-slider.active {
- background: var(--bg);
- display: block;
- width: 100%;
- height: 50px;
- z-index: 10;
- text-align: center;
- line-height: 50px;
- border-bottom: 1px solid #ddd;
- }
-
- .stat.half {
- grid-column: 1 / span 2;
- }
-}
-
/*=== PRINTER */
/*============*/
@@ -874,11 +777,6 @@ li.item.active {
.flux_content .content a {
color: #000;
}
-
- .flux_content .content a::after {
- content: " [" attr(href) "] ";
- font-style: italic;
- }
}
/*=== PREVIEW */
@@ -914,15 +812,6 @@ li.item.active {
margin: -1px;
}
-.feed .item-title:not([data-unread="0"])::before {
- content: "(" attr(data-unread) ") ";
- display: none
-}
-
-.feed .item-title:not([data-unread="0"])::after {
- content: " (" attr(data-unread) ")";
-}
-
/*BEGINS BASE.CSS*/
@@ -940,17 +829,10 @@ textarea {
height: 100px;
}
-
option {
padding: 0 .5em;
}
-
-input.extend {
- transition: width 200ms linear;
-}
-
-
/*=== COMPONENTS */
/*===============*/
/*=== Forms */
@@ -959,8 +841,18 @@ input.extend {
}
/*=== Navigation */
+.nav-list {
+ font-size: 0.9rem;
+}
+
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
}
.nav-list a:hover {
@@ -968,7 +860,7 @@ input.extend {
}
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
font-weight: bold;
}
@@ -979,7 +871,6 @@ input.extend {
/*=== Dropdown */
.dropdown-menu::after {
- content: "";
position: absolute;
top: -6px;
left: 13px;
@@ -1088,7 +979,30 @@ input.extend {
/*===========*/
@media (max-width: 840px) {
+ .aside:target + .close-aside {
+ background: rgba(0, 0, 0, 0.2);
+ display: block;
+ font-size: 0;
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ cursor: pointer;
+ z-index: 99;
+ }
+
+ .nav_mobile {
+ display: block;
+ }
+
.aside {
+ position: fixed;
+ top: 0; bottom: 0;
+ right: 0;
+ width: 0;
+ overflow: hidden;
+ z-index: 100;
transition: width 200ms linear;
}
@@ -1096,6 +1010,78 @@ input.extend {
padding: 0;
}
+ .aside:target,
+ .reader .aside:target {
+ width: 90%;
+ height: 100vh;
+ }
+
+ .aside_feed .configure-feeds {
+ margin-top: 10px;
+ }
+
+ .flux_header .item.website {
+ width: 40px;
+ }
+
+ .flux:not(.current):hover .item.title {
+ position: relative;
+ width: auto;
+ white-space: nowrap;
+ }
+
+ .flux .website .favicon {
+ position: relative;
+ }
+
+ .notification {
+ top: 0;
+ right: 0;
+ left: 0;
+ }
+
+ #nav_entries {
+ width: 100%;
+ }
+
+ #panel {
+ top: 25px; bottom: 30px;
+ right: 0; left: 0;
+ }
+
+ #panel .close {
+ top: 0; left: 0;
+ right: auto; bottom: auto;
+ display: inline-block;
+ width: 30px;
+ height: 30px;
+ }
+
+ #slider.active {
+ right: 0;
+ top: 50px;
+ background-color: var(--bg);
+ }
+
+ #close-slider img {
+ display: initial;
+ }
+
+ #close-slider.active {
+ background: var(--bg);
+ display: block;
+ width: 100%;
+ height: 50px;
+ z-index: 10;
+ text-align: center;
+ line-height: 50px;
+ border-bottom: 1px solid #ddd;
+ }
+
+ .stat.half {
+ grid-column: 1 / span 2;
+ }
+
.nav_menu .btn {
margin: 5px 10px;
}
@@ -1122,6 +1108,10 @@ input.extend {
width: 400px;
}
+ .post {
+ padding: 1rem;
+ }
+
.day .name {
font-size: 1.1rem;
}
diff --git a/p/themes/Origine-compact/origine-compact.css b/p/themes/Origine-compact/origine-compact.css
index 880af2433..d94b09fb7 100644
--- a/p/themes/Origine-compact/origine-compact.css
+++ b/p/themes/Origine-compact/origine-compact.css
@@ -2,10 +2,8 @@
/*=== GENERAL */
/*============*/
-input, select, textarea {
- padding: 3px 5px 2px 5px;
- min-height: 25px;
- line-height: 2;
+:root {
+ --frss-padding-top-bottom: 0.125rem;
}
/*=== COMPONENTS */
@@ -41,73 +39,45 @@ a.btn,
font-size: 0.9rem;
}
-.horizontal-list .item {
- line-height: 2.2;
-}
-
-.horizontal-list .item .item-element {
- padding: 1px 0 0 0;
-}
-
/*=== Dropdown */
-.item ~ .dropdown-header,
-.item.separator {
- border-top-color: #ddd;
-}
-
/*=== Alerts */
/*=== Pagination */
/*=== Boxes */
/*=== Tree */
-.tree-folder-title {
- padding: 0 5px;
- line-height: 2.2;
- font-size: 0.9rem;
-}
-
-.tree-folder-items > .item {
- line-height: 2.4;
-}
-
/*=== STRUCTURE */
/*===============*/
/*=== Header */
.header {
- height: 40px;
-}
-
-.header > .item {
- padding: 0px;
+ /* search bar and config button height = 2.1rem */
+ height: calc(2.1rem + 2 * var(--frss-padding-top-bottom));
}
-.header .item.configure .btn,
-.header .item.search .btn {
- min-height: 18px;
- padding: 4px 10px;
- line-height: 1.4;
+.header > .item.title a {
+ padding: 0 1rem;
}
.header > .item.title .logo {
- height: 25px;
-}
-
-.header > .item.search input {
- padding: 1px 5px;
+ /* logo is smaller than needed */
+ height: 1.5rem;
}
/*=== Body */
#global {
- height: calc(100vh - 40px);
+ height: calc(100vh - (calc(2.1rem + 2 * var(--frss-padding-top-bottom))))
}
/*=== Aside main page (categories) */
.aside.aside_feed .category .title:not([data-unread="0"])::after,
.global .box.category .title:not([data-unread="0"])::after {
- margin: 0.4em 0 0 0;
+ font-size: 0.8rem;
+}
+
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ margin-top: 0.125rem;
}
.aside.aside_feed .feed .item-title:not([data-unread="0"])::after {
- margin: 0.5em 0 0 0;
+ font-size: 0.7rem;
}
/*=== Day indication */
@@ -123,10 +93,6 @@ a.btn,
/*=== Index menu */
/*=== Feed articles */
-.flux .item {
- padding: 0;
-}
-
.flux .item.thumbnail {
padding: 5px;
height: 50px;
@@ -169,14 +135,6 @@ a.btn,
}
/*=== Content of feed articles */
-.content {
- padding: 10px 10px;
-}
-
-#stream.normal .content > h1.title {
- display: none;
-}
-
/*=== Notification and actualize notification */
/*=== "Load more" part */
#bigMarkAsRead {
@@ -188,10 +146,6 @@ a.btn,
}
/*=== Navigation menu (for articles) */
-#nav_entries {
- line-height: 2.2;
-}
-
/*=== READER VIEW */
/*================*/
/*=== GLOBAL VIEW */
@@ -213,7 +167,7 @@ a.btn,
}
.post {
- padding-left: 15px;
- padding-right: 15px;
+ padding-left: 1rem;
+ padding-right: 1rem;
}
}
diff --git a/p/themes/Origine-compact/origine-compact.rtl.css b/p/themes/Origine-compact/origine-compact.rtl.css
index cfa978650..c98c5f6da 100644
--- a/p/themes/Origine-compact/origine-compact.rtl.css
+++ b/p/themes/Origine-compact/origine-compact.rtl.css
@@ -2,10 +2,8 @@
/*=== GENERAL */
/*============*/
-input, select, textarea {
- padding: 3px 5px 2px 5px;
- min-height: 25px;
- line-height: 2;
+:root {
+ --frss-padding-top-bottom: 0.125rem;
}
/*=== COMPONENTS */
@@ -41,73 +39,45 @@ a.btn,
font-size: 0.9rem;
}
-.horizontal-list .item {
- line-height: 2.2;
-}
-
-.horizontal-list .item .item-element {
- padding: 1px 0 0 0;
-}
-
/*=== Dropdown */
-.item ~ .dropdown-header,
-.item.separator {
- border-top-color: #ddd;
-}
-
/*=== Alerts */
/*=== Pagination */
/*=== Boxes */
/*=== Tree */
-.tree-folder-title {
- padding: 0 5px;
- line-height: 2.2;
- font-size: 0.9rem;
-}
-
-.tree-folder-items > .item {
- line-height: 2.4;
-}
-
/*=== STRUCTURE */
/*===============*/
/*=== Header */
.header {
- height: 40px;
-}
-
-.header > .item {
- padding: 0px;
+ /* search bar and config button height = 2.1rem */
+ height: calc(2.1rem + 2 * var(--frss-padding-top-bottom));
}
-.header .item.configure .btn,
-.header .item.search .btn {
- min-height: 18px;
- padding: 4px 10px;
- line-height: 1.4;
+.header > .item.title a {
+ padding: 0 1rem;
}
.header > .item.title .logo {
- height: 25px;
-}
-
-.header > .item.search input {
- padding: 1px 5px;
+ /* logo is smaller than needed */
+ height: 1.5rem;
}
/*=== Body */
#global {
- height: calc(100vh - 40px);
+ height: calc(100vh - (calc(2.1rem + 2 * var(--frss-padding-top-bottom))))
}
/*=== Aside main page (categories) */
.aside.aside_feed .category .title:not([data-unread="0"])::after,
.global .box.category .title:not([data-unread="0"])::after {
- margin: 0.4em 0 0 0;
+ font-size: 0.8rem;
+}
+
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ margin-top: 0.125rem;
}
.aside.aside_feed .feed .item-title:not([data-unread="0"])::after {
- margin: 0.5em 0 0 0;
+ font-size: 0.7rem;
}
/*=== Day indication */
@@ -123,10 +93,6 @@ a.btn,
/*=== Index menu */
/*=== Feed articles */
-.flux .item {
- padding: 0;
-}
-
.flux .item.thumbnail {
padding: 5px;
height: 50px;
@@ -169,14 +135,6 @@ a.btn,
}
/*=== Content of feed articles */
-.content {
- padding: 10px 10px;
-}
-
-#stream.normal .content > h1.title {
- display: none;
-}
-
/*=== Notification and actualize notification */
/*=== "Load more" part */
#bigMarkAsRead {
@@ -188,10 +146,6 @@ a.btn,
}
/*=== Navigation menu (for articles) */
-#nav_entries {
- line-height: 2.2;
-}
-
/*=== READER VIEW */
/*================*/
/*=== GLOBAL VIEW */
@@ -213,7 +167,7 @@ a.btn,
}
.post {
- padding-right: 15px;
- padding-left: 15px;
+ padding-right: 1rem;
+ padding-left: 1rem;
}
}
diff --git a/p/themes/Origine/origine.css b/p/themes/Origine/origine.css
index 4b41e1434..3a7d395a0 100644
--- a/p/themes/Origine/origine.css
+++ b/p/themes/Origine/origine.css
@@ -3,6 +3,8 @@
/*=== GENERAL */
/*============*/
:root {
+ --frss-padding-top-bottom: 0.5rem;
+
--background-color-light-gradient: #eee;
--background-color-light: #fff;
--background-color-light-shadowed: #f6f6f6;
@@ -10,8 +12,10 @@
--background-color-hover: #f6f6f6;
--unread-article-background-color: #fff3ed;
+ --unread-article-background-color-hover: #faeee8;
--unread-article-border-color: #ff5300;
--favorite-article-background-color: #fff6da;
+ --favorite-article-background-color-hover: #fcf2d6;
--favorite-article-border-color: #ffc300;
--contrast-background-color: #0084cc;
@@ -147,17 +151,12 @@ input:disabled, select:disabled {
background-color: var(--background-color-light-shadowed);
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid var(--border-color);
}
@@ -187,7 +186,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: right;
}
.form-group .group-controls {
@@ -218,7 +216,8 @@ form th {
}
.stick .btn:first-child,
-.stick input:first-child {
+.stick input:first-child,
+.stick select:first-child {
border-radius: 3px 0 0 3px;
}
@@ -235,6 +234,7 @@ form th {
.stick .btn + input,
.stick .btn + .dropdown > .btn,
.stick input + .btn,
+.stick select + .btn,
.stick input + input,
.stick input + .dropdown > .btn,
.stick .dropdown + .btn,
@@ -293,6 +293,10 @@ a:hover .icon {
filter: brightness(1.1);
}
+#toggle-search.active > .icon {
+ filter: invert(8%) sepia(99%) saturate(7064%) hue-rotate(248deg) brightness(99%) contrast(142%);
+}
+
.btn.active,
.btn:active,
.dropdown-target:target ~ .btn.dropdown-toggle {
@@ -351,21 +355,22 @@ a:hover .icon {
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5;
+.nav-list {
font-size: 0.9rem;
}
-.nav-list .item:hover {
- background-color: var(--background-color-hover);
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
}
-.nav-list .item:hover a {
+.nav-list .nav-section .item:hover a {
+ background-color: var(--background-color-hover);
color: var(--font-color-link-hover);
}
+.nav-list .nav-section .item.active:hover a,
.nav-list .item.active {
background-color: var(--contrast-background-color-active);
color: var(--font-color-light);
@@ -376,8 +381,8 @@ a:hover .icon {
}
.nav-list .item > a,
-.nav-list .item > span {
- padding: 0 10px;
+.nav-list .item > div {
+ padding: 0 1rem;
}
.nav-list .item > span {
@@ -389,26 +394,7 @@ a:hover .icon {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: var(--empty-feed-color);
-}
-
-.nav-list .item.active.empty a {
- background-color: var(--empty-feed-color);
- color: var(--font-color-light);
-}
-
-.nav-list .item.error a {
- color: var(--error-feed-color);
-}
-
-.nav-list .item.active.error a {
- background-color: var(--error-feed-color);
- color: var(--font-color-light);
-}
-
.nav-list .nav-header {
- padding: 0 10px;
background-color: var(--background-color-grey);
color: var(--font-color-grey);
border-bottom: 1px solid var(--border-color);
@@ -436,33 +422,45 @@ a:hover .icon {
border-color: var(--border-color);
}
-.dropdown-header {
- padding: 0 5px 5px;
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
+ padding: 0.25rem 0.5rem 0.25rem 1rem;
color: var(--font-color-grey);
font-weight: bold;
text-align: left;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5;
- font-size: 0.8rem;
+ font-size: inherit;
+}
+
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-left: 2rem;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover,
-.dropdown-menu > .item > label:hover:not(.noHover) {
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background-color: var(--contrast-background-color-active);
color: var(--font-color-light);
}
-.dropdown-menu > .item > label {
+.dropdown-menu .item > label {
padding: 0;
}
-.dropdown-menu > .item:hover .icon {
+.dropdown-menu > .item:hover > a > .icon,
+.dropdown-menu .item.dropdown-section .item:hover .icon {
filter: grayscale(100%) brightness(2.5);
}
@@ -479,14 +477,13 @@ a:hover .icon {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: var(--border-color);
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background-color: var(--background-color-grey);
color: var(--font-color-grey);
font-size: 0.9em;
@@ -567,7 +564,7 @@ a:hover .icon {
}
.box .box-content .item {
- padding: 0.5rem 0 0.25rem 0;
+ padding-bottom: 0.25rem;
font-size: 0.9rem;
line-height: 1.5;
}
@@ -579,9 +576,9 @@ a:hover .icon {
.tree-folder-title {
position: relative;
- padding: 0 10px;
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
background-color: var(--background-color-light);
- line-height: 2.5;
font-size: 1rem;
}
@@ -615,7 +612,7 @@ a:hover .icon {
.tree-folder-items > .item {
padding: 0 10px;
- line-height: 3.125;
+ line-height: 1.7;
font-size: 0.8rem;
}
@@ -643,35 +640,28 @@ a:hover .icon {
/*===============*/
/*=== Header */
.header {
- height: 4rem;
background-color: var(--background-color-grey);
}
.header > .item {
- padding: 10px;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
text-align: center;
}
.header > .item.title {
- padding: 10px 0;
width: 300px;
}
-.header > .item.search input {
- width: 230px;
+.header > .item.title a:hover .logo {
+ filter: brightness(1.4);
}
-.header .item.search input:focus {
+.header > .item.search input {
width: 350px;
}
/*=== Body */
-#global {
- height: calc(100vh - 4rem);
-}
-
.aside {
background-color: var(--background-color-light);
border-right: 1px solid var(--border-color);
@@ -689,7 +679,8 @@ a:hover .icon {
/*=== Aside main page (categories) */
.aside.aside_feed .category .title:not([data-unread="0"])::after,
-.global .box.category .title:not([data-unread="0"])::after {
+.global .box.category .title:not([data-unread="0"])::after,
+.global .feed .item-title:not([data-unread="0"])::after {
background-color: var(--background-color-light-shadowed);
color: var(--font-color-grey);
}
@@ -781,7 +772,6 @@ a:hover .icon {
}
#new-article > a {
- padding: 1em;
color: var(--font-color-light);
font-weight: bold;
}
@@ -820,10 +810,8 @@ a:hover .icon {
/*=== Index menu */
.nav_menu {
- padding: 0.5rem 0;
background-color: var(--background-color-light-shadowed);
border-bottom: 1px solid var(--border-color);
- text-align: center;
}
/*=== Feed articles */
@@ -832,39 +820,47 @@ a:hover .icon {
border-left: 2px solid transparent;
}
-.flux:hover:not(.active) {
- background-color: var(--background-color-hover) !important;
+.flux.current {
+ background-color: var(--background-color-light);
+ border-left: 2px solid var(--contrast-border-color-active);
}
-.flux:not(.current):hover .item.title {
- background: inherit;
+.flux .flux_header:hover,
+.flux .flux_header:hover .item {
+ background-color: var(--background-color-hover);
}
-.flux.current {
- background-color: var(--background-color-light);
- border-left: 2px solid var(--contrast-border-color-active);
+.flux .flux_header:not(.current):hover .flux_header,
+.flux .flux_header:not(.current):hover .flux_header .item {
+ background-color: var(--background-color-hover);
}
.flux.not_read {
border-left: 2px solid var(--unread-article-border-color);
}
-.flux.not_read:not(.current) {
+.flux.not_read .flux_header .item {
background-color: var(--unread-article-background-color);
}
-.flux.not_read:not(.current):hover .item.title {
- background: inherit;
+.flux.not_read:not(.current):hover .flux_header,
+.flux.not_read:not(.current):hover .flux_header .item {
+ background-color: var(--unread-article-background-color-hover);
}
.flux.favorite {
border-left: 2px solid var(--favorite-article-border-color);
}
-.flux.favorite:not(.current) {
+.flux.favorite:not(.current) .flux_header .item {
background-color: var(--favorite-article-background-color);
}
+.flux.favorite:not(.current):hover .flux_header,
+.flux.favorite:not(.current):hover .flux_header .item {
+ background-color: var(--favorite-article-background-color-hover);
+}
+
.flux_header {
font-size: 0.9rem;
border-top: 1px solid var(--border-color);
@@ -882,10 +878,6 @@ a:hover .icon {
}
/*=== Content of feed articles */
-.content {
- padding: 20px 10px;
-}
-
.content h1.title > a {
color: var(--font-color);
}
@@ -963,15 +955,6 @@ a:hover .icon {
border: 1px solid var(--notification-bad-border-color);
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3;
-}
-
-.notification a.close:hover .icon {
- filter: brightness(0.5);
-}
-
.notification.good a.close:hover {
background-color: var(--notification-close-background-color-hover);
}
@@ -981,7 +964,7 @@ a:hover .icon {
}
.notification#actualizeProgress {
- line-height: 2;
+ line-height: 2em;
}
/*=== "Load more" part */
@@ -1104,22 +1087,22 @@ a:hover .icon {
/*===========*/
@media (max-width: 840px) {
+ .header > .item {
+ padding: 0.5rem 1rem;
+ }
+
.header > .item.title {
- padding: 10px 20px;
width: 75%;
text-align: left;
}
.header > .item.configure {
- padding: 10px 20px;
width: 25%;
text-align: right;
}
.form-group .group-name {
padding-bottom: 0;
-
- text-align: left;
}
.aside {
@@ -1143,8 +1126,8 @@ a:hover .icon {
}
.post {
- padding-left: 15px;
- padding-right: 15px;
+ padding-left: 1rem;
+ padding-right: 1rem;
}
.nav_menu .btn {
@@ -1161,28 +1144,16 @@ a:hover .icon {
margin: 5px 0;
}
- .nav_menu .search {
- display: inline-block;
- max-width: 97%;
- }
-
- .nav_menu .search input {
- padding: 3px 5px;
- max-width: 97%;
- width: 90px;
- line-height: 2;
- }
-
- .nav_menu .search input:focus {
- width: 400px;
- }
-
.dropdown-target:target ~ .dropdown-toggle::after {
background-color: var(--background-color-light);
border-top: 1px solid var(--border-color);
border-left: 1px solid var(--border-color);
}
+ .dropdown-menu .dropdown-section:last-child {
+ margin-bottom: 3rem;
+ }
+
.form-group.form-actions {
margin-left: -15px;
margin-right: -15px;
@@ -1209,3 +1180,96 @@ a:hover .icon {
display: none;
}
}
+
+@media screen and (prefers-color-scheme: dark) {
+ :root .darkMode_auto {
+ --frss-background-color: #000;
+ --frss-background-color-middle: #222;
+ --frss-border-color: #444;
+ --frss-font-color-grey-dark: #999;
+ --frss-font-color-dark: #ddd;
+ --frss-modal-background-color-transparent: #000000a3;
+ --frss-background-color-transparent: #000000a3;
+ --frss-scrollbar-handle: #fff1;
+ --frss-scrollbar-handle-hover: #fff4;
+
+ --background-color-light-gradient: #111;
+ --background-color-light: #111;
+ --background-color-light-shadowed: #191919;
+ --background-color-grey: #1f1f1f;
+ --background-color-hover: #09090977;
+
+ --unread-article-background-color: #201f1e;
+ --unread-article-background-color-hover: #1a1918;
+ --unread-article-border-color: #ff5300;
+ --favorite-article-background-color: #24221d;
+ --favorite-article-background-color-hover: #1d1b17;
+ --favorite-article-border-color: #ffc300;
+
+ --contrast-background-color: #0084cc;
+ --contrast-background-color-gradient: #0045cc;
+ --contrast-background-color-hover: #06c;
+ --contrast-background-color-active: #038;
+ --contrast-border-color: #0062b7;
+
+ --contrast-background-font-color: #eee;
+
+ --attention-background-color-gradient1: #ea4a46;
+ --attention-background-color-gradient2: #911811;
+
+ --attention-background-color-gradient1-hover: #d14641;
+ --attention-background-color-gradient2-hover: #bd362f;
+ --attention-background-color-active: #bd362f;
+ --attention-border-color: #c44742;
+
+ --empty-feed-color: #e67e22;
+ --error-feed-color: #bd362f;
+
+ --alert-warn-background-color: #ffffe022;
+ --alert-warn-font-color: #ccc;
+ --alert-warn-border-color: #eeb;
+ --alert-success-background-color: #e8ffe814;
+ --alert-success-font-color: #96c196;
+ --alert-success-border-color: #cec;
+ --alert-error-background-color: #fdda;
+ --alert-error-font-color: #693a3a;
+ --alert-error-boder-color: #ecc;
+
+ --notification-good-background-color: #ffe;
+ --notification-good-border-color: #eeb;
+ --notification-good-font-color: #916a37;
+ --notification-bad-background-color: #fdd;
+ --notification-bad-font-color: #643838;
+ --notification-bad-border-color: #ecc;
+ --notification-close-background-color-hover: #aaa2;
+
+ --font-color: #ccc;
+ --font-color-grey: #aaa;
+ --font-color-light-shadowed: #555;
+ --font-color-light: #ccc;
+
+ --text-shadow-color: #1c1c1c;
+ --text-shadow-color-dark: #666;
+ --box-shadow-color: #4446;
+ --box-shadow-color-inset: #1f1f1f;
+
+ --font-color-link: #467eb3;
+ --font-color-link-hover: #0062be;
+
+ --border-color: #222;
+ --border-color-shadow-side: #333;
+ --contrast-border-color-active: #0062be;
+
+ --form-element-font-color-focus: #879db1;
+ --form-element-border-color-focus: #0062be;
+ --form-element-focus-box-shadow-color-inset: #110;
+ --form-element-border-color-invalid: #f00;
+ --form-element-invalid-box-shadow-color-inset: #fdd;
+ }
+
+ .btn.active,
+ .btn:active,
+ .dropdown-target:target ~ .btn.dropdown-toggle {
+ background: var(--border-color);
+ }
+}
diff --git a/p/themes/Origine/origine.rtl.css b/p/themes/Origine/origine.rtl.css
index 8f0c9095a..ae5f1ca39 100644
--- a/p/themes/Origine/origine.rtl.css
+++ b/p/themes/Origine/origine.rtl.css
@@ -3,6 +3,8 @@
/*=== GENERAL */
/*============*/
:root {
+ --frss-padding-top-bottom: 0.5rem;
+
--background-color-light-gradient: #eee;
--background-color-light: #fff;
--background-color-light-shadowed: #f6f6f6;
@@ -10,8 +12,10 @@
--background-color-hover: #f6f6f6;
--unread-article-background-color: #fff3ed;
+ --unread-article-background-color-hover: #faeee8;
--unread-article-border-color: #ff5300;
--favorite-article-background-color: #fff6da;
+ --favorite-article-background-color-hover: #fcf2d6;
--favorite-article-border-color: #ffc300;
--contrast-background-color: #0084cc;
@@ -147,17 +151,12 @@ input:disabled, select:disabled {
background-color: var(--background-color-light-shadowed);
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid var(--border-color);
}
@@ -187,7 +186,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: left;
}
.form-group .group-controls {
@@ -218,7 +216,8 @@ form th {
}
.stick .btn:first-child,
-.stick input:first-child {
+.stick input:first-child,
+.stick select:first-child {
border-radius: 0 3px 3px 0;
}
@@ -235,6 +234,7 @@ form th {
.stick .btn + input,
.stick .btn + .dropdown > .btn,
.stick input + .btn,
+.stick select + .btn,
.stick input + input,
.stick input + .dropdown > .btn,
.stick .dropdown + .btn,
@@ -293,6 +293,10 @@ a:hover .icon {
filter: brightness(1.1);
}
+#toggle-search.active > .icon {
+ filter: invert(8%) sepia(99%) saturate(7064%) hue-rotate(248deg) brightness(99%) contrast(142%);
+}
+
.btn.active,
.btn:active,
.dropdown-target:target ~ .btn.dropdown-toggle {
@@ -351,21 +355,22 @@ a:hover .icon {
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5;
+.nav-list {
font-size: 0.9rem;
}
-.nav-list .item:hover {
- background-color: var(--background-color-hover);
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
}
-.nav-list .item:hover a {
+.nav-list .nav-section .item:hover a {
+ background-color: var(--background-color-hover);
color: var(--font-color-link-hover);
}
+.nav-list .nav-section .item.active:hover a,
.nav-list .item.active {
background-color: var(--contrast-background-color-active);
color: var(--font-color-light);
@@ -376,8 +381,8 @@ a:hover .icon {
}
.nav-list .item > a,
-.nav-list .item > span {
- padding: 0 10px;
+.nav-list .item > div {
+ padding: 0 1rem;
}
.nav-list .item > span {
@@ -389,26 +394,7 @@ a:hover .icon {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: var(--empty-feed-color);
-}
-
-.nav-list .item.active.empty a {
- background-color: var(--empty-feed-color);
- color: var(--font-color-light);
-}
-
-.nav-list .item.error a {
- color: var(--error-feed-color);
-}
-
-.nav-list .item.active.error a {
- background-color: var(--error-feed-color);
- color: var(--font-color-light);
-}
-
.nav-list .nav-header {
- padding: 0 10px;
background-color: var(--background-color-grey);
color: var(--font-color-grey);
border-bottom: 1px solid var(--border-color);
@@ -436,33 +422,45 @@ a:hover .icon {
border-color: var(--border-color);
}
-.dropdown-header {
- padding: 0 5px 5px;
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
+ padding: 0.25rem 1rem 0.25rem 0.5rem;
color: var(--font-color-grey);
font-weight: bold;
text-align: right;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5;
- font-size: 0.8rem;
+ font-size: inherit;
+}
+
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-right: 2rem;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover,
-.dropdown-menu > .item > label:hover:not(.noHover) {
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background-color: var(--contrast-background-color-active);
color: var(--font-color-light);
}
-.dropdown-menu > .item > label {
+.dropdown-menu .item > label {
padding: 0;
}
-.dropdown-menu > .item:hover .icon {
+.dropdown-menu > .item:hover > a > .icon,
+.dropdown-menu .item.dropdown-section .item:hover .icon {
filter: grayscale(100%) brightness(2.5);
}
@@ -479,14 +477,13 @@ a:hover .icon {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: var(--border-color);
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background-color: var(--background-color-grey);
color: var(--font-color-grey);
font-size: 0.9em;
@@ -567,7 +564,7 @@ a:hover .icon {
}
.box .box-content .item {
- padding: 0.5rem 0 0.25rem 0;
+ padding-bottom: 0.25rem;
font-size: 0.9rem;
line-height: 1.5;
}
@@ -579,9 +576,9 @@ a:hover .icon {
.tree-folder-title {
position: relative;
- padding: 0 10px;
+ padding-right: 0.75rem;
+ padding-left: 0.75rem;
background-color: var(--background-color-light);
- line-height: 2.5;
font-size: 1rem;
}
@@ -615,7 +612,7 @@ a:hover .icon {
.tree-folder-items > .item {
padding: 0 10px;
- line-height: 3.125;
+ line-height: 1.7;
font-size: 0.8rem;
}
@@ -643,35 +640,28 @@ a:hover .icon {
/*===============*/
/*=== Header */
.header {
- height: 4rem;
background-color: var(--background-color-grey);
}
.header > .item {
- padding: 10px;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
text-align: center;
}
.header > .item.title {
- padding: 10px 0;
width: 300px;
}
-.header > .item.search input {
- width: 230px;
+.header > .item.title a:hover .logo {
+ filter: brightness(1.4);
}
-.header .item.search input:focus {
+.header > .item.search input {
width: 350px;
}
/*=== Body */
-#global {
- height: calc(100vh - 4rem);
-}
-
.aside {
background-color: var(--background-color-light);
border-left: 1px solid var(--border-color);
@@ -689,7 +679,8 @@ a:hover .icon {
/*=== Aside main page (categories) */
.aside.aside_feed .category .title:not([data-unread="0"])::after,
-.global .box.category .title:not([data-unread="0"])::after {
+.global .box.category .title:not([data-unread="0"])::after,
+.global .feed .item-title:not([data-unread="0"])::after {
background-color: var(--background-color-light-shadowed);
color: var(--font-color-grey);
}
@@ -781,7 +772,6 @@ a:hover .icon {
}
#new-article > a {
- padding: 1em;
color: var(--font-color-light);
font-weight: bold;
}
@@ -820,10 +810,8 @@ a:hover .icon {
/*=== Index menu */
.nav_menu {
- padding: 0.5rem 0;
background-color: var(--background-color-light-shadowed);
border-bottom: 1px solid var(--border-color);
- text-align: center;
}
/*=== Feed articles */
@@ -832,39 +820,47 @@ a:hover .icon {
border-right: 2px solid transparent;
}
-.flux:hover:not(.active) {
- background-color: var(--background-color-hover) !important;
+.flux.current {
+ background-color: var(--background-color-light);
+ border-right: 2px solid var(--contrast-border-color-active);
}
-.flux:not(.current):hover .item.title {
- background: inherit;
+.flux .flux_header:hover,
+.flux .flux_header:hover .item {
+ background-color: var(--background-color-hover);
}
-.flux.current {
- background-color: var(--background-color-light);
- border-right: 2px solid var(--contrast-border-color-active);
+.flux .flux_header:not(.current):hover .flux_header,
+.flux .flux_header:not(.current):hover .flux_header .item {
+ background-color: var(--background-color-hover);
}
.flux.not_read {
border-right: 2px solid var(--unread-article-border-color);
}
-.flux.not_read:not(.current) {
+.flux.not_read .flux_header .item {
background-color: var(--unread-article-background-color);
}
-.flux.not_read:not(.current):hover .item.title {
- background: inherit;
+.flux.not_read:not(.current):hover .flux_header,
+.flux.not_read:not(.current):hover .flux_header .item {
+ background-color: var(--unread-article-background-color-hover);
}
.flux.favorite {
border-right: 2px solid var(--favorite-article-border-color);
}
-.flux.favorite:not(.current) {
+.flux.favorite:not(.current) .flux_header .item {
background-color: var(--favorite-article-background-color);
}
+.flux.favorite:not(.current):hover .flux_header,
+.flux.favorite:not(.current):hover .flux_header .item {
+ background-color: var(--favorite-article-background-color-hover);
+}
+
.flux_header {
font-size: 0.9rem;
border-top: 1px solid var(--border-color);
@@ -882,10 +878,6 @@ a:hover .icon {
}
/*=== Content of feed articles */
-.content {
- padding: 20px 10px;
-}
-
.content h1.title > a {
color: var(--font-color);
}
@@ -963,15 +955,6 @@ a:hover .icon {
border: 1px solid var(--notification-bad-border-color);
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3;
-}
-
-.notification a.close:hover .icon {
- filter: brightness(0.5);
-}
-
.notification.good a.close:hover {
background-color: var(--notification-close-background-color-hover);
}
@@ -981,7 +964,7 @@ a:hover .icon {
}
.notification#actualizeProgress {
- line-height: 2;
+ line-height: 2em;
}
/*=== "Load more" part */
@@ -1104,22 +1087,22 @@ a:hover .icon {
/*===========*/
@media (max-width: 840px) {
+ .header > .item {
+ padding: 0.5rem 1rem;
+ }
+
.header > .item.title {
- padding: 10px 20px;
width: 75%;
text-align: right;
}
.header > .item.configure {
- padding: 10px 20px;
width: 25%;
text-align: left;
}
.form-group .group-name {
padding-bottom: 0;
-
- text-align: right;
}
.aside {
@@ -1143,8 +1126,8 @@ a:hover .icon {
}
.post {
- padding-right: 15px;
- padding-left: 15px;
+ padding-right: 1rem;
+ padding-left: 1rem;
}
.nav_menu .btn {
@@ -1161,28 +1144,16 @@ a:hover .icon {
margin: 5px 0;
}
- .nav_menu .search {
- display: inline-block;
- max-width: 97%;
- }
-
- .nav_menu .search input {
- padding: 3px 5px;
- max-width: 97%;
- width: 90px;
- line-height: 2;
- }
-
- .nav_menu .search input:focus {
- width: 400px;
- }
-
.dropdown-target:target ~ .dropdown-toggle::after {
background-color: var(--background-color-light);
border-top: 1px solid var(--border-color);
border-right: 1px solid var(--border-color);
}
+ .dropdown-menu .dropdown-section:last-child {
+ margin-bottom: 3rem;
+ }
+
.form-group.form-actions {
margin-right: -15px;
margin-left: -15px;
@@ -1209,3 +1180,96 @@ a:hover .icon {
display: none;
}
}
+
+@media screen and (prefers-color-scheme: dark) {
+ :root .darkMode_auto {
+ --frss-background-color: #000;
+ --frss-background-color-middle: #222;
+ --frss-border-color: #444;
+ --frss-font-color-grey-dark: #999;
+ --frss-font-color-dark: #ddd;
+ --frss-modal-background-color-transparent: #000000a3;
+ --frss-background-color-transparent: #000000a3;
+ --frss-scrollbar-handle: #fff1;
+ --frss-scrollbar-handle-hover: #fff4;
+
+ --background-color-light-gradient: #111;
+ --background-color-light: #111;
+ --background-color-light-shadowed: #191919;
+ --background-color-grey: #1f1f1f;
+ --background-color-hover: #09090977;
+
+ --unread-article-background-color: #201f1e;
+ --unread-article-background-color-hover: #1a1918;
+ --unread-article-border-color: #ff5300;
+ --favorite-article-background-color: #24221d;
+ --favorite-article-background-color-hover: #1d1b17;
+ --favorite-article-border-color: #ffc300;
+
+ --contrast-background-color: #0084cc;
+ --contrast-background-color-gradient: #0045cc;
+ --contrast-background-color-hover: #06c;
+ --contrast-background-color-active: #038;
+ --contrast-border-color: #0062b7;
+
+ --contrast-background-font-color: #eee;
+
+ --attention-background-color-gradient1: #ea4a46;
+ --attention-background-color-gradient2: #911811;
+
+ --attention-background-color-gradient1-hover: #d14641;
+ --attention-background-color-gradient2-hover: #bd362f;
+ --attention-background-color-active: #bd362f;
+ --attention-border-color: #c44742;
+
+ --empty-feed-color: #e67e22;
+ --error-feed-color: #bd362f;
+
+ --alert-warn-background-color: #ffffe022;
+ --alert-warn-font-color: #ccc;
+ --alert-warn-border-color: #eeb;
+ --alert-success-background-color: #e8ffe814;
+ --alert-success-font-color: #96c196;
+ --alert-success-border-color: #cec;
+ --alert-error-background-color: #fdda;
+ --alert-error-font-color: #693a3a;
+ --alert-error-boder-color: #ecc;
+
+ --notification-good-background-color: #ffe;
+ --notification-good-border-color: #eeb;
+ --notification-good-font-color: #916a37;
+ --notification-bad-background-color: #fdd;
+ --notification-bad-font-color: #643838;
+ --notification-bad-border-color: #ecc;
+ --notification-close-background-color-hover: #aaa2;
+
+ --font-color: #ccc;
+ --font-color-grey: #aaa;
+ --font-color-light-shadowed: #555;
+ --font-color-light: #ccc;
+
+ --text-shadow-color: #1c1c1c;
+ --text-shadow-color-dark: #666;
+ --box-shadow-color: #4446;
+ --box-shadow-color-inset: #1f1f1f;
+
+ --font-color-link: #467eb3;
+ --font-color-link-hover: #0062be;
+
+ --border-color: #222;
+ --border-color-shadow-side: #333;
+ --contrast-border-color-active: #0062be;
+
+ --form-element-font-color-focus: #879db1;
+ --form-element-border-color-focus: #0062be;
+ --form-element-focus-box-shadow-color-inset: #110;
+ --form-element-border-color-invalid: #f00;
+ --form-element-invalid-box-shadow-color-inset: #fdd;
+ }
+
+ .btn.active,
+ .btn:active,
+ .dropdown-target:target ~ .btn.dropdown-toggle {
+ background: var(--border-color);
+ }
+}
diff --git a/p/themes/Pafat/icons/all.svg b/p/themes/Pafat/icons/all.svg
index 14fa80366..1c7ba3ffe 100644
--- a/p/themes/Pafat/icons/all.svg
+++ b/p/themes/Pafat/icons/all.svg
@@ -1,7 +1,5 @@
-<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
-<g transform="translate(-40.0002,-746)" fill="#FFF">
-<rect style="color:#FFF;" height="2.0002" width="9.9996" y="749" x="43"/>
-<rect style="color:#FFF;" height="2.0002" width="9.9996" y="753" x="43"/>
-<rect style="color:#FFF;" height="2.0002" width="9.9996" y="757" x="43"/>
-</g>
-</svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+ <g style="fill:#666;fill-opacity:1">
+ <path d="M43 749h10v2H43zM43 753h10v2H43zM43 757h10v2H43z" style="fill:#666;fill-opacity:1" transform="translate(-40 -746)"/>
+ </g>
+</svg>
diff --git a/p/themes/Pafat/icons/bookmark.svg b/p/themes/Pafat/icons/bookmark.svg
deleted file mode 100644
index 70d0c81fb..000000000
--- a/p/themes/Pafat/icons/bookmark.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
-<g transform="translate(-41.000202,-397)">
-<path style="enable-background:accumulate;color:#000000;" d="m530.95,186.71c-0.77941,0.55189-3.1576-1.906-4.1125-1.9179-0.95532-0.0119-3.3949,2.3858-4.161,1.8149-0.76573-0.57072,0.83698-3.592,0.55319-4.5039-0.2839-0.91223-3.3182-2.4915-3.0119-3.3965,0.30617-0.90461,3.6749-0.31399,4.4544-0.86567,0.77986-0.5519,1.3442-3.9257,2.2995-3.914,0.95494,0.0116,1.4342,3.398,2.1998,3.9689,0.76588,0.57114,4.1489,0.0653,4.4331,0.97746,0.28402,0.9118-2.7885,2.414-3.0949,3.3186-0.30652,0.90489,1.22,3.966,0.44027,4.5182z" fill-rule="nonzero" transform="matrix(1.0472113,-0.00871584,0.00871584,1.0472113,-504.35434,220.15425)" fill="#FFF"/>
-</g>
-</svg> \ No newline at end of file
diff --git a/p/themes/Pafat/pafat.css b/p/themes/Pafat/pafat.css
index b513ba7ea..4ff9d2812 100644
--- a/p/themes/Pafat/pafat.css
+++ b/p/themes/Pafat/pafat.css
@@ -2,15 +2,100 @@
/*=== GENERAL */
/*============*/
+:root {
+ --font-color-white: #fff;
+ --font-color-grey-light: #c5c6ca;
+ --font-color-grey: #666;
+ --font-color-hover: #000;
+ --font-color-link-title: #333;
+ --font-color-link-general: #2980b9;
+ --font-color-link-general-hover: #038;
+
+ --font-color-unread-articles: #428bca;
+
+ --font-color-article: #41444f;
+
+ --font-color-blockquote: #41444f;
+ --font-color-code: #d14;
+
+ --background-color-white: #fff;
+ --background-color-grey-light: #fafafa;
+ --background-color-grey: #f4f4f4;
+ --background-color-grey-hover: #f0f0f0;
+ --background-color-grey-button-active: #eee;
+
+ --background-color-dark: #41444f;
+
+ --background-color-navlist-active: #3498db;
+
+ --background-color-favorite: #fff6da;
+ --background-color-favorite-hover: #fff9e8;
+
+ --background-color-button-important: #5cb85c;
+ --background-color-button-important-hover: #47a447;
+
+ --background-color-button-attention: #d9534f;
+ --background-color-button-attention-hover: #d2322d;
+
+ --background-color-active-feed: #5cb85c;
+
+ --background-color-mainstream: #428bca;
+ --background-color-mainstream-active: #3276b1;
+ --background-color-favorites: #f0ad4e;
+ --background-color-favorites-active: #ed9c28;
+ --background-color-category: #5bc0de;
+ --background-color-category-active: #39b3d7;
+
+ --background-color-new-article: #428bca;
+ --background-color-new-article-hover: #3276b1;
+
+ --color-empty-feed: #f39c12;
+ --color-error-feed: #bd362f;
+
+ --color-warning-icon-folder: #f0ad4e;
+
+ --notification-good-background-color: #ffe;
+ --notification-good-border-color: #eeb;
+ --notification-good-font-color: #c95;
+ --notification-bad-background-color: #fdd;
+ --notification-bad-font-color: #844;
+ --notification-bad-border-color: #ecc;
+ --notification-box-shadow-color: #ddd;
+
+ --alert-warn-background-color: #ffe;
+ --alert-warn-border-color: #eeb;
+ --alert-warn-font-color: #c95;
+ --alert-success-background-color: #dfd;
+ --alert-success-border-color: #cec;
+ --alert-success-font-color: #484;
+ --alert-error-background-color: #fdd;
+ --alert-error-font-color: #844;
+ --alert-error-border-color: #ecc;
+
+ --invalid-box-border-color: #f00;
+ --invalid-box-shadow-color: #fdd;
+
+ --border-color-white: #fff;
+
+ --border-color-grey-dark: #aaa;
+ --border-color-grey-light: #ddd;
+
+ --border-left-article: #5cb85c;
+ --border-left-article-current: #39b3d7;
+ --border-left-article-unread: #d9534f;
+ --border-left-article-favorite: #428bca;
+}
+
+
html, body {
- background: #fafafa;
- color: #666;
+ background-color: var(--background-color-grey-light);
+ color: var(--font-color-grey);
font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", "PingFang SC", "Microsoft YaHei", sans-serif;
}
/*=== Links */
a {
- color: #2980b9;
+ color: var(--font-color-link-general);
outline: none;
}
@@ -19,7 +104,7 @@ legend {
margin: 20px 0 5px;
padding: 5px 0;
font-size: 1.4em;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color-grey-light);
}
label {
@@ -35,9 +120,9 @@ textarea {
input, select, textarea {
padding: 7px;
- background: #fdfdfd;
- color: #666;
- border: 1px solid #bbb;
+ background-color: var(--background-color-white);
+ color: var(--font-color-grey);
+ border: 1px solid var(--border-color-grey-dark);
border-radius: 3px;
vertical-align: middle;
}
@@ -47,21 +132,12 @@ option {
}
input:focus, select:focus, textarea:focus {
- outline-color: #aaa;
+ border-color: var(--border-color-grey-dark);
}
input:invalid, select:invalid {
- border-color: #f00;
- box-shadow: 0 0 2px 2px #fdd inset;
- outline-color: #fdd;
-}
-
-input:disabled, select:disabled {
- background: #eee;
-}
-
-input.extend {
- transition: width 200ms linear;
+ border-color: var(--invalid-box-border-color);
+ box-shadow: 0 0 2px 2px var(--invalid-box-shadow-color) inset;
}
/*=== Tables */
@@ -69,13 +145,12 @@ table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
- border: 1px solid #ddd;
+th, td {
+ border: 1px solid var(--border-color-grey-light);
}
th {
- background: #f6f6f6;
+ background-color: var(--background-color-grey);
}
form td,
@@ -89,8 +164,8 @@ form th {
/*=== Forms */
.form-group.form-actions {
padding: 5px 0;
- background: #f4f4f4;
- border-top: 1px solid #ddd;
+ background-color: var(--background-color-grey);
+ border-top: 1px solid var(--border-color-grey-light);
}
.form-group.form-actions .btn {
@@ -99,7 +174,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: right;
}
.form-group .group-controls {
@@ -150,11 +224,11 @@ form th {
.btn {
margin: 0;
padding: 1px 5px;
- background: #fff;
+ background-color: var(--background-color-white);
display: inline-block;
- color: #666;
+ color: var(--font-color-grey);
font-size: 0.9rem;
- border: 1px solid #aaa;
+ border: 1px solid var(--border-color-grey-dark);
border-radius: 3px;
min-height: 29px;
min-width: 15px;
@@ -174,27 +248,26 @@ a.btn {
}
.btn:hover {
- background: #f0f0f0;
+ background-color: var(--background-color-grey-hover);
text-decoration: none;
}
.btn.active,
.btn:active,
.dropdown-target:target ~ .btn.dropdown-toggle {
- background: #eee;
+ background-color: var(--background-color-grey-button-active);
}
.btn-important {
- background: #5cb85c;
- color: #fff;
- border-color: #5cb85c;
+ background-color: var(--background-color-button-important);
+ color: var(--font-color-white);
+ border: none;
font-weight: normal;
}
.btn-important:hover, .btn-important:active {
- background: #47a447;
- border-color: #47a447;
- box-shadow: none;
+ background-color: var(--background-color-button-important-hover);
+ border: none;
}
.btn-important .icon {
@@ -202,78 +275,59 @@ a.btn {
}
.btn-attention {
- background: #d9534f;
- color: #fff;
- border: 1px solid #d9534f;
- outline-color: #aaa;
+ background-color: var(--background-color-button-attention);
+ color: var(--font-color-white);
+ border: none;
}
.btn-attention:hover {
- background: #d2322d;
- border-color: #d2322d;
+ background-color: var(--background-color-button-attention-hover);
+ border: none;
}
.btn-attention:active {
- background: #d2322d;
- box-shadow: none;
+ background-color: var(--background-color-button-attention-hover);
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5;
+.nav-list {
font-size: 0.9rem;
}
-.nav-list .item:hover {
- background: #fafafa;
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
}
-.nav-list .item:hover a {
- color: #038;
+.nav-list .item a:hover {
+ color: var(--font-color-link-general-hover);
+ background-color: var(--background-color-grey-hover);
}
-.nav-list .item.active {
- background: #3498db;
- color: #fff;
+.nav-list .item.active,
+.nav-list .item.active a:hover {
+ background-color: var(--background-color-navlist-active);
+ color: var(--font-color-white);
}
.nav-list .item.active a {
- color: #fff;
+ color: var(--font-color-white);
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
}
.nav-list a:hover {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: #f39c12;
-}
-
-.nav-list .item.active.empty a {
- background: #f39c12;
- color: #fff;
-}
-
-.nav-list .item.error a {
- color: #bd362f;
-}
-
-.nav-list .item.active.error a {
- background: #bd362f;
- color: #fff;
-}
-
.nav-list .nav-header {
- padding: 0 10px;
- background: #f4f4f4;
- color: #888;
- border-bottom: 1px solid #ddd;
+ padding: 0 1rem;
+ background-color: var(--background-color-grey);
+ color: var(--font-color-grey);
+ border-bottom: 1px solid var(--border-color-grey-light);
font-weight: bold;
}
@@ -285,38 +339,50 @@ a.btn {
/*=== Dropdown */
.dropdown-menu {
margin: 5px 0 0;
- padding: 5px 0;
+ padding: 0.5rem 0 0.25rem 0;
font-size: 0.8rem;
- border: 1px solid #aaa;
+ border: 1px solid var(--border-color-grey-dark);
border-radius: 5px;
text-align: left;
}
.dropdown-menu::after {
- border-color: #aaa;
+ border-color: var(--border-color-grey-dark);
right: 8px;
}
-.dropdown-header {
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
padding: 0 5px 5px;
- color: #888;
+ color: var(--font-color-grey);
font-weight: bold;
text-align: left;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5;
- color: #666;
- font-size: 0.8rem;
+ color: var(--font-color-grey);
+ font-size: inherit;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
- background: #eee;
- color: #666;
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-left: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
+ background-color: var(--background-color-grey-hover);
+ color: var(--font-color-grey);
}
.dropdown-menu > .item[aria-checked="true"] > a::before {
@@ -332,20 +398,19 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
- border-top-color: #ddd;
+ border-top-color: var(--border-color-grey-light);
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
- background: #f4f4f4;
- color: #aaa;
+ background-color: var(--background-color-grey);
+ color: var(--font-color-grey);
font-size: 0.9em;
- border: 1px solid #ccc;
- border-right: 1px solid #aaa;
- border-bottom: 1px solid #aaa;
+ border: 1px solid var(--border-color-grey-light);
+ border-right: 1px solid var(--border-color-grey-dark);
+ border-bottom: 1px solid var(--border-color-grey-dark);
border-radius: 5px;
}
@@ -359,52 +424,43 @@ a.btn {
}
.alert-warn {
- background: #ffe;
- color: #c95;
- border: 1px solid #eeb;
+ background: var(--alert-warn-background-color);
+ color: var(--alert-warn-font-color);
+ border: 1px solid var(--alert-warn-border-color);
}
.alert-success {
- background: #dfd;
- color: #484;
- border: 1px solid #cec;
+ background-color: var(--alert-success-background-color);
+ color: var(--alert-success-font-color);
+ border: 1px solid var(--alert-success-border-color);
}
.alert-error {
- background: #fdd;
- color: #844;
- border: 1px solid #ecc;
+ background: var(--alert-error-background-color);
+ color: var(--alert-error-font-color);
+ border: 1px solid var(--alert-error-border-color);
}
/*=== Pagination */
-.pagination {
- background: #fff;
- color: #41444f;
-}
-
-.pagination .item a {
- color: #41444f;
-}
-
.pagination:first-child .item {
- border-bottom: 1px solid #aaa;
+ border-bottom: 1px solid var(--border-color-grey-dark);
}
.pagination:last-child .item {
- border-top: 1px solid #aaa;
+ border-top: 1px solid var(--border-color-grey-dark);
}
/*=== Boxes */
.box {
- border: 1px solid #aaa;
+ border: 1px solid var(--border-color-grey-dark);
border-radius: 5px;
}
.box .box-title {
margin: 0;
padding: 5px 10px;
- background: #f6f6f6;
- border-bottom: 1px solid #aaa;
+ background-color: var(--background-color-grey);
+ border-bottom: 1px solid var(--border-color-grey-dark);
border-radius: 5px 5px 0 0;
}
@@ -419,7 +475,6 @@ a.btn {
.box .box-content .item {
font-size: 0.9rem;
- line-height: 2.5;
}
/*=== Tree */
@@ -433,8 +488,8 @@ a.btn {
.tree-folder-title {
padding: 0.125rem 0.5rem;
- background: #5bc0de;
- color: #fff;
+ background-color: var(--background-color-category);
+ color: var(--font-color-white);
font-size: 0.9rem;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
@@ -443,9 +498,13 @@ a.btn {
line-height: 2.15;
}
+.tree-folder-title:hover {
+ background-color: var(--background-color-category-active);
+}
+
.tree-folder-title .title {
background: inherit;
- color: #fff;
+ color: var(--font-color-white);
}
.tree-folder-title .title:hover {
@@ -459,21 +518,31 @@ a.btn {
}
.tree-folder-title .title.error::before {
- color: #f0ad4e;
+ color: var(--color-warning-icon-folder);
font-size: 1.2rem;
font-weight: normal;
line-height: 1;
}
+.tree-folder-title > .icon,
.tree-folder-title .title .icon {
filter: brightness(2.5);
}
.tree-folder.active .tree-folder-title {
- background: #39b3d7;
+ background-color: var(--background-color-category-active);
font-weight: bold;
- border-top: 1px solid #666;
- border-bottom: 1px solid #666;
+}
+
+.tree-folder.active .tree-folder-title::before {
+ background-color: var(--background-color-white);
+ width: 0.5rem;
+ height: 0.5rem;
+ position: absolute;
+ top: 1rem;
+ left: -0.25rem;
+ z-index: 10;
+ transform: rotate(45deg);
}
.aside_feed .configure-feeds .btn {
@@ -482,8 +551,6 @@ a.btn {
.aside_feed .tree-folder-items > .item.feed {
- padding: 0 0.5rem;
- line-height: 3.1;
font-size: 0.8rem;
}
@@ -492,7 +559,7 @@ a.btn {
}
.tree-folder-items > .item.active {
- background: #5cb85c;
+ background-color: var(--background-color-active-feed);
}
.tree-folder-items > .item > a {
@@ -500,35 +567,33 @@ a.btn {
}
.tree-folder-items > .item.active > a {
- color: #fff;
+ color: var(--font-color-white);
}
/*=== STRUCTURE */
/*===============*/
/*=== Header */
.header {
- background: #41444f;
+ background-color: var(--background-color-dark);
}
.header > .item {
- padding: 10px;
- border-bottom: 1px solid #aaa;
+ border-bottom: 1px solid var(--border-color-grey-dark);
vertical-align: middle;
text-align: center;
}
-.header > .item.title {
- width: 230px;
-}
-
.header > .item.title .logo {
- margin: 0.5em 0;
filter: grayscale(100%) brightness(2.5);
}
+.header > .item.title a:hover .logo {
+ filter: grayscale(100%) brightness(4);
+}
+
a.signin {
text-decoration: none;
- color: #c5c6ca;
+ color: var(--font-color-grey-light);
}
.header > .item.search input {
@@ -542,14 +607,14 @@ a.signin {
height: 29px;
}
-.header .item.search input:focus {
+.header .item.search input {
width: 350px;
}
/*=== Body */
.aside {
- background: #fff;
- border-right: 1px solid #aaa;
+ background-color: var(--background-color-white);
+ border-right: 1px solid var(--border-color-grey-dark);
}
.aside.aside_feed {
@@ -565,17 +630,18 @@ a.signin {
.aside_feed .tree-folder-title > .title:not([data-unread="0"])::after,
.global .box.category .title:not([data-unread="0"])::after {
margin: 0.55em 0 0 0;
- background-color: white;
- color: #428bca;
+ background-color: var(--background-color-white);
+ color: var(--font-color-unread-articles);
}
.aside.aside_feed .feed .item-title:not([data-unread="0"])::after {
- background-color: #5bc0de;
- color: white;
+ background-color: var(--background-color-category);
+ color: var(--font-color-white);
}
.aside.aside_feed .tree-folder.category.active .feed .item-title:not([data-unread="0"])::after {
- background-color: #39b3d7;
+ background-color: var(--background-color-category-active);
+ font-size: 0.7rem;
}
.aside.aside_feed .category .title:not([data-unread="0"])::after {
@@ -584,51 +650,53 @@ a.signin {
.aside.aside_feed .feed.active .item-title:not([data-unread="0"])::after {
background-color: transparent;
- color: white;
- border: 1px solid #fff;
+ color: var(--font-color-white);
+ border: 1px solid var(--border-color-white);
font-weight: bold;
}
.aside_feed .tree-folder.all .tree-folder-title {
- background: #428bca;
+ background-color: var(--background-color-mainstream);
}
+.aside_feed .tree-folder.all:hover .tree-folder-title,
.aside_feed .tree-folder.all.active .tree-folder-title {
- background: #3276b1;
+ background-color: var(--background-color-mainstream-active);
}
.aside_feed .tree-folder.favorites .tree-folder-title {
- background: #f0ad4e;
+ background-color: var(--background-color-favorites);
}
+.aside_feed .tree-folder.favorites:hover .tree-folder-title,
.aside_feed .tree-folder.favorites.active .tree-folder-title {
- background: #ed9c28;
+ background-color: var(--background-color-favorites-active);
}
/*=== Aside main page (feeds) */
.feed.item.empty.active {
- background: #e67e22;
+ background: var(--color-empty-feed);
}
.feed.item.error.active {
- background: #bd362f;
+ background: var(--color-empty-feed);
}
.feed.item.empty,
.feed.item.empty > a {
- color: #e67e22;
+ color: var(--color-empty-feed);
}
.feed.item.error,
.feed.item.error > a {
- color: #bd362f;
+ color: var(--color-error-feed);
}
.feed.item.empty.active,
.feed.item.error.active,
.feed.item.empty.active > a,
.feed.item.error.active > a {
- color: #fff;
+ color: var(--font-color-white);
}
.aside_feed .tree-folder-items .dropdown-menu::after {
@@ -639,7 +707,7 @@ a.signin {
.aside_feed .tree-folder-items .item:hover .dropdown-toggle > .icon,
.aside_feed .tree-folder-items .item.active .dropdown-toggle > .icon {
border-radius: 3px;
- background-color: #fff;
+ background-color: var(--background-color-white);
}
.aside_feed .tree-folder-title .dropdown-toggle .icon {
@@ -684,29 +752,29 @@ a.signin {
/*=== New article notification */
#new-article {
- background: #428bca;
+ background-color: var(--background-color-new-article);
text-align: center;
font-size: 0.9em;
}
#new-article > a {
padding: 0.75rem;
- color: #fff;
+ color: var(--font-color-white);
font-weight: bold;
}
#new-article > a:hover {
text-decoration: none;
- background: #3276b1;
+ background-color: var(--background-color-new-article-hover);
}
/*=== Day indication */
.day {
padding: 0 10px;
- background: #fff;
- color: #666;
- border-top: 1px solid #aaa;
- border-bottom: 1px solid #aaa;
+ background-color: var(--background-color-white);
+ color: var(--font-color-grey);
+ border-top: 1px solid var(--border-color-grey-dark);
+ border-bottom: 1px solid var(--border-color-grey-dark);
font-weight: bold;
line-height: 3;
}
@@ -721,7 +789,7 @@ a.signin {
.day .name {
padding: 0 10px 0 0;
- color: #666;
+ color: var(--font-color-grey);
font-size: 1.8em;
opacity: 0.3;
font-style: italic;
@@ -731,49 +799,53 @@ a.signin {
/*=== Index menu */
.nav_menu {
padding: 5px 0;
- background: #fafafa;
- border-bottom: 1px solid #aaa;
+ background-color: var(--background-color-grey-light);
+ border-bottom: 1px solid var(--border-color-grey-dark);
text-align: center;
}
/*=== Feed articles */
.flux {
- background: #fafafa;
- border-left: 3px solid #5cb85c;
+ background-color: var(--background-color-grey-light);
+ border-left: 2px solid var(--border-left-article);
}
-.flux:hover {
- background: #fff;
+.flux .flux_header:hover {
+ background-color: var(--background-color-grey-hover);
+}
+
+.flux .flux_header:hover .title a {
+ color: var(--font-color-hover);
}
.flux.current {
- background: #fff;
- border-left: 3px solid #39b3d7;
+ background-color: var(--background-color-white);
+ border-left: 2px solid var(--border-left-article-current);
}
.flux.not_read {
- border-left: 3px solid #d9534f;
+ border-left: 2px solid var(--border-left-article-unread);
}
.flux .item.title a, .flux.not_read:not(.current):hover .item.title {
- color: #333;
+ color: var(--font-color-link-title);
}
.flux.favorite {
- border-left: 2px solid #428bca;
+ border-left: 2px solid var(--border-left-article-favorite);
}
.flux.favorite:not(.current) {
- background: #fff6da;
+ background-color: var(--background-color-favorite);
}
-.flux.favorite:not(.current):hover .item.title {
- background: #fff6da;
+.flux.favorite:not(.current) .flux_header:hover {
+ background-color: var(--background-color-favorite-hover);
}
.flux_header {
font-size: 0.8rem;
- border-top: 1px solid #ddd;
+ border-top: 1px solid var(--border-color-grey-light);
cursor: pointer;
}
@@ -786,7 +858,7 @@ a.signin {
}
.flux .item.date {
- color: #666;
+ color: var(--font-color-grey);
font-size: 0.7rem;
}
@@ -801,49 +873,48 @@ a.signin {
}
.content > h1.title > a {
- color: #333;
+ color: var(--font-color-link-title);
}
.content hr {
margin: 30px 10px;
- background: #ddd;
+ background-color: var(--background-color-grey);
height: 1px;
border: 0;
- box-shadow: 0 2px 5px #ccc;
}
.content pre {
margin: 10px auto;
padding: 10px 20px;
overflow: auto;
- background: #222;
- color: #fff;
+ background-color: var(--background-color-dark);
+ color: var(--font-color-white);
font-size: 0.9rem;
border-radius: 3px;
}
.content code {
padding: 2px 5px;
- background: #fafafa;
- color: #d14;
- border: 1px solid #eee;
+ background-color: var(--background-color-grey-light);
+ color: var(--font-color-code);
+ border: 1px solid var(--border-color-grey-light);
border-radius: 3px;
}
.content pre code {
background: transparent;
- color: #fff;
+ color: var(--font-color-white);
border: none;
}
.content blockquote {
margin: 0;
padding: 5px 20px;
- background: #fafafa;
+ background-color: var(--background-color-grey-light);
display: block;
- color: #41444f;
- border-top: 1px solid #ddd;
- border-bottom: 1px solid #ddd;
+ color: var(--font-color-blockquote);
+ border-top: 1px solid var(--border-color-grey-light);
+ border-bottom: 1px solid var(--border-color-grey-light);
}
.content blockquote p {
@@ -853,89 +924,76 @@ a.signin {
/*=== Notification and actualize notification */
.notification {
font-size: 0.9em;
- border: 1px solid #eeb;
+ border: 1px solid var(--notification-good-border-color);
border-radius: 3px;
- box-shadow: 0 0 5px #ddd;
+ box-shadow: 0 0 5px var(--notification-box-shadow-color);
text-align: center;
font-weight: bold;
vertical-align: middle;
}
.notification.good {
- background: #ffe;
- color: #c95;
- border: 1px solid #eeb;
+ background-color: var(--notification-good-background-color);
+ color: var(--notification-good-font-color);
+ border: 1px solid var(--notification-good-border-color);
}
.notification.bad {
- background: #fdd;
- color: #844;
- border: 1px solid #ecc;
-}
-
-.notification a.close {
- padding: 0 15px;
- line-height: 3;
+ background-color: var(--notification-bad-background-color);
+ color: var(--notification-bad-font-color);
+ border: 1px solid var(--notification-bad-border-color);
}
.notification.good a.close:hover {
- background: #eeb;
+ background-color: var(--notification-good-border-color);
}
.notification.bad a.close:hover {
- background: #ecc;
-}
-
-.notification#actualizeProgress {
- line-height: 2;
+ background-color: var(--notification-bad-border-color);
}
/*=== "Load more" part */
#bigMarkAsRead {
- background: #fafafa;
- color: #666;
+ background-color: var(--background-color-grey-light);
+ color: var(--font-color-grey);
text-align: center;
text-decoration: none;
}
#bigMarkAsRead:hover {
- background: #f0f0f0;
- color: #000;
-}
-
-#bigMarkAsRead:hover .bigTick {
- /* text-shadow: 0 0 10px #666;*/
+ background-color: var(--background-color-grey-hover);
+ color: var(--font-color-hover);
}
/*=== Navigation menu (for articles) */
#nav_entries {
margin: 0;
- background: #fff;
- border-top: 1px solid #ddd;
+ background-color: var(--background-color-white);
+ border-top: 1px solid var(--border-color-grey-light);
text-align: center;
line-height: 3;
table-layout: fixed;
}
#nav_entries .item:hover {
- background: #eee ;
+ background-color: var(--background-color-grey-hover);
}
/*=== READER VIEW */
/*================*/
#stream.reader .flux {
- background: #fafafa;
- color: #41444f;
+ background-color: var(--background-color-grey-light);
+ color: var(--font-color-article);
border: none;
}
#stream.reader .flux .content {
- background-color: #fff;
- border-color: #ddd;
+ background-color: var(--background-color-white);
+ border-color: var(--border-color-grey-light);
}
#stream.reader .flux .author {
margin: 0 0 10px;
- color: #666;
+ color: var(--font-color-grey);
font-size: 90%;
}
@@ -948,19 +1006,22 @@ a.signin {
}
.box.category:not([data-unread="0"]) .box-title {
- background: #5bc0de;
+ background-color: var(--background-color-category);
}
.box.category:not([data-unread="0"]) .box-title .title {
font-weight: bold;
- color: #fff;
+ color: var(--font-color-white);
}
.box.category .title:not([data-unread="0"])::after {
background: none;
- border: 0;
- box-shadow: none;
- text-shadow: none;
+ border: none;
+}
+
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ background-color: var(--background-color-category);
+ color: var(--font-color-white);
}
/*=== DIVERS */
@@ -992,7 +1053,7 @@ a.signin {
.stat > table td,
.stat > table th {
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color-grey-light);
text-align: center;
}
@@ -1002,22 +1063,17 @@ a.signin {
@media (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: left;
}
.aside {
transition: width 200ms linear;
}
- .aside:target {
- box-shadow: 3px 0 3px #aaa;
- }
-
.aside .toggle_aside,
#panel .close,
.dropdown-menu .toggle_aside {
- background: #f6f6f6;
- border-bottom: 1px solid #ddd;
+ background-color: var(--background-color-grey);
+ border-bottom: 1px solid var(--border-color-grey-light);
}
.aside.aside_feed {
@@ -1037,24 +1093,9 @@ a.signin {
margin: 5px 0;
}
- .nav_menu .search {
- display: inline-block;
- max-width: 97%;
- }
-
- .nav_menu .search input {
- max-width: 97%;
- width: 90px;
- line-height: 2;
- }
-
- .nav_menu .search input:focus {
- width: 400px;
- }
-
.dropdown-target:target ~ .dropdown-toggle::after {
- border-top: 1px solid #aaa;
- border-left: 1px solid #aaa;
+ border-top: 1px solid var(--border-color-grey-dark);
+ border-left: 1px solid var(--border-color-grey-dark);
}
.day .name {
diff --git a/p/themes/Pafat/pafat.rtl.css b/p/themes/Pafat/pafat.rtl.css
index 7d5ae91c7..8a71232bb 100644
--- a/p/themes/Pafat/pafat.rtl.css
+++ b/p/themes/Pafat/pafat.rtl.css
@@ -2,15 +2,100 @@
/*=== GENERAL */
/*============*/
+:root {
+ --font-color-white: #fff;
+ --font-color-grey-light: #c5c6ca;
+ --font-color-grey: #666;
+ --font-color-hover: #000;
+ --font-color-link-title: #333;
+ --font-color-link-general: #2980b9;
+ --font-color-link-general-hover: #038;
+
+ --font-color-unread-articles: #428bca;
+
+ --font-color-article: #41444f;
+
+ --font-color-blockquote: #41444f;
+ --font-color-code: #d14;
+
+ --background-color-white: #fff;
+ --background-color-grey-light: #fafafa;
+ --background-color-grey: #f4f4f4;
+ --background-color-grey-hover: #f0f0f0;
+ --background-color-grey-button-active: #eee;
+
+ --background-color-dark: #41444f;
+
+ --background-color-navlist-active: #3498db;
+
+ --background-color-favorite: #fff6da;
+ --background-color-favorite-hover: #fff9e8;
+
+ --background-color-button-important: #5cb85c;
+ --background-color-button-important-hover: #47a447;
+
+ --background-color-button-attention: #d9534f;
+ --background-color-button-attention-hover: #d2322d;
+
+ --background-color-active-feed: #5cb85c;
+
+ --background-color-mainstream: #428bca;
+ --background-color-mainstream-active: #3276b1;
+ --background-color-favorites: #f0ad4e;
+ --background-color-favorites-active: #ed9c28;
+ --background-color-category: #5bc0de;
+ --background-color-category-active: #39b3d7;
+
+ --background-color-new-article: #428bca;
+ --background-color-new-article-hover: #3276b1;
+
+ --color-empty-feed: #f39c12;
+ --color-error-feed: #bd362f;
+
+ --color-warning-icon-folder: #f0ad4e;
+
+ --notification-good-background-color: #ffe;
+ --notification-good-border-color: #eeb;
+ --notification-good-font-color: #c95;
+ --notification-bad-background-color: #fdd;
+ --notification-bad-font-color: #844;
+ --notification-bad-border-color: #ecc;
+ --notification-box-shadow-color: #ddd;
+
+ --alert-warn-background-color: #ffe;
+ --alert-warn-border-color: #eeb;
+ --alert-warn-font-color: #c95;
+ --alert-success-background-color: #dfd;
+ --alert-success-border-color: #cec;
+ --alert-success-font-color: #484;
+ --alert-error-background-color: #fdd;
+ --alert-error-font-color: #844;
+ --alert-error-border-color: #ecc;
+
+ --invalid-box-border-color: #f00;
+ --invalid-box-shadow-color: #fdd;
+
+ --border-color-white: #fff;
+
+ --border-color-grey-dark: #aaa;
+ --border-color-grey-light: #ddd;
+
+ --border-left-article: #5cb85c;
+ --border-left-article-current: #39b3d7;
+ --border-left-article-unread: #d9534f;
+ --border-left-article-favorite: #428bca;
+}
+
+
html, body {
- background: #fafafa;
- color: #666;
+ background-color: var(--background-color-grey-light);
+ color: var(--font-color-grey);
font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", "PingFang SC", "Microsoft YaHei", sans-serif;
}
/*=== Links */
a {
- color: #2980b9;
+ color: var(--font-color-link-general);
outline: none;
}
@@ -19,7 +104,7 @@ legend {
margin: 20px 0 5px;
padding: 5px 0;
font-size: 1.4em;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color-grey-light);
}
label {
@@ -35,9 +120,9 @@ textarea {
input, select, textarea {
padding: 7px;
- background: #fdfdfd;
- color: #666;
- border: 1px solid #bbb;
+ background-color: var(--background-color-white);
+ color: var(--font-color-grey);
+ border: 1px solid var(--border-color-grey-dark);
border-radius: 3px;
vertical-align: middle;
}
@@ -47,21 +132,12 @@ option {
}
input:focus, select:focus, textarea:focus {
- outline-color: #aaa;
+ border-color: var(--border-color-grey-dark);
}
input:invalid, select:invalid {
- border-color: #f00;
- box-shadow: 0 0 2px 2px #fdd inset;
- outline-color: #fdd;
-}
-
-input:disabled, select:disabled {
- background: #eee;
-}
-
-input.extend {
- transition: width 200ms linear;
+ border-color: var(--invalid-box-border-color);
+ box-shadow: 0 0 2px 2px var(--invalid-box-shadow-color) inset;
}
/*=== Tables */
@@ -69,13 +145,12 @@ table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
- border: 1px solid #ddd;
+th, td {
+ border: 1px solid var(--border-color-grey-light);
}
th {
- background: #f6f6f6;
+ background-color: var(--background-color-grey);
}
form td,
@@ -89,8 +164,8 @@ form th {
/*=== Forms */
.form-group.form-actions {
padding: 5px 0;
- background: #f4f4f4;
- border-top: 1px solid #ddd;
+ background-color: var(--background-color-grey);
+ border-top: 1px solid var(--border-color-grey-light);
}
.form-group.form-actions .btn {
@@ -99,7 +174,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: left;
}
.form-group .group-controls {
@@ -150,11 +224,11 @@ form th {
.btn {
margin: 0;
padding: 1px 5px;
- background: #fff;
+ background-color: var(--background-color-white);
display: inline-block;
- color: #666;
+ color: var(--font-color-grey);
font-size: 0.9rem;
- border: 1px solid #aaa;
+ border: 1px solid var(--border-color-grey-dark);
border-radius: 3px;
min-height: 29px;
min-width: 15px;
@@ -174,27 +248,26 @@ a.btn {
}
.btn:hover {
- background: #f0f0f0;
+ background-color: var(--background-color-grey-hover);
text-decoration: none;
}
.btn.active,
.btn:active,
.dropdown-target:target ~ .btn.dropdown-toggle {
- background: #eee;
+ background-color: var(--background-color-grey-button-active);
}
.btn-important {
- background: #5cb85c;
- color: #fff;
- border-color: #5cb85c;
+ background-color: var(--background-color-button-important);
+ color: var(--font-color-white);
+ border: none;
font-weight: normal;
}
.btn-important:hover, .btn-important:active {
- background: #47a447;
- border-color: #47a447;
- box-shadow: none;
+ background-color: var(--background-color-button-important-hover);
+ border: none;
}
.btn-important .icon {
@@ -202,78 +275,59 @@ a.btn {
}
.btn-attention {
- background: #d9534f;
- color: #fff;
- border: 1px solid #d9534f;
- outline-color: #aaa;
+ background-color: var(--background-color-button-attention);
+ color: var(--font-color-white);
+ border: none;
}
.btn-attention:hover {
- background: #d2322d;
- border-color: #d2322d;
+ background-color: var(--background-color-button-attention-hover);
+ border: none;
}
.btn-attention:active {
- background: #d2322d;
- box-shadow: none;
+ background-color: var(--background-color-button-attention-hover);
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5;
+.nav-list {
font-size: 0.9rem;
}
-.nav-list .item:hover {
- background: #fafafa;
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
}
-.nav-list .item:hover a {
- color: #038;
+.nav-list .item a:hover {
+ color: var(--font-color-link-general-hover);
+ background-color: var(--background-color-grey-hover);
}
-.nav-list .item.active {
- background: #3498db;
- color: #fff;
+.nav-list .item.active,
+.nav-list .item.active a:hover {
+ background-color: var(--background-color-navlist-active);
+ color: var(--font-color-white);
}
.nav-list .item.active a {
- color: #fff;
+ color: var(--font-color-white);
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
}
.nav-list a:hover {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: #f39c12;
-}
-
-.nav-list .item.active.empty a {
- background: #f39c12;
- color: #fff;
-}
-
-.nav-list .item.error a {
- color: #bd362f;
-}
-
-.nav-list .item.active.error a {
- background: #bd362f;
- color: #fff;
-}
-
.nav-list .nav-header {
- padding: 0 10px;
- background: #f4f4f4;
- color: #888;
- border-bottom: 1px solid #ddd;
+ padding: 0 1rem;
+ background-color: var(--background-color-grey);
+ color: var(--font-color-grey);
+ border-bottom: 1px solid var(--border-color-grey-light);
font-weight: bold;
}
@@ -285,38 +339,50 @@ a.btn {
/*=== Dropdown */
.dropdown-menu {
margin: 5px 0 0;
- padding: 5px 0;
+ padding: 0.5rem 0 0.25rem 0;
font-size: 0.8rem;
- border: 1px solid #aaa;
+ border: 1px solid var(--border-color-grey-dark);
border-radius: 5px;
text-align: right;
}
.dropdown-menu::after {
- border-color: #aaa;
+ border-color: var(--border-color-grey-dark);
left: 8px;
}
-.dropdown-header {
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
padding: 0 5px 5px;
- color: #888;
+ color: var(--font-color-grey);
font-weight: bold;
text-align: right;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5;
- color: #666;
- font-size: 0.8rem;
+ color: var(--font-color-grey);
+ font-size: inherit;
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
- background: #eee;
- color: #666;
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-right: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
+}
+
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
+ background-color: var(--background-color-grey-hover);
+ color: var(--font-color-grey);
}
.dropdown-menu > .item[aria-checked="true"] > a::before {
@@ -332,20 +398,19 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
- border-top-color: #ddd;
+ border-top-color: var(--border-color-grey-light);
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
- background: #f4f4f4;
- color: #aaa;
+ background-color: var(--background-color-grey);
+ color: var(--font-color-grey);
font-size: 0.9em;
- border: 1px solid #ccc;
- border-left: 1px solid #aaa;
- border-bottom: 1px solid #aaa;
+ border: 1px solid var(--border-color-grey-light);
+ border-left: 1px solid var(--border-color-grey-dark);
+ border-bottom: 1px solid var(--border-color-grey-dark);
border-radius: 5px;
}
@@ -359,52 +424,43 @@ a.btn {
}
.alert-warn {
- background: #ffe;
- color: #c95;
- border: 1px solid #eeb;
+ background: var(--alert-warn-background-color);
+ color: var(--alert-warn-font-color);
+ border: 1px solid var(--alert-warn-border-color);
}
.alert-success {
- background: #dfd;
- color: #484;
- border: 1px solid #cec;
+ background-color: var(--alert-success-background-color);
+ color: var(--alert-success-font-color);
+ border: 1px solid var(--alert-success-border-color);
}
.alert-error {
- background: #fdd;
- color: #844;
- border: 1px solid #ecc;
+ background: var(--alert-error-background-color);
+ color: var(--alert-error-font-color);
+ border: 1px solid var(--alert-error-border-color);
}
/*=== Pagination */
-.pagination {
- background: #fff;
- color: #41444f;
-}
-
-.pagination .item a {
- color: #41444f;
-}
-
.pagination:first-child .item {
- border-bottom: 1px solid #aaa;
+ border-bottom: 1px solid var(--border-color-grey-dark);
}
.pagination:last-child .item {
- border-top: 1px solid #aaa;
+ border-top: 1px solid var(--border-color-grey-dark);
}
/*=== Boxes */
.box {
- border: 1px solid #aaa;
+ border: 1px solid var(--border-color-grey-dark);
border-radius: 5px;
}
.box .box-title {
margin: 0;
padding: 5px 10px;
- background: #f6f6f6;
- border-bottom: 1px solid #aaa;
+ background-color: var(--background-color-grey);
+ border-bottom: 1px solid var(--border-color-grey-dark);
border-radius: 5px 5px 0 0;
}
@@ -419,7 +475,6 @@ a.btn {
.box .box-content .item {
font-size: 0.9rem;
- line-height: 2.5;
}
/*=== Tree */
@@ -433,8 +488,8 @@ a.btn {
.tree-folder-title {
padding: 0.125rem 0.5rem;
- background: #5bc0de;
- color: #fff;
+ background-color: var(--background-color-category);
+ color: var(--font-color-white);
font-size: 0.9rem;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
@@ -443,9 +498,13 @@ a.btn {
line-height: 2.15;
}
+.tree-folder-title:hover {
+ background-color: var(--background-color-category-active);
+}
+
.tree-folder-title .title {
background: inherit;
- color: #fff;
+ color: var(--font-color-white);
}
.tree-folder-title .title:hover {
@@ -459,21 +518,31 @@ a.btn {
}
.tree-folder-title .title.error::before {
- color: #f0ad4e;
+ color: var(--color-warning-icon-folder);
font-size: 1.2rem;
font-weight: normal;
line-height: 1;
}
+.tree-folder-title > .icon,
.tree-folder-title .title .icon {
filter: brightness(2.5);
}
.tree-folder.active .tree-folder-title {
- background: #39b3d7;
+ background-color: var(--background-color-category-active);
font-weight: bold;
- border-top: 1px solid #666;
- border-bottom: 1px solid #666;
+}
+
+.tree-folder.active .tree-folder-title::before {
+ background-color: var(--background-color-white);
+ width: 0.5rem;
+ height: 0.5rem;
+ position: absolute;
+ top: 1rem;
+ right: -0.25rem;
+ z-index: 10;
+ transform: rotate(-45deg);
}
.aside_feed .configure-feeds .btn {
@@ -482,8 +551,6 @@ a.btn {
.aside_feed .tree-folder-items > .item.feed {
- padding: 0 0.5rem;
- line-height: 3.1;
font-size: 0.8rem;
}
@@ -492,7 +559,7 @@ a.btn {
}
.tree-folder-items > .item.active {
- background: #5cb85c;
+ background-color: var(--background-color-active-feed);
}
.tree-folder-items > .item > a {
@@ -500,35 +567,33 @@ a.btn {
}
.tree-folder-items > .item.active > a {
- color: #fff;
+ color: var(--font-color-white);
}
/*=== STRUCTURE */
/*===============*/
/*=== Header */
.header {
- background: #41444f;
+ background-color: var(--background-color-dark);
}
.header > .item {
- padding: 10px;
- border-bottom: 1px solid #aaa;
+ border-bottom: 1px solid var(--border-color-grey-dark);
vertical-align: middle;
text-align: center;
}
-.header > .item.title {
- width: 230px;
-}
-
.header > .item.title .logo {
- margin: 0.5em 0;
filter: grayscale(100%) brightness(2.5);
}
+.header > .item.title a:hover .logo {
+ filter: grayscale(100%) brightness(4);
+}
+
a.signin {
text-decoration: none;
- color: #c5c6ca;
+ color: var(--font-color-grey-light);
}
.header > .item.search input {
@@ -542,14 +607,14 @@ a.signin {
height: 29px;
}
-.header .item.search input:focus {
+.header .item.search input {
width: 350px;
}
/*=== Body */
.aside {
- background: #fff;
- border-left: 1px solid #aaa;
+ background-color: var(--background-color-white);
+ border-left: 1px solid var(--border-color-grey-dark);
}
.aside.aside_feed {
@@ -565,17 +630,18 @@ a.signin {
.aside_feed .tree-folder-title > .title:not([data-unread="0"])::after,
.global .box.category .title:not([data-unread="0"])::after {
margin: 0.55em 0 0 0;
- background-color: white;
- color: #428bca;
+ background-color: var(--background-color-white);
+ color: var(--font-color-unread-articles);
}
.aside.aside_feed .feed .item-title:not([data-unread="0"])::after {
- background-color: #5bc0de;
- color: white;
+ background-color: var(--background-color-category);
+ color: var(--font-color-white);
}
.aside.aside_feed .tree-folder.category.active .feed .item-title:not([data-unread="0"])::after {
- background-color: #39b3d7;
+ background-color: var(--background-color-category-active);
+ font-size: 0.7rem;
}
.aside.aside_feed .category .title:not([data-unread="0"])::after {
@@ -584,51 +650,53 @@ a.signin {
.aside.aside_feed .feed.active .item-title:not([data-unread="0"])::after {
background-color: transparent;
- color: white;
- border: 1px solid #fff;
+ color: var(--font-color-white);
+ border: 1px solid var(--border-color-white);
font-weight: bold;
}
.aside_feed .tree-folder.all .tree-folder-title {
- background: #428bca;
+ background-color: var(--background-color-mainstream);
}
+.aside_feed .tree-folder.all:hover .tree-folder-title,
.aside_feed .tree-folder.all.active .tree-folder-title {
- background: #3276b1;
+ background-color: var(--background-color-mainstream-active);
}
.aside_feed .tree-folder.favorites .tree-folder-title {
- background: #f0ad4e;
+ background-color: var(--background-color-favorites);
}
+.aside_feed .tree-folder.favorites:hover .tree-folder-title,
.aside_feed .tree-folder.favorites.active .tree-folder-title {
- background: #ed9c28;
+ background-color: var(--background-color-favorites-active);
}
/*=== Aside main page (feeds) */
.feed.item.empty.active {
- background: #e67e22;
+ background: var(--color-empty-feed);
}
.feed.item.error.active {
- background: #bd362f;
+ background: var(--color-empty-feed);
}
.feed.item.empty,
.feed.item.empty > a {
- color: #e67e22;
+ color: var(--color-empty-feed);
}
.feed.item.error,
.feed.item.error > a {
- color: #bd362f;
+ color: var(--color-error-feed);
}
.feed.item.empty.active,
.feed.item.error.active,
.feed.item.empty.active > a,
.feed.item.error.active > a {
- color: #fff;
+ color: var(--font-color-white);
}
.aside_feed .tree-folder-items .dropdown-menu::after {
@@ -639,7 +707,7 @@ a.signin {
.aside_feed .tree-folder-items .item:hover .dropdown-toggle > .icon,
.aside_feed .tree-folder-items .item.active .dropdown-toggle > .icon {
border-radius: 3px;
- background-color: #fff;
+ background-color: var(--background-color-white);
}
.aside_feed .tree-folder-title .dropdown-toggle .icon {
@@ -684,29 +752,29 @@ a.signin {
/*=== New article notification */
#new-article {
- background: #428bca;
+ background-color: var(--background-color-new-article);
text-align: center;
font-size: 0.9em;
}
#new-article > a {
padding: 0.75rem;
- color: #fff;
+ color: var(--font-color-white);
font-weight: bold;
}
#new-article > a:hover {
text-decoration: none;
- background: #3276b1;
+ background-color: var(--background-color-new-article-hover);
}
/*=== Day indication */
.day {
padding: 0 10px;
- background: #fff;
- color: #666;
- border-top: 1px solid #aaa;
- border-bottom: 1px solid #aaa;
+ background-color: var(--background-color-white);
+ color: var(--font-color-grey);
+ border-top: 1px solid var(--border-color-grey-dark);
+ border-bottom: 1px solid var(--border-color-grey-dark);
font-weight: bold;
line-height: 3;
}
@@ -721,7 +789,7 @@ a.signin {
.day .name {
padding: 0 0 0 10px;
- color: #666;
+ color: var(--font-color-grey);
font-size: 1.8em;
opacity: 0.3;
font-style: italic;
@@ -731,49 +799,53 @@ a.signin {
/*=== Index menu */
.nav_menu {
padding: 5px 0;
- background: #fafafa;
- border-bottom: 1px solid #aaa;
+ background-color: var(--background-color-grey-light);
+ border-bottom: 1px solid var(--border-color-grey-dark);
text-align: center;
}
/*=== Feed articles */
.flux {
- background: #fafafa;
- border-right: 3px solid #5cb85c;
+ background-color: var(--background-color-grey-light);
+ border-right: 2px solid var(--border-left-article);
}
-.flux:hover {
- background: #fff;
+.flux .flux_header:hover {
+ background-color: var(--background-color-grey-hover);
+}
+
+.flux .flux_header:hover .title a {
+ color: var(--font-color-hover);
}
.flux.current {
- background: #fff;
- border-right: 3px solid #39b3d7;
+ background-color: var(--background-color-white);
+ border-right: 2px solid var(--border-left-article-current);
}
.flux.not_read {
- border-right: 3px solid #d9534f;
+ border-right: 2px solid var(--border-left-article-unread);
}
.flux .item.title a, .flux.not_read:not(.current):hover .item.title {
- color: #333;
+ color: var(--font-color-link-title);
}
.flux.favorite {
- border-right: 2px solid #428bca;
+ border-right: 2px solid var(--border-left-article-favorite);
}
.flux.favorite:not(.current) {
- background: #fff6da;
+ background-color: var(--background-color-favorite);
}
-.flux.favorite:not(.current):hover .item.title {
- background: #fff6da;
+.flux.favorite:not(.current) .flux_header:hover {
+ background-color: var(--background-color-favorite-hover);
}
.flux_header {
font-size: 0.8rem;
- border-top: 1px solid #ddd;
+ border-top: 1px solid var(--border-color-grey-light);
cursor: pointer;
}
@@ -786,7 +858,7 @@ a.signin {
}
.flux .item.date {
- color: #666;
+ color: var(--font-color-grey);
font-size: 0.7rem;
}
@@ -801,49 +873,48 @@ a.signin {
}
.content > h1.title > a {
- color: #333;
+ color: var(--font-color-link-title);
}
.content hr {
margin: 30px 10px;
- background: #ddd;
+ background-color: var(--background-color-grey);
height: 1px;
border: 0;
- box-shadow: 0 2px 5px #ccc;
}
.content pre {
margin: 10px auto;
padding: 10px 20px;
overflow: auto;
- background: #222;
- color: #fff;
+ background-color: var(--background-color-dark);
+ color: var(--font-color-white);
font-size: 0.9rem;
border-radius: 3px;
}
.content code {
padding: 2px 5px;
- background: #fafafa;
- color: #d14;
- border: 1px solid #eee;
+ background-color: var(--background-color-grey-light);
+ color: var(--font-color-code);
+ border: 1px solid var(--border-color-grey-light);
border-radius: 3px;
}
.content pre code {
background: transparent;
- color: #fff;
+ color: var(--font-color-white);
border: none;
}
.content blockquote {
margin: 0;
padding: 5px 20px;
- background: #fafafa;
+ background-color: var(--background-color-grey-light);
display: block;
- color: #41444f;
- border-top: 1px solid #ddd;
- border-bottom: 1px solid #ddd;
+ color: var(--font-color-blockquote);
+ border-top: 1px solid var(--border-color-grey-light);
+ border-bottom: 1px solid var(--border-color-grey-light);
}
.content blockquote p {
@@ -853,89 +924,76 @@ a.signin {
/*=== Notification and actualize notification */
.notification {
font-size: 0.9em;
- border: 1px solid #eeb;
+ border: 1px solid var(--notification-good-border-color);
border-radius: 3px;
- box-shadow: 0 0 5px #ddd;
+ box-shadow: 0 0 5px var(--notification-box-shadow-color);
text-align: center;
font-weight: bold;
vertical-align: middle;
}
.notification.good {
- background: #ffe;
- color: #c95;
- border: 1px solid #eeb;
+ background-color: var(--notification-good-background-color);
+ color: var(--notification-good-font-color);
+ border: 1px solid var(--notification-good-border-color);
}
.notification.bad {
- background: #fdd;
- color: #844;
- border: 1px solid #ecc;
-}
-
-.notification a.close {
- padding: 0 15px;
- line-height: 3;
+ background-color: var(--notification-bad-background-color);
+ color: var(--notification-bad-font-color);
+ border: 1px solid var(--notification-bad-border-color);
}
.notification.good a.close:hover {
- background: #eeb;
+ background-color: var(--notification-good-border-color);
}
.notification.bad a.close:hover {
- background: #ecc;
-}
-
-.notification#actualizeProgress {
- line-height: 2;
+ background-color: var(--notification-bad-border-color);
}
/*=== "Load more" part */
#bigMarkAsRead {
- background: #fafafa;
- color: #666;
+ background-color: var(--background-color-grey-light);
+ color: var(--font-color-grey);
text-align: center;
text-decoration: none;
}
#bigMarkAsRead:hover {
- background: #f0f0f0;
- color: #000;
-}
-
-#bigMarkAsRead:hover .bigTick {
- /* text-shadow: 0 0 10px #666;*/
+ background-color: var(--background-color-grey-hover);
+ color: var(--font-color-hover);
}
/*=== Navigation menu (for articles) */
#nav_entries {
margin: 0;
- background: #fff;
- border-top: 1px solid #ddd;
+ background-color: var(--background-color-white);
+ border-top: 1px solid var(--border-color-grey-light);
text-align: center;
line-height: 3;
table-layout: fixed;
}
#nav_entries .item:hover {
- background: #eee ;
+ background-color: var(--background-color-grey-hover);
}
/*=== READER VIEW */
/*================*/
#stream.reader .flux {
- background: #fafafa;
- color: #41444f;
+ background-color: var(--background-color-grey-light);
+ color: var(--font-color-article);
border: none;
}
#stream.reader .flux .content {
- background-color: #fff;
- border-color: #ddd;
+ background-color: var(--background-color-white);
+ border-color: var(--border-color-grey-light);
}
#stream.reader .flux .author {
margin: 0 0 10px;
- color: #666;
+ color: var(--font-color-grey);
font-size: 90%;
}
@@ -948,19 +1006,22 @@ a.signin {
}
.box.category:not([data-unread="0"]) .box-title {
- background: #5bc0de;
+ background-color: var(--background-color-category);
}
.box.category:not([data-unread="0"]) .box-title .title {
font-weight: bold;
- color: #fff;
+ color: var(--font-color-white);
}
.box.category .title:not([data-unread="0"])::after {
background: none;
- border: 0;
- box-shadow: none;
- text-shadow: none;
+ border: none;
+}
+
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ background-color: var(--background-color-category);
+ color: var(--font-color-white);
}
/*=== DIVERS */
@@ -992,7 +1053,7 @@ a.signin {
.stat > table td,
.stat > table th {
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color-grey-light);
text-align: center;
}
@@ -1002,22 +1063,17 @@ a.signin {
@media (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: right;
}
.aside {
transition: width 200ms linear;
}
- .aside:target {
- box-shadow: -3px 0 3px #aaa;
- }
-
.aside .toggle_aside,
#panel .close,
.dropdown-menu .toggle_aside {
- background: #f6f6f6;
- border-bottom: 1px solid #ddd;
+ background-color: var(--background-color-grey);
+ border-bottom: 1px solid var(--border-color-grey-light);
}
.aside.aside_feed {
@@ -1037,24 +1093,9 @@ a.signin {
margin: 5px 0;
}
- .nav_menu .search {
- display: inline-block;
- max-width: 97%;
- }
-
- .nav_menu .search input {
- max-width: 97%;
- width: 90px;
- line-height: 2;
- }
-
- .nav_menu .search input:focus {
- width: 400px;
- }
-
.dropdown-target:target ~ .dropdown-toggle::after {
- border-top: 1px solid #aaa;
- border-right: 1px solid #aaa;
+ border-top: 1px solid var(--border-color-grey-dark);
+ border-right: 1px solid var(--border-color-grey-dark);
}
.day .name {
diff --git a/p/themes/Screwdriver/icons/bookmark.svg b/p/themes/Screwdriver/icons/bookmark.svg
deleted file mode 100644
index edf5a02db..000000000
--- a/p/themes/Screwdriver/icons/bookmark.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
-<g transform="translate(-41.000202,-397)">
-<path style="color:#000000;enable-background:accumulate;" d="m530.95,186.71c-0.77941,0.55189-3.1576-1.906-4.1125-1.9179-0.95532-0.0119-3.3949,2.3858-4.161,1.8149-0.76573-0.57072,0.83698-3.592,0.55319-4.5039-0.2839-0.91223-3.3182-2.4915-3.0119-3.3965,0.30617-0.90461,3.6749-0.31399,4.4544-0.86567,0.77986-0.5519,1.3442-3.9257,2.2995-3.914,0.95494,0.0116,1.4342,3.398,2.1998,3.9689,0.76588,0.57114,4.1489,0.0653,4.4331,0.97746,0.28402,0.9118-2.7885,2.414-3.0949,3.3186-0.30652,0.90489,1.22,3.966,0.44027,4.5182z" fill-rule="nonzero" transform="matrix(1.0472113,-0.00871584,0.00871584,1.0472113,-504.35434,220.15425)" fill="#d18104"/>
-</g>
-</svg> \ No newline at end of file
diff --git a/p/themes/Screwdriver/metadata.json b/p/themes/Screwdriver/metadata.json
index 08654e51d..8da73f80c 100644
--- a/p/themes/Screwdriver/metadata.json
+++ b/p/themes/Screwdriver/metadata.json
@@ -3,5 +3,6 @@
"author": "Mister aiR",
"description": "C’est un cocktail ! C’est chaud mais « fresh » à la fois. Ce thème tue du chaton.",
"version": 1.1,
- "files": ["_frss.css","screwdriver.css"]
+ "files": ["_frss.css","screwdriver.css"],
+ "deprecated" : true
}
diff --git a/p/themes/Screwdriver/screwdriver.css b/p/themes/Screwdriver/screwdriver.css
index 3829c787b..859e34f35 100644
--- a/p/themes/Screwdriver/screwdriver.css
+++ b/p/themes/Screwdriver/screwdriver.css
@@ -64,17 +64,12 @@ input:disabled, select:disabled {
background: #eee;
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #ddd;
}
@@ -105,7 +100,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: right;
}
.form-group .group-controls {
@@ -265,13 +259,16 @@ a.btn {
box-shadow: 0 -1px rgba(255,255,255,0.08) inset;
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5em;
+.nav-list {
font-size: 0.9rem;
}
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item a:hover {
text-shadow: 0 0 2px rgba(255,255,255,0.28);
color: #fff;
@@ -292,7 +289,7 @@ a.btn {
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
color: #ccc;
}
@@ -300,27 +297,8 @@ a.btn {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: #f39c12;
-}
-
-.nav-list .item.active.empty a {
- background: linear-gradient(180deg, #e4992c 0%, #d18114 100%) #e4992c;
- background: -webkit-linear-gradient(180deg, #e4992c 0%, #d18114 100%);
- color: #fff;
-}
-
-.nav-list .item.error a {
- color: #bd362f;
-}
-
-.nav-list .item.active.error a {
- background: #bd362f;
- color: #fff;
-}
-
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
background: transparent;
color: #222;
}
@@ -333,7 +311,7 @@ a.btn {
/*=== Dropdown */
.dropdown-menu {
margin: 5px 0 0;
- padding: 5px 0;
+ padding: 0.5rem 0 0.25rem 0;
background: #222;
font-size: 0.8rem;
border: 1px solid #171717;
@@ -346,30 +324,43 @@ a.btn {
border-color: #171717;
}
-.dropdown-header {
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
padding: 0 5px 5px;
color: #ccc;
font-weight: bold;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5em;
color: #ccc;
font-size: 0.8rem;
}
-.dropdown-menu > .item > label {
- color: #ccc;
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-left: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
}
-.dropdown-menu > .item:hover {
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background: #171717;
color: #fff;
}
+.dropdown-menu > .item > label {
+ color: #ccc;
+}
+
.dropdown-menu > .item[aria-checked="true"] > a::before {
font-weight: bold;
margin: 0 0 0 -14px;
@@ -381,14 +372,13 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: rgba(255,255,255,0.08);
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background: #f4f4f4;
color: #aaa;
font-size: 0.9em;
@@ -480,7 +470,6 @@ a.btn {
.box .box-content .item {
font-size: 0.9rem;
- line-height: 2.5em;
}
/*=== Tree */
@@ -519,8 +508,6 @@ a.btn {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 2.5rem;
font-size: 0.8rem;
}
@@ -586,10 +573,6 @@ a.btn {
}
.header > .item.search input {
- width: 230px;
-}
-
-.header .item.search input:focus {
width: 350px;
}
@@ -780,7 +763,7 @@ a.btn {
background: #ede7de;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #f9f7f4;
}
@@ -929,15 +912,6 @@ a.btn {
color: #844;
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3em;
-}
-
-.notification#actualizeProgress {
- line-height: 2em;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
background: #ede7de;
@@ -1096,7 +1070,6 @@ a.btn {
@media screen and (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: left;
}
.header {
@@ -1144,10 +1117,6 @@ a.btn {
margin: 5px 0;
}
- .nav_menu .search {
- display: none;
- }
-
.nav_menu .search input {
padding: 3px 5px;
max-width: 97%;
diff --git a/p/themes/Screwdriver/screwdriver.rtl.css b/p/themes/Screwdriver/screwdriver.rtl.css
index 7d745560d..216c84262 100644
--- a/p/themes/Screwdriver/screwdriver.rtl.css
+++ b/p/themes/Screwdriver/screwdriver.rtl.css
@@ -64,17 +64,12 @@ input:disabled, select:disabled {
background: #eee;
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
border: 1px solid #ddd;
}
@@ -105,7 +100,6 @@ form th {
.form-group .group-name {
padding: 10px 0;
- text-align: left;
}
.form-group .group-controls {
@@ -265,13 +259,16 @@ a.btn {
box-shadow: 0 -1px rgba(255,255,255,0.08) inset;
}
/*=== Navigation */
-.nav-list .nav-header,
-.nav-list .item {
- height: 2.5em;
- line-height: 2.5em;
+.nav-list {
font-size: 0.9rem;
}
+.nav-list .item,
+.nav-list .item.nav-header {
+ min-height: 2.5em;
+ line-height: 2.5;
+}
+
.nav-list .item a:hover {
text-shadow: 0 0 2px rgba(255,255,255,0.28);
color: #fff;
@@ -292,7 +289,7 @@ a.btn {
}
.nav-list .item > a {
- padding: 0 10px;
+ padding: 0 1rem;
color: #ccc;
}
@@ -300,27 +297,8 @@ a.btn {
text-decoration: none;
}
-.nav-list .item.empty a {
- color: #f39c12;
-}
-
-.nav-list .item.active.empty a {
- background: linear-gradient(-180deg, #e4992c 0%, #d18114 100%) #e4992c;
- background: -webkit-linear-gradient(-180deg, #e4992c 0%, #d18114 100%);
- color: #fff;
-}
-
-.nav-list .item.error a {
- color: #bd362f;
-}
-
-.nav-list .item.active.error a {
- background: #bd362f;
- color: #fff;
-}
-
.nav-list .nav-header {
- padding: 0 10px;
+ padding: 0 1rem;
background: transparent;
color: #222;
}
@@ -333,7 +311,7 @@ a.btn {
/*=== Dropdown */
.dropdown-menu {
margin: 5px 0 0;
- padding: 5px 0;
+ padding: 0.5rem 0 0.25rem 0;
background: #222;
font-size: 0.8rem;
border: 1px solid #171717;
@@ -346,30 +324,43 @@ a.btn {
border-color: #171717;
}
-.dropdown-header {
+.dropdown-header,
+.dropdown-section .dropdown-section-title {
padding: 0 5px 5px;
color: #ccc;
font-weight: bold;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > span,
-.dropdown-menu > .item > .as-link {
+.dropdown-menu .item > a,
+.dropdown-menu .item > span,
+.dropdown-menu .item > .as-link {
padding: 0 22px;
line-height: 2.5em;
color: #ccc;
font-size: 0.8rem;
}
-.dropdown-menu > .item > label {
- color: #ccc;
+.dropdown-menu .dropdown-section .item > a,
+.dropdown-menu .dropdown-section .item > span,
+.dropdown-menu .dropdown-section .item > .as-link {
+ padding-right: 2rem;
+}
+
+.dropdown-menu .dropdown-section .item:last-child {
+ margin-bottom: 0.5rem;
}
-.dropdown-menu > .item:hover {
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover:not([disabled]),
+.dropdown-menu .item > label:hover:not(.noHover) {
background: #171717;
color: #fff;
}
+.dropdown-menu > .item > label {
+ color: #ccc;
+}
+
.dropdown-menu > .item[aria-checked="true"] > a::before {
font-weight: bold;
margin: 0 -14px 0 0;
@@ -381,14 +372,13 @@ a.btn {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
border-top-color: rgba(255,255,255,0.08);
}
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
background: #f4f4f4;
color: #aaa;
font-size: 0.9em;
@@ -480,7 +470,6 @@ a.btn {
.box .box-content .item {
font-size: 0.9rem;
- line-height: 2.5em;
}
/*=== Tree */
@@ -519,8 +508,6 @@ a.btn {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 2.5rem;
font-size: 0.8rem;
}
@@ -586,10 +573,6 @@ a.btn {
}
.header > .item.search input {
- width: 230px;
-}
-
-.header .item.search input:focus {
width: 350px;
}
@@ -780,7 +763,7 @@ a.btn {
background: #ede7de;
}
-.flux:hover {
+.flux .flux_header:hover {
background: #f9f7f4;
}
@@ -929,15 +912,6 @@ a.btn {
color: #844;
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3em;
-}
-
-.notification#actualizeProgress {
- line-height: 2em;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
background: #ede7de;
@@ -1096,7 +1070,6 @@ a.btn {
@media screen and (max-width: 840px) {
.form-group .group-name {
padding-bottom: 0;
- text-align: right;
}
.header {
@@ -1144,10 +1117,6 @@ a.btn {
margin: 5px 0;
}
- .nav_menu .search {
- display: none;
- }
-
.nav_menu .search input {
padding: 3px 5px;
max-width: 97%;
diff --git a/p/themes/Swage/icons/bookmark.svg b/p/themes/Swage/icons/bookmark.svg
deleted file mode 100644
index 09bf263fd..000000000
--- a/p/themes/Swage/icons/bookmark.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg enable-background="new 0 0 16 16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
-<style type="text/css">.st0{fill:#FFD255;}</style>
-<path class="st0" d="M8,12.6l4.9,3L11.6,10L16,6.2l-5.7-0.5L8,0.4L5.8,5.7L0,6.2L4.4,10l-1.3,5.6L8,12.6z"/>
-</svg>
-
diff --git a/p/themes/Swage/swage.css b/p/themes/Swage/swage.css
index 1c438f4a1..6478c0581 100644
--- a/p/themes/Swage/swage.css
+++ b/p/themes/Swage/swage.css
@@ -54,13 +54,12 @@ select:invalid {
box-shadow: none;
}
-.nav-list .item, .nav-list .nav-header {
- height: 2.5em;
+.nav-list .item .nav-header, .nav-list .item {
+ min-height: 2.5em;
line-height: 2.5;
- font-size: 0.9rem;
}
-.dropdown-menu > .item > a,
+.dropdown-menu > .item a,
.dropdown-menu > .item > span,
.dropdown-menu > .item > .as-link,
.dropdown-menu > .item button, .dropdown-menu > .item {
@@ -71,7 +70,6 @@ select:invalid {
}
.flux::after, .form-group::after {
- content: "";
display: block;
clear: both;
}
@@ -87,7 +85,7 @@ body {
}
a {
- color: var(--color-nav-lighter);
+ color: var(--color-text-nav);
outline: none;
}
a.btn {
@@ -150,10 +148,6 @@ select {
padding-bottom: 8px;
}
-input.extend {
- transition: width 200ms linear;
-}
-
option {
padding: 0 0.5em;
}
@@ -162,10 +156,8 @@ table {
border-collapse: collapse;
}
-tr,
td,
th {
- padding: 0.5em;
border: 1px solid var(--color-border-light-darker);
}
@@ -266,7 +258,10 @@ form th {
background-image: url("./icons/disabled-light.svg");
}
-.nav-list .nav-header {
+.nav-list {
+ font-size: 0.9rem;
+}
+.nav-list .item .nav-header {
padding: 0 1rem;
font-weight: bold;
background-color: var(--color-background-aside);
@@ -284,25 +279,9 @@ form th {
.nav-list .item.active a {
color: var(--color-text-light);
}
-.nav-list .item.active.empty a,
-.nav-list .item.active .error a {
- color: var(--color-text-light);
-}
-.nav-list .item.active.empty a {
- background-color: var(--color-background-alert);
-}
-.nav-list .item.active.error a {
- background-color: var(--color-background-bad);
-}
.nav-list .item > a {
padding: 0 1.5rem;
}
-.nav-list .item.empty a {
- color: var(--color-text-alert);
-}
-.nav-list .item.error a {
- color: var(--color-text-bad-lighter);
-}
.nav-list .item .icon {
filter: brightness(3);
}
@@ -335,12 +314,20 @@ form th {
.dropdown-menu .dropdown-header a:hover {
background-color: var(--color-background-nav);
}
-.dropdown-menu::after {
- content: none;
+.dropdown-menu .dropdown-section .dropdown-section-title {
+ cursor: default;
+ padding: 0.25rem 0.5rem 0.125rem 0.25rem;
+ font-weight: bold;
+ color: var(--color-text-light);
+}
+.dropdown-menu .dropdown-section .item a {
+ padding: 0 22px;
+}
+.dropdown-menu .dropdown-section .item a:hover {
+ background-color: var(--color-background-nav);
}
.dropdown-menu > .item {
- padding: 0;
- margin-left: 10px;
+ padding: 0 0 0 0.5rem;
}
.dropdown-menu > .item > a {
min-width: initial;
@@ -355,12 +342,32 @@ form th {
font-weight: bold;
margin: 0 0 0 -14px;
}
+.dropdown-menu .help a {
+ color: var(--color-text-light);
+ text-decoration: underline;
+ text-decoration-style: dotted;
+}
+.dropdown-menu .help a:hover {
+ text-decoration-style: solid;
+}
.dropdown-menu .input select,
.dropdown-menu .input input {
margin: 0 auto 5px;
padding: 2px 5px;
}
+#dropdown-search-wrapper .dropdown-menu {
+ padding-top: 1rem;
+ padding-bottom: 0.25rem;
+}
+#dropdown-search-wrapper .dropdown-menu .stick.search {
+ width: 100%;
+}
+#dropdown-search-wrapper .dropdown-menu .stick.search input[type=search] {
+ width: 100%;
+ border: 0;
+}
+
.labels .dropdown-menu,
.tags .dropdown-menu,
.share .dropdown-menu {
@@ -374,8 +381,7 @@ form th {
}
.alert {
- margin: 5px auto;
- padding: 10px 15px;
+ margin: 0.25rem auto;
background-color: var(--color-background-light);
color: var(--color-text-light-darker);
font-size: 0.9em;
@@ -496,7 +502,6 @@ form th {
.box .box-content .item {
padding: 0 10px;
font-size: 0.9rem;
- line-height: 2.5;
}
.box .box-content .item .configure .icon {
vertical-align: middle;
@@ -556,8 +561,6 @@ form th {
background-color: var(--color-background-aside);
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 3;
font-size: 0.8rem;
}
.tree-folder-items > .item.active {
@@ -572,12 +575,19 @@ form th {
height: auto;
}
.header > .item {
+ padding: 0;
vertical-align: middle;
}
.header > .item.title {
position: absolute;
text-align: center;
}
+.header > .item.title a {
+ padding: 0 1rem;
+}
+.header > .item.title a:hover .logo {
+ filter: grayscale(100%) brightness(100) opacity(90%);
+}
.header > .item.title .logo {
display: inline-block;
height: 26px;
@@ -586,9 +596,6 @@ form th {
top: 5px;
filter: grayscale(100%) brightness(100);
}
-.header .item.search input:focus {
- width: 350px;
-}
.header .item.search {
display: none;
}
@@ -724,9 +731,11 @@ form th {
}
.nav_menu {
+ padding: 0;
width: 100%;
font-size: 0;
background-color: var(--color-background-nav);
+ text-align: left;
position: sticky;
top: 0;
z-index: 90;
@@ -754,11 +763,11 @@ form th {
width: 90%;
border-top: 1px solid var(--color-border-light-darker);
}
-.flux:hover,
+.flux .flux_header:hover,
.flux .current {
background-color: var(--color-background-hover);
}
-.flux:hover:not(.current):hover .item.title,
+.flux .flux_header:hover:not(.current):hover .item.title,
.flux .current:not(.current):hover .item.title {
background-color: var(--color-background-hover);
}
@@ -825,9 +834,6 @@ form th {
.notification.bad a.close:hover {
background-color: var(--color-background-bad);
}
-.notification#actualizeProgress {
- line-height: 2;
-}
.notification a.close {
display: none;
}
@@ -926,6 +932,9 @@ a.signin {
.dropdown {
position: relative;
}
+ .dropdown .dropdown-menu {
+ width: auto;
+ }
#new-article {
margin-top: 2rem;
width: 100%;
@@ -1166,10 +1175,21 @@ button.as-link {
right: auto;
}
+#nav_menu_actions ul.dropdown-menu::after {
+ display: none;
+}
+
+#nav_menu_actions .dropdown.only-mobile {
+ display: initial !important;
+}
+
#nav_menu_read_all ul.dropdown-menu {
right: 0;
left: auto;
}
+#nav_menu_read_all ul.dropdown-menu::after {
+ display: none;
+}
#slider label {
min-height: initial;
diff --git a/p/themes/Swage/swage.rtl.css b/p/themes/Swage/swage.rtl.css
index 0c3e52d20..a4ba4f159 100644
--- a/p/themes/Swage/swage.rtl.css
+++ b/p/themes/Swage/swage.rtl.css
@@ -54,13 +54,12 @@ select:invalid {
box-shadow: none;
}
-.nav-list .item, .nav-list .nav-header {
- height: 2.5em;
+.nav-list .item .nav-header, .nav-list .item {
+ min-height: 2.5em;
line-height: 2.5;
- font-size: 0.9rem;
}
-.dropdown-menu > .item > a,
+.dropdown-menu > .item a,
.dropdown-menu > .item > span,
.dropdown-menu > .item > .as-link,
.dropdown-menu > .item button, .dropdown-menu > .item {
@@ -71,7 +70,6 @@ select:invalid {
}
.flux::after, .form-group::after {
- content: "";
display: block;
clear: both;
}
@@ -87,7 +85,7 @@ body {
}
a {
- color: var(--color-nav-lighter);
+ color: var(--color-text-nav);
outline: none;
}
a.btn {
@@ -150,10 +148,6 @@ select {
padding-bottom: 8px;
}
-input.extend {
- transition: width 200ms linear;
-}
-
option {
padding: 0 0.5em;
}
@@ -162,10 +156,8 @@ table {
border-collapse: collapse;
}
-tr,
td,
th {
- padding: 0.5em;
border: 1px solid var(--color-border-light-darker);
}
@@ -266,7 +258,10 @@ form th {
background-image: url("./icons/disabled-light.svg");
}
-.nav-list .nav-header {
+.nav-list {
+ font-size: 0.9rem;
+}
+.nav-list .item .nav-header {
padding: 0 1rem;
font-weight: bold;
background-color: var(--color-background-aside);
@@ -284,25 +279,9 @@ form th {
.nav-list .item.active a {
color: var(--color-text-light);
}
-.nav-list .item.active.empty a,
-.nav-list .item.active .error a {
- color: var(--color-text-light);
-}
-.nav-list .item.active.empty a {
- background-color: var(--color-background-alert);
-}
-.nav-list .item.active.error a {
- background-color: var(--color-background-bad);
-}
.nav-list .item > a {
padding: 0 1.5rem;
}
-.nav-list .item.empty a {
- color: var(--color-text-alert);
-}
-.nav-list .item.error a {
- color: var(--color-text-bad-lighter);
-}
.nav-list .item .icon {
filter: brightness(3);
}
@@ -335,12 +314,20 @@ form th {
.dropdown-menu .dropdown-header a:hover {
background-color: var(--color-background-nav);
}
-.dropdown-menu::after {
- content: none;
+.dropdown-menu .dropdown-section .dropdown-section-title {
+ cursor: default;
+ padding: 0.25rem 0.25rem 0.125rem 0.5rem;
+ font-weight: bold;
+ color: var(--color-text-light);
+}
+.dropdown-menu .dropdown-section .item a {
+ padding: 0 22px;
+}
+.dropdown-menu .dropdown-section .item a:hover {
+ background-color: var(--color-background-nav);
}
.dropdown-menu > .item {
- padding: 0;
- margin-right: 10px;
+ padding: 0 0.5rem 0 0;
}
.dropdown-menu > .item > a {
min-width: initial;
@@ -355,12 +342,32 @@ form th {
font-weight: bold;
margin: 0 -14px 0 0;
}
+.dropdown-menu .help a {
+ color: var(--color-text-light);
+ text-decoration: underline;
+ text-decoration-style: dotted;
+}
+.dropdown-menu .help a:hover {
+ text-decoration-style: solid;
+}
.dropdown-menu .input select,
.dropdown-menu .input input {
margin: 0 auto 5px;
padding: 2px 5px;
}
+#dropdown-search-wrapper .dropdown-menu {
+ padding-top: 1rem;
+ padding-bottom: 0.25rem;
+}
+#dropdown-search-wrapper .dropdown-menu .stick.search {
+ width: 100%;
+}
+#dropdown-search-wrapper .dropdown-menu .stick.search input[type=search] {
+ width: 100%;
+ border: 0;
+}
+
.labels .dropdown-menu,
.tags .dropdown-menu,
.share .dropdown-menu {
@@ -374,8 +381,7 @@ form th {
}
.alert {
- margin: 5px auto;
- padding: 10px 15px;
+ margin: 0.25rem auto;
background-color: var(--color-background-light);
color: var(--color-text-light-darker);
font-size: 0.9em;
@@ -496,7 +502,6 @@ form th {
.box .box-content .item {
padding: 0 10px;
font-size: 0.9rem;
- line-height: 2.5;
}
.box .box-content .item .configure .icon {
vertical-align: middle;
@@ -556,8 +561,6 @@ form th {
background-color: var(--color-background-aside);
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 3;
font-size: 0.8rem;
}
.tree-folder-items > .item.active {
@@ -572,12 +575,19 @@ form th {
height: auto;
}
.header > .item {
+ padding: 0;
vertical-align: middle;
}
.header > .item.title {
position: absolute;
text-align: center;
}
+.header > .item.title a {
+ padding: 0 1rem;
+}
+.header > .item.title a:hover .logo {
+ filter: grayscale(100%) brightness(100) opacity(90%);
+}
.header > .item.title .logo {
display: inline-block;
height: 26px;
@@ -586,9 +596,6 @@ form th {
top: 5px;
filter: grayscale(100%) brightness(100);
}
-.header .item.search input:focus {
- width: 350px;
-}
.header .item.search {
display: none;
}
@@ -724,9 +731,11 @@ form th {
}
.nav_menu {
+ padding: 0;
width: 100%;
font-size: 0;
background-color: var(--color-background-nav);
+ text-align: right;
position: sticky;
top: 0;
z-index: 90;
@@ -754,11 +763,11 @@ form th {
width: 90%;
border-top: 1px solid var(--color-border-light-darker);
}
-.flux:hover,
+.flux .flux_header:hover,
.flux .current {
background-color: var(--color-background-hover);
}
-.flux:hover:not(.current):hover .item.title,
+.flux .flux_header:hover:not(.current):hover .item.title,
.flux .current:not(.current):hover .item.title {
background-color: var(--color-background-hover);
}
@@ -825,9 +834,6 @@ form th {
.notification.bad a.close:hover {
background-color: var(--color-background-bad);
}
-.notification#actualizeProgress {
- line-height: 2;
-}
.notification a.close {
display: none;
}
@@ -926,6 +932,9 @@ a.signin {
.dropdown {
position: relative;
}
+ .dropdown .dropdown-menu {
+ width: auto;
+ }
#new-article {
margin-top: 2rem;
width: 100%;
@@ -1166,10 +1175,21 @@ button.as-link {
left: auto;
}
+#nav_menu_actions ul.dropdown-menu::after {
+ display: none;
+}
+
+#nav_menu_actions .dropdown.only-mobile {
+ display: initial !important;
+}
+
#nav_menu_read_all ul.dropdown-menu {
left: 0;
right: auto;
}
+#nav_menu_read_all ul.dropdown-menu::after {
+ display: none;
+}
#slider label {
min-height: initial;
diff --git a/p/themes/Swage/swage.scss b/p/themes/Swage/swage.scss
index 8f61015c5..cc7aed10e 100644
--- a/p/themes/Swage/swage.scss
+++ b/p/themes/Swage/swage.scss
@@ -76,9 +76,8 @@ $color_hover: #fff;
}
%nav-list {
- height: 2.5em;
+ min-height: 2.5em;
line-height: 2.5;
- font-size: 0.9rem;
}
%dropdown {
@@ -89,7 +88,6 @@ $color_hover: #fff;
}
%after {
- content: "";
display: block;
clear: both;
}
@@ -106,7 +104,7 @@ body {
}
a {
- color: var(--color-nav-lighter);
+ color: var(--color-text-nav);
outline: none;
&.btn {
@@ -188,12 +186,6 @@ select {
padding-bottom: 8px;
}
-input {
- &.extend {
- transition: width 200ms linear;
- }
-}
-
option {
padding: 0 .5em;
}
@@ -202,10 +194,8 @@ table {
border-collapse: collapse;
}
-tr,
td,
th {
- padding: 0.5em;
border: 1px solid var(--color-border-light-darker);
}
@@ -341,20 +331,22 @@ form {
}
.nav-list {
- .nav-header {
-
- @extend %nav-list;
- padding: 0 1rem;
- font-weight: bold;
- background-color: var(--color-background-aside);
- color: var(--color-text-light);
- cursor: default;
- }
+ font-size: 0.9rem;
.item {
@extend %nav-list;
+ .nav-header {
+
+ @extend %nav-list;
+ padding: 0 1rem;
+ font-weight: bold;
+ background-color: var(--color-background-aside);
+ color: var(--color-text-light);
+ cursor: default;
+ }
+
a:hover {
background-color: var(--color-background-nav-darker);
color: var(--color-text-light);
@@ -367,33 +359,12 @@ form {
a {
color: var(--color-text-light);
}
-
- &.empty a,
- .error a {
- color: var(--color-text-light);
- }
-
- &.empty a {
- background-color: var(--color-background-alert);
- }
-
- &.error a {
- background-color: var(--color-background-bad);
- }
}
> a {
padding: 0 1.5rem;
}
- &.empty a {
- color: var(--color-text-alert);
- }
-
- &.error a {
- color: var(--color-text-bad-lighter);
- }
-
.icon {
filter: brightness(3);
}
@@ -433,18 +404,32 @@ form {
}
}
- &::after {
- content: none;
+ .dropdown-section {
+ .dropdown-section-title {
+ cursor: default;
+ padding: 0.25rem 0.5rem 0.125rem 0.25rem;
+ font-weight: bold;
+ color: var(--color-text-light);
+ }
+
+ .item {
+ a {
+ padding: 0 22px;
+
+ &:hover {
+ background-color: var(--color-background-nav);
+ }
+ }
+ }
}
> {
.item {
@extend %dropdown;
- padding: 0;
- margin-left: 10px;
+ padding: 0 0 0 0.5rem;
- > a,
+ a,
> span,
> .as-link,
button {
@@ -470,6 +455,16 @@ form {
}
}
+ .help a {
+ color: var(--color-text-light);
+ text-decoration: underline;
+ text-decoration-style: dotted;
+
+ &:hover {
+ text-decoration-style: solid;
+ }
+ }
+
.input {
select,
input {
@@ -479,6 +474,20 @@ form {
}
}
+#dropdown-search-wrapper .dropdown-menu {
+ padding-top: 1rem;
+ padding-bottom: 0.25rem;
+
+ .stick.search {
+ width: 100%;
+
+ input[type="search"] {
+ width: 100%;
+ border: 0;
+ }
+ }
+}
+
.labels,
.tags,
.share {
@@ -494,8 +503,7 @@ form {
}
.alert {
- margin: 5px auto;
- padding: 10px 15px;
+ margin: 0.25rem auto;
background-color: var(--color-background-light);
color: var(--color-text-light-darker);
font-size: 0.9em;
@@ -632,7 +640,6 @@ form {
.item {
padding: 0 10px;
font-size: 0.9rem;
- line-height: 2.5;
.configure {
.icon {
@@ -708,8 +715,6 @@ form {
background-color: var(--color-background-aside);
> .item {
- padding: 0 10px;
- line-height: 3;
font-size: 0.8rem;
&.active {
@@ -727,6 +732,7 @@ form {
height: auto;
> .item {
+ padding: 0;
vertical-align: middle;
&.title {
@@ -735,6 +741,16 @@ form {
position: absolute;
text-align: center;
+ a {
+ padding: 0 1rem;
+
+ &:hover {
+ .logo {
+ filter: grayscale(100%) brightness(100) opacity(90%);
+ }
+ }
+ }
+
.logo {
display: inline-block;
height: 26px;
@@ -746,10 +762,6 @@ form {
}
}
- .item.search input:focus {
- width: 350px;
- }
-
.item.search {
display: none;
}
@@ -933,9 +945,11 @@ form {
.nav_menu {
+ padding: 0;
width: 100%;
font-size: 0;
background-color: var(--color-background-nav);
+ text-align: left;
position: sticky;
top: 0;
z-index: 90;
@@ -971,7 +985,7 @@ form {
border-top: 1px solid var(--color-border-light-darker);
}
- &:hover,
+ .flux_header:hover,
.current {
background-color: var(--color-background-hover);
@@ -1061,10 +1075,6 @@ form {
}
}
- &#actualizeProgress {
- line-height: 2;
- }
-
a.close {
display: none;
}
@@ -1185,6 +1195,10 @@ a.signin {
.dropdown {
position: relative;
+
+ .dropdown-menu {
+ width: auto;
+ }
}
#new-article {
@@ -1485,6 +1499,14 @@ button.as-link {
ul.dropdown-menu {
left: 0;
right: auto;
+
+ &::after {
+ display: none;
+ }
+ }
+
+ .dropdown.only-mobile {
+ display: initial !important;
}
}
@@ -1492,6 +1514,10 @@ button.as-link {
ul.dropdown-menu {
right: 0;
left: auto;
+
+ &::after {
+ display: none;
+ }
}
}
diff --git a/p/themes/base-theme/base.css b/p/themes/base-theme/base.css
index 22bc5a5e7..095ef49c0 100644
--- a/p/themes/base-theme/base.css
+++ b/p/themes/base-theme/base.css
@@ -52,17 +52,12 @@ input:invalid, select:invalid {
input:disabled, select:disabled {
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
}
th {
@@ -200,18 +195,6 @@ a.btn {
text-decoration: none;
}
-.nav-list .item.empty a {
-}
-
-.nav-list .item.active.empty a {
-}
-
-.nav-list .item.error a {
-}
-
-.nav-list .item.active.error a {
-}
-
.nav-list .nav-header {
padding: 0 10px;
font-weight: bold;
@@ -231,7 +214,6 @@ a.btn {
}
.dropdown-menu::after {
- content: "";
position: absolute;
top: -6px;
right: 13px;
@@ -277,8 +259,6 @@ a.btn {
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
font-size: 0.9em;
}
@@ -386,8 +366,6 @@ a.btn {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 3.1;
font-size: 0.8rem;
}
@@ -409,15 +387,10 @@ a.btn {
}
.header > .item {
- padding: 10px;
vertical-align: middle;
text-align: center;
}
-.header > .item.title {
- width: 230px;
-}
-
.header > .item.title h1 {
margin: 0.5em 0;
}
@@ -427,10 +400,6 @@ a.btn {
}
.header > .item.search input {
- width: 230px;
-}
-
-.header .item.search input:focus {
width: 350px;
}
@@ -658,13 +627,6 @@ a.btn {
/*=== Notification and actualize notification */
.notification {
- padding: 0 0 0 5px;
- text-align: center;
- font-weight: bold;
- font-size: 0.9em;
- line-height: 3;
- z-index: 10;
- vertical-align: middle;
}
.notification.good {
@@ -673,21 +635,12 @@ a.btn {
.notification.bad {
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3;
-}
-
.notification.good a.close:hover {
}
.notification.bad a.close:hover {
}
-.notification#actualizeProgress {
- line-height: 2;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
text-align: center;
diff --git a/p/themes/base-theme/base.rtl.css b/p/themes/base-theme/base.rtl.css
index d03906a90..a97d4876c 100644
--- a/p/themes/base-theme/base.rtl.css
+++ b/p/themes/base-theme/base.rtl.css
@@ -52,17 +52,12 @@ input:invalid, select:invalid {
input:disabled, select:disabled {
}
-input.extend {
- transition: width 200ms linear;
-}
-
/*=== Tables */
table {
border-collapse: collapse;
}
-tr, th, td {
- padding: 0.5em;
+th, td {
}
th {
@@ -200,18 +195,6 @@ a.btn {
text-decoration: none;
}
-.nav-list .item.empty a {
-}
-
-.nav-list .item.active.empty a {
-}
-
-.nav-list .item.error a {
-}
-
-.nav-list .item.active.error a {
-}
-
.nav-list .nav-header {
padding: 0 10px;
font-weight: bold;
@@ -231,7 +214,6 @@ a.btn {
}
.dropdown-menu::after {
- content: "";
position: absolute;
top: -6px;
left: 13px;
@@ -277,8 +259,6 @@ a.btn {
/*=== Alerts */
.alert {
- margin: 15px auto;
- padding: 10px 15px;
font-size: 0.9em;
}
@@ -386,8 +366,6 @@ a.btn {
}
.tree-folder-items > .item {
- padding: 0 10px;
- line-height: 3.1;
font-size: 0.8rem;
}
@@ -409,15 +387,10 @@ a.btn {
}
.header > .item {
- padding: 10px;
vertical-align: middle;
text-align: center;
}
-.header > .item.title {
- width: 230px;
-}
-
.header > .item.title h1 {
margin: 0.5em 0;
}
@@ -427,10 +400,6 @@ a.btn {
}
.header > .item.search input {
- width: 230px;
-}
-
-.header .item.search input:focus {
width: 350px;
}
@@ -658,13 +627,6 @@ a.btn {
/*=== Notification and actualize notification */
.notification {
- padding: 0 5px 0 0;
- text-align: center;
- font-weight: bold;
- font-size: 0.9em;
- line-height: 3;
- z-index: 10;
- vertical-align: middle;
}
.notification.good {
@@ -673,21 +635,12 @@ a.btn {
.notification.bad {
}
-.notification a.close {
- padding: 0 15px;
- line-height: 3;
-}
-
.notification.good a.close:hover {
}
.notification.bad a.close:hover {
}
-.notification#actualizeProgress {
- line-height: 2;
-}
-
/*=== "Load more" part */
#bigMarkAsRead {
text-align: center;
diff --git a/p/themes/base-theme/frss.css b/p/themes/base-theme/frss.css
index e0ef320e1..6a5ef8b87 100644
--- a/p/themes/base-theme/frss.css
+++ b/p/themes/base-theme/frss.css
@@ -39,6 +39,7 @@
--frss-loading-image: url("loader.gif");
--frss-padding-flux-items: 0.75rem;
+ --frss-padding-top-bottom: 0.5rem;
line-height: 1.5;
}
@@ -114,10 +115,13 @@ h3 {
display: none;
}
+.only-mobile {
+ display: none !important;
+}
+
/*=== Paragraphs */
p {
margin: 1rem 0 0.5rem;
- font-size: 1rem;
}
p.help, .prompt p.help {
@@ -194,7 +198,8 @@ label {
}
input {
- width: 180px;
+ max-width: 90%;
+ width: 300px;
}
input[type=number] {
@@ -203,8 +208,7 @@ input[type=number] {
textarea,
input[type="file"],
-input.long,
-input.extend:focus {
+input.long {
width: 300px;
}
@@ -212,6 +216,11 @@ input, select, textarea {
display: inline-block;
max-width: 100%;
font-size: 0.8rem;
+ box-sizing: border-box;
+}
+
+select {
+ min-width: 6em;
}
input.w50,
@@ -286,8 +295,8 @@ input[type="checkbox"] {
width: calc(99% - 5em);
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover {
text-decoration: none;
}
@@ -317,7 +326,6 @@ button.as-link:hover,
button.as-link:active {
background: transparent;
color: inherit;
- font-size: 1.1rem;
border: none;
cursor: pointer;
text-align: left;
@@ -333,9 +341,14 @@ button.as-link[disabled] {
}
table {
+ margin: 0.5rem 0;
max-width: 100%;
}
+th, td {
+ padding: 0.5rem;
+}
+
th.numeric,
td.numeric {
text-align: center;
@@ -389,10 +402,11 @@ td.numeric {
display: block;
float: left;
width: 200px;
+ text-align: right;
}
.form-group .group-controls {
- min-width: 250px;
+ min-width: 200px;
margin: 0 0 0 220px;
overflow-x: auto;
}
@@ -437,7 +451,7 @@ td.numeric {
flex-shrink: 0;
}
-.stick form {
+#nav_menu_read_all form {
display: inline-flex;
}
@@ -554,6 +568,10 @@ input[type="checkbox"]:focus-visible {
}
/*=== Navigation */
+.nav-list {
+ padding-bottom: 3rem;
+}
+
.nav-list .nav-header,
.nav-list .item {
display: block;
@@ -583,7 +601,7 @@ input[type="checkbox"]:focus-visible {
}
.horizontal-list .item .item-element {
- padding: 0.5rem 0;
+ padding: var(--frss-padding-top-bottom) 0;
}
/*=== manage-list */
@@ -649,13 +667,13 @@ input[type="checkbox"]:focus-visible {
display: block;
}
-.dropdown-menu > .item {
+.dropdown-menu .item {
display: block;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > .as-link,
-.dropdown-menu > .item > span {
+.dropdown-menu .item > a,
+.dropdown-menu .item > .as-link,
+.dropdown-menu .item > span {
display: block;
width: 100%;
white-space: nowrap;
@@ -696,6 +714,7 @@ input[type="checkbox"]:focus-visible {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
margin-top: 5px;
padding-top: 5px;
@@ -705,6 +724,8 @@ input[type="checkbox"]:focus-visible {
/*=== Alerts */
.alert {
+ margin: 1rem auto;
+ padding: 0.75rem 1rem;
display: block;
width: 90%;
}
@@ -826,6 +847,7 @@ input[type="checkbox"]:focus-visible {
.box .box-content .item.feed {
display: block;
+ padding-top: var(--frss-padding-top-bottom);
}
.box .box-content .item.feed.moved {
@@ -955,6 +977,11 @@ li.drag-hover {
transition: max-height .3s linear;
}
+.tree-folder-title {
+ padding-top: var(--frss-padding-top-bottom);
+ padding-bottom: var(--frss-padding-top-bottom);
+}
+
.tree-folder-title .title {
display: inline-block;
width: 100%;
@@ -993,16 +1020,19 @@ li.drag-hover {
.header {
display: table;
width: 100%;
- height: 85px;
+ height: calc(2.5rem + 2 * var(--frss-padding-top-bottom));
table-layout: fixed;
}
.header > .item {
+ padding-top: var(--frss-padding-top-bottom);
+ padding-bottom: var(--frss-padding-top-bottom);
display: table-cell;
}
.header > .item.title {
- width: 250px;
+ width: 300px;
+ text-align: center;
white-space: nowrap;
}
@@ -1010,12 +1040,22 @@ li.drag-hover {
display: inline-block;
}
+.header > .item.title a {
+ padding: 0.25rem 1rem;
+ display: inline-block;
+}
+
.header > .item.title .logo {
display: inline-block;
- height: 32px;
+ height: 2rem;
vertical-align: middle;
}
+.header > .item.title a:hover .logo {
+ filter: brightness(1.4);
+ transition: filter 0.1s linear;
+}
+
.header > .item.configure {
width: 100px;
}
@@ -1029,7 +1069,7 @@ input[type="search"] {
background: inherit;
display: table;
width: 100%;
- height: calc(100vh - 85px);
+ height: calc(100vh - (calc(2.5rem + 2 * var(--frss-padding-top-bottom) + 1px)));
table-layout: fixed;
}
@@ -1061,7 +1101,7 @@ input[type="search"] {
}
.aside_feed .tree-folder-items .item.feed {
- padding: 0 0.75rem;
+ padding: var(--frss-padding-top-bottom) 0.75rem;
}
.aside_feed .tree-folder-items:not(.active) {
@@ -1104,6 +1144,7 @@ input[type="search"] {
}
#new-article > a {
+ padding: calc(0.25rem + var(--frss-padding-top-bottom)) 1rem;
display: block;
}
@@ -1132,7 +1173,7 @@ input[type="search"] {
}
.flux .flux_header .item .item-element {
- padding: 0.5rem 0;
+ padding: var(--frss-padding-top-bottom) 0;
line-height: 1.5rem;
}
@@ -1163,7 +1204,7 @@ a.website:hover .favicon {
}
.flux:not(.current):hover .item.title {
- background-color: var(--frss-background-color);
+ background-color: inherit;
max-width: calc(100% - 320px);
position: absolute;
}
@@ -1179,10 +1220,14 @@ a.website:hover .favicon {
.flux .item.thumbnail {
line-height: 0;
- padding: 0.75rem;
+ padding: var(--frss-padding-top-bottom) var(--frss-padding-flux-items);
height: 80px;
}
+.flux .item.thumbnail .item-element {
+ padding: 0;
+}
+
.flux .item.thumbnail.small {
height: 40px;
}
@@ -1225,6 +1270,7 @@ a.website:hover .favicon {
color: var(--frss-font-color-grey-dark);
font-size: 0.9rem;
font-weight: normal;
+ overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
@@ -1272,16 +1318,38 @@ a.website:hover .favicon {
word-wrap: break-word;
}
+.content .text {
+ overflow-x: auto;
+}
+
+.content .text div {
+ overflow-x: auto;
+}
+
+.content header,
+.content .text,
+.content footer {
+ padding: 0 3rem;
+}
+
+.content header {
+ padding-top: calc(2 * var(--frss-padding-top-bottom));
+}
+
+.content footer {
+ padding-bottom: calc(2 * var(--frss-padding-top-bottom));
+}
+
.content.large {
- max-width: 1000px;
+ max-width: 1100px;
}
.content.medium {
- max-width: 800px;
+ max-width: 900px;
}
.content.thin {
- max-width: 550px;
+ max-width: 650px;
}
.content .article-header-topline {
@@ -1295,14 +1363,13 @@ a.website:hover .favicon {
}
.content > footer {
- margin: 2rem 0 2rem;
padding-top: 1rem;
- border-top: 2px solid var(--frss-border-color);
clear: both;
}
.content > footer .subtitle {
- padding-bottom: 1rem;
+ padding: 2rem 0 1rem;
+ border-top: 2px solid var(--frss-border-color);
}
.content > header .tags,
@@ -1312,8 +1379,9 @@ a.website:hover .favicon {
}
.content > header .tags .icon,
+.content > header .website .favicon,
.content > footer .tags .icon {
- padding: 0 1rem 0 0;
+ margin: 0 0.5rem 0 0;
line-height: 1.5;
}
@@ -1362,15 +1430,19 @@ a.website:hover .favicon {
/*=== Notification and actualize notification */
.notification {
- padding: 10px 50px 10px 10px;
+ padding: 0.75rem 3.5rem 0.75rem 0.75rem;
position: absolute;
- top: 1em;
+ top: 1rem;
left: 25%; right: 25%;
z-index: 9999;
background-color: var(--frss-background-color);
+ font-weight: bold;
+ font-size: 0.9rem;
border: 1px solid var(--frss-border-color);
opacity: 1;
+ text-align: center;
line-height: 2;
+ vertical-align: middle;
visibility: visible;
transition: visibility 0s, opacity .3s linear;
}
@@ -1381,10 +1453,12 @@ a.website:hover .favicon {
}
.notification a.close {
+ padding: 0 1rem;
position: absolute;
top: 0; bottom: 0;
right: 0;
display: inline-block;
+ line-height: 3;
}
.notification a.close:hover {
@@ -1771,18 +1845,23 @@ input:checked + .slide-container .properties {
.item.share.error a::after,
.category .title.error::before,
-.item.feed.error .item-title::before {
+.item.feed.error .item-title::before,
+.properties .error::before {
content: " ⚠ ";
color: var(--frss-font-color-error);
}
+.deprecated {
+ font-weight: bold;
+}
+
.feed.item.error.active .item-title::before {
color: var(--frss-font-color-light);
}
.aside .category .title:not([data-unread="0"])::after,
.aside .feed .item-title:not([data-unread="0"])::after {
- margin: 0.6rem 0 0 0;
+ margin: calc(0.125rem + var(--frss-padding-top-bottom)) 0 0 0;
padding: 0.25rem 0.5rem;
min-width: 2rem;
display: block;
@@ -1833,6 +1912,26 @@ input:checked + .slide-container .properties {
margin: 1em 0 0 0;
}
+#stream.global .feed {
+ position: relative;
+}
+
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ margin: 0.5rem 0px 0px;
+ padding: 5px 10px;
+ min-width: 20px;
+ display: block;
+ content: attr(data-unread);
+ position: absolute;
+ top: 0px;
+ right: 0px;
+ text-align: center;
+ font-size: 0.75rem;
+ border-radius: 12px;
+ line-height: 1;
+ font-weight: initial;
+}
+
.feed.active .item-title:not([data-unread="0"])::after {
color: var(--frss-font-color-light);
border: 1px solid var(--frss-border-color);
@@ -1850,7 +1949,10 @@ input:checked + .slide-container .properties {
}
.nav_menu {
+ padding-top: var(--frss-padding-top-bottom);
+ padding-bottom: var(--frss-padding-top-bottom);
background: inherit;
+ text-align: center;
}
.nav_mobile {
@@ -1858,7 +1960,6 @@ input:checked + .slide-container .properties {
}
.nav-login,
-.nav_menu .search,
.aside .toggle_aside,
#slider .toggle_aside,
.nav_menu .toggle_aside,
@@ -1909,7 +2010,7 @@ input:checked + .slide-container .properties {
}
.reader .flux .content {
- padding: 3rem;
+ padding: 3rem 0;
background-color: var(--frss-background-color);
border: 1px solid var(--frss-border-color);
}
@@ -1933,10 +2034,19 @@ input:checked + .slide-container .properties {
display: none;
}
+ .only-mobile {
+ display: unset !important;
+ }
+
.header > .item {
padding: 5px;
}
+ .header > .item.title {
+ width: 75%;
+ text-align: left;
+ }
+
.header > .item.title .logo {
height: 24px;
}
@@ -1954,7 +2064,7 @@ input:checked + .slide-container .properties {
#panel .close,
.dropdown-menu .toggle_aside,
#slider .toggle_aside {
- padding: 1rem;
+ padding: 1rem 0;
display: block;
width: 100%;
border-bottom: 1px solid var(--frss-border-color);
@@ -1968,6 +2078,7 @@ input:checked + .slide-container .properties {
.form-group .group-name {
float: none;
width: auto;
+ text-align: left;
}
.form-group .group-controls {
@@ -1978,7 +2089,8 @@ input:checked + .slide-container .properties {
position: inherit;
}
- .dropdown .dropdown-header {
+ .dropdown .dropdown-header,
+ .dropdown .dropdown-section {
line-height: 2;
}
@@ -1994,6 +2106,23 @@ input:checked + .slide-container .properties {
margin: 2px 0;
}
+ .dropdown .dropdown-menu .item .stick .btn {
+ margin: 0;
+ }
+
+ .dropdown .dropdown-menu .item form {
+ display: block;
+ text-align: center;
+ }
+
+ .dropdown .dropdown-menu .item .stick.search {
+ width: calc(100% - 20px);
+ }
+
+ .dropdown .dropdown-menu .item .stick.search input {
+ width: 95%;
+ }
+
.dropdown .dropdown-menu .item button.as-link,
.dropdown .dropdown-menu .item button.as-link:hover, button.as-link:active {
width: 100%;
@@ -2018,7 +2147,7 @@ input:checked + .slide-container .properties {
}
.dropdown-target:target ~ .dropdown-toggle:not(.btn) ~ .dropdown-menu {
- margin-top: 0;
+ margin-top: 10px;
}
.configure .dropdown .dropdown-menu {
@@ -2056,7 +2185,6 @@ input:checked + .slide-container .properties {
}
.nav_menu .toggle_aside,
- .nav_menu .search,
#panel .close img {
display: inline-block;
}
@@ -2109,7 +2237,22 @@ input:checked + .slide-container .properties {
top: 0;
}
+ .content header,
+ .content .text,
+ .content footer {
+ padding: 1rem;
+ }
+
+ table {
+ font-size: 0.9rem;
+ }
+
+ th, td {
+ padding: 0.25rem;
+ }
+
.notification {
+ padding: 0.75rem;
top: 0;
left: 0;
right: 0;
diff --git a/p/themes/base-theme/frss.rtl.css b/p/themes/base-theme/frss.rtl.css
index 1755291bd..1d818cf1a 100644
--- a/p/themes/base-theme/frss.rtl.css
+++ b/p/themes/base-theme/frss.rtl.css
@@ -39,6 +39,7 @@
--frss-loading-image: url("loader.gif");
--frss-padding-flux-items: 0.75rem;
+ --frss-padding-top-bottom: 0.5rem;
line-height: 1.5;
}
@@ -114,10 +115,13 @@ h3 {
display: none;
}
+.only-mobile {
+ display: none !important;
+}
+
/*=== Paragraphs */
p {
margin: 1rem 0 0.5rem;
- font-size: 1rem;
}
p.help, .prompt p.help {
@@ -194,7 +198,8 @@ label {
}
input {
- width: 180px;
+ max-width: 90%;
+ width: 300px;
}
input[type=number] {
@@ -203,8 +208,7 @@ input[type=number] {
textarea,
input[type="file"],
-input.long,
-input.extend:focus {
+input.long {
width: 300px;
}
@@ -212,6 +216,11 @@ input, select, textarea {
display: inline-block;
max-width: 100%;
font-size: 0.8rem;
+ box-sizing: border-box;
+}
+
+select {
+ min-width: 6em;
}
input.w50,
@@ -286,8 +295,8 @@ input[type="checkbox"] {
width: calc(99% - 5em);
}
-.dropdown-menu > .item > a:hover,
-.dropdown-menu > .item > button:hover {
+.dropdown-menu .item > a:hover,
+.dropdown-menu .item > button:hover {
text-decoration: none;
}
@@ -317,7 +326,6 @@ button.as-link:hover,
button.as-link:active {
background: transparent;
color: inherit;
- font-size: 1.1rem;
border: none;
cursor: pointer;
text-align: right;
@@ -333,9 +341,14 @@ button.as-link[disabled] {
}
table {
+ margin: 0.5rem 0;
max-width: 100%;
}
+th, td {
+ padding: 0.5rem;
+}
+
th.numeric,
td.numeric {
text-align: center;
@@ -389,10 +402,11 @@ td.numeric {
display: block;
float: right;
width: 200px;
+ text-align: left;
}
.form-group .group-controls {
- min-width: 250px;
+ min-width: 200px;
margin: 0 220px 0 0;
overflow-x: auto;
}
@@ -437,7 +451,7 @@ td.numeric {
flex-shrink: 0;
}
-.stick form {
+#nav_menu_read_all form {
display: inline-flex;
}
@@ -554,6 +568,10 @@ input[type="checkbox"]:focus-visible {
}
/*=== Navigation */
+.nav-list {
+ padding-bottom: 3rem;
+}
+
.nav-list .nav-header,
.nav-list .item {
display: block;
@@ -583,7 +601,7 @@ input[type="checkbox"]:focus-visible {
}
.horizontal-list .item .item-element {
- padding: 0.5rem 0;
+ padding: var(--frss-padding-top-bottom) 0;
}
/*=== manage-list */
@@ -649,13 +667,13 @@ input[type="checkbox"]:focus-visible {
display: block;
}
-.dropdown-menu > .item {
+.dropdown-menu .item {
display: block;
}
-.dropdown-menu > .item > a,
-.dropdown-menu > .item > .as-link,
-.dropdown-menu > .item > span {
+.dropdown-menu .item > a,
+.dropdown-menu .item > .as-link,
+.dropdown-menu .item > span {
display: block;
width: 100%;
white-space: nowrap;
@@ -696,6 +714,7 @@ input[type="checkbox"]:focus-visible {
}
.item ~ .dropdown-header,
+.dropdown-section ~ .dropdown-section,
.item.separator {
margin-top: 5px;
padding-top: 5px;
@@ -705,6 +724,8 @@ input[type="checkbox"]:focus-visible {
/*=== Alerts */
.alert {
+ margin: 1rem auto;
+ padding: 0.75rem 1rem;
display: block;
width: 90%;
}
@@ -826,6 +847,7 @@ input[type="checkbox"]:focus-visible {
.box .box-content .item.feed {
display: block;
+ padding-top: var(--frss-padding-top-bottom);
}
.box .box-content .item.feed.moved {
@@ -955,6 +977,11 @@ li.drag-hover {
transition: max-height .3s linear;
}
+.tree-folder-title {
+ padding-top: var(--frss-padding-top-bottom);
+ padding-bottom: var(--frss-padding-top-bottom);
+}
+
.tree-folder-title .title {
display: inline-block;
width: 100%;
@@ -993,16 +1020,19 @@ li.drag-hover {
.header {
display: table;
width: 100%;
- height: 85px;
+ height: calc(2.5rem + 2 * var(--frss-padding-top-bottom));
table-layout: fixed;
}
.header > .item {
+ padding-top: var(--frss-padding-top-bottom);
+ padding-bottom: var(--frss-padding-top-bottom);
display: table-cell;
}
.header > .item.title {
- width: 250px;
+ width: 300px;
+ text-align: center;
white-space: nowrap;
}
@@ -1010,12 +1040,22 @@ li.drag-hover {
display: inline-block;
}
+.header > .item.title a {
+ padding: 0.25rem 1rem;
+ display: inline-block;
+}
+
.header > .item.title .logo {
display: inline-block;
- height: 32px;
+ height: 2rem;
vertical-align: middle;
}
+.header > .item.title a:hover .logo {
+ filter: brightness(1.4);
+ transition: filter 0.1s linear;
+}
+
.header > .item.configure {
width: 100px;
}
@@ -1029,7 +1069,7 @@ input[type="search"] {
background: inherit;
display: table;
width: 100%;
- height: calc(100vh - 85px);
+ height: calc(100vh - (calc(2.5rem + 2 * var(--frss-padding-top-bottom) + 1px)));
table-layout: fixed;
}
@@ -1061,7 +1101,7 @@ input[type="search"] {
}
.aside_feed .tree-folder-items .item.feed {
- padding: 0 0.75rem;
+ padding: var(--frss-padding-top-bottom) 0.75rem;
}
.aside_feed .tree-folder-items:not(.active) {
@@ -1104,6 +1144,7 @@ input[type="search"] {
}
#new-article > a {
+ padding: calc(0.25rem + var(--frss-padding-top-bottom)) 1rem;
display: block;
}
@@ -1132,7 +1173,7 @@ input[type="search"] {
}
.flux .flux_header .item .item-element {
- padding: 0.5rem 0;
+ padding: var(--frss-padding-top-bottom) 0;
line-height: 1.5rem;
}
@@ -1163,7 +1204,7 @@ a.website:hover .favicon {
}
.flux:not(.current):hover .item.title {
- background-color: var(--frss-background-color);
+ background-color: inherit;
max-width: calc(100% - 320px);
position: absolute;
}
@@ -1179,10 +1220,14 @@ a.website:hover .favicon {
.flux .item.thumbnail {
line-height: 0;
- padding: 0.75rem;
+ padding: var(--frss-padding-top-bottom) var(--frss-padding-flux-items);
height: 80px;
}
+.flux .item.thumbnail .item-element {
+ padding: 0;
+}
+
.flux .item.thumbnail.small {
height: 40px;
}
@@ -1225,6 +1270,7 @@ a.website:hover .favicon {
color: var(--frss-font-color-grey-dark);
font-size: 0.9rem;
font-weight: normal;
+ overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
@@ -1272,16 +1318,38 @@ a.website:hover .favicon {
word-wrap: break-word;
}
+.content .text {
+ overflow-x: auto;
+}
+
+.content .text div {
+ overflow-x: auto;
+}
+
+.content header,
+.content .text,
+.content footer {
+ padding: 0 3rem;
+}
+
+.content header {
+ padding-top: calc(2 * var(--frss-padding-top-bottom));
+}
+
+.content footer {
+ padding-bottom: calc(2 * var(--frss-padding-top-bottom));
+}
+
.content.large {
- max-width: 1000px;
+ max-width: 1100px;
}
.content.medium {
- max-width: 800px;
+ max-width: 900px;
}
.content.thin {
- max-width: 550px;
+ max-width: 650px;
}
.content .article-header-topline {
@@ -1295,14 +1363,13 @@ a.website:hover .favicon {
}
.content > footer {
- margin: 2rem 0 2rem;
padding-top: 1rem;
- border-top: 2px solid var(--frss-border-color);
clear: both;
}
.content > footer .subtitle {
- padding-bottom: 1rem;
+ padding: 2rem 0 1rem;
+ border-top: 2px solid var(--frss-border-color);
}
.content > header .tags,
@@ -1312,8 +1379,9 @@ a.website:hover .favicon {
}
.content > header .tags .icon,
+.content > header .website .favicon,
.content > footer .tags .icon {
- padding: 0 0 0 1rem;
+ margin: 0 0 0 0.5rem;
line-height: 1.5;
}
@@ -1362,15 +1430,19 @@ a.website:hover .favicon {
/*=== Notification and actualize notification */
.notification {
- padding: 10px 10px 10px 50px;
+ padding: 0.75rem 0.75rem 0.75rem 3.5rem;
position: absolute;
- top: 1em;
+ top: 1rem;
right: 25%; left: 25%;
z-index: 9999;
background-color: var(--frss-background-color);
+ font-weight: bold;
+ font-size: 0.9rem;
border: 1px solid var(--frss-border-color);
opacity: 1;
+ text-align: center;
line-height: 2;
+ vertical-align: middle;
visibility: visible;
transition: visibility 0s, opacity .3s linear;
}
@@ -1381,10 +1453,12 @@ a.website:hover .favicon {
}
.notification a.close {
+ padding: 0 1rem;
position: absolute;
top: 0; bottom: 0;
left: 0;
display: inline-block;
+ line-height: 3;
}
.notification a.close:hover {
@@ -1771,18 +1845,23 @@ input:checked + .slide-container .properties {
.item.share.error a::after,
.category .title.error::before,
-.item.feed.error .item-title::before {
+.item.feed.error .item-title::before,
+.properties .error::before {
content: " ⚠ ";
color: var(--frss-font-color-error);
}
+.deprecated {
+ font-weight: bold;
+}
+
.feed.item.error.active .item-title::before {
color: var(--frss-font-color-light);
}
.aside .category .title:not([data-unread="0"])::after,
.aside .feed .item-title:not([data-unread="0"])::after {
- margin: 0.6rem 0 0 0;
+ margin: calc(0.125rem + var(--frss-padding-top-bottom)) 0 0 0;
padding: 0.25rem 0.5rem;
min-width: 2rem;
display: block;
@@ -1833,6 +1912,26 @@ input:checked + .slide-container .properties {
margin: 1em 0 0 0;
}
+#stream.global .feed {
+ position: relative;
+}
+
+#stream.global .feed .item-title:not([data-unread="0"])::after {
+ margin: 0.5rem 0px 0px;
+ padding: 5px 10px;
+ min-width: 20px;
+ display: block;
+ content: attr(data-unread);
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ text-align: center;
+ font-size: 0.75rem;
+ border-radius: 12px;
+ line-height: 1;
+ font-weight: initial;
+}
+
.feed.active .item-title:not([data-unread="0"])::after {
color: var(--frss-font-color-light);
border: 1px solid var(--frss-border-color);
@@ -1850,7 +1949,10 @@ input:checked + .slide-container .properties {
}
.nav_menu {
+ padding-top: var(--frss-padding-top-bottom);
+ padding-bottom: var(--frss-padding-top-bottom);
background: inherit;
+ text-align: center;
}
.nav_mobile {
@@ -1858,7 +1960,6 @@ input:checked + .slide-container .properties {
}
.nav-login,
-.nav_menu .search,
.aside .toggle_aside,
#slider .toggle_aside,
.nav_menu .toggle_aside,
@@ -1909,7 +2010,7 @@ input:checked + .slide-container .properties {
}
.reader .flux .content {
- padding: 3rem;
+ padding: 3rem 0;
background-color: var(--frss-background-color);
border: 1px solid var(--frss-border-color);
}
@@ -1933,10 +2034,19 @@ input:checked + .slide-container .properties {
display: none;
}
+ .only-mobile {
+ display: unset !important;
+ }
+
.header > .item {
padding: 5px;
}
+ .header > .item.title {
+ width: 75%;
+ text-align: right;
+ }
+
.header > .item.title .logo {
height: 24px;
}
@@ -1954,7 +2064,7 @@ input:checked + .slide-container .properties {
#panel .close,
.dropdown-menu .toggle_aside,
#slider .toggle_aside {
- padding: 1rem;
+ padding: 1rem 0;
display: block;
width: 100%;
border-bottom: 1px solid var(--frss-border-color);
@@ -1968,6 +2078,7 @@ input:checked + .slide-container .properties {
.form-group .group-name {
float: none;
width: auto;
+ text-align: right;
}
.form-group .group-controls {
@@ -1978,7 +2089,8 @@ input:checked + .slide-container .properties {
position: inherit;
}
- .dropdown .dropdown-header {
+ .dropdown .dropdown-header,
+ .dropdown .dropdown-section {
line-height: 2;
}
@@ -1994,6 +2106,23 @@ input:checked + .slide-container .properties {
margin: 2px 0;
}
+ .dropdown .dropdown-menu .item .stick .btn {
+ margin: 0;
+ }
+
+ .dropdown .dropdown-menu .item form {
+ display: block;
+ text-align: center;
+ }
+
+ .dropdown .dropdown-menu .item .stick.search {
+ width: calc(100% - 20px);
+ }
+
+ .dropdown .dropdown-menu .item .stick.search input {
+ width: 95%;
+ }
+
.dropdown .dropdown-menu .item button.as-link,
.dropdown .dropdown-menu .item button.as-link:hover, button.as-link:active {
width: 100%;
@@ -2018,7 +2147,7 @@ input:checked + .slide-container .properties {
}
.dropdown-target:target ~ .dropdown-toggle:not(.btn) ~ .dropdown-menu {
- margin-top: 0;
+ margin-top: 10px;
}
.configure .dropdown .dropdown-menu {
@@ -2056,7 +2185,6 @@ input:checked + .slide-container .properties {
}
.nav_menu .toggle_aside,
- .nav_menu .search,
#panel .close img {
display: inline-block;
}
@@ -2109,7 +2237,22 @@ input:checked + .slide-container .properties {
top: 0;
}
+ .content header,
+ .content .text,
+ .content footer {
+ padding: 1rem;
+ }
+
+ table {
+ font-size: 0.9rem;
+ }
+
+ th, td {
+ padding: 0.25rem;
+ }
+
.notification {
+ padding: 0.75rem;
top: 0;
right: 0;
left: 0;
diff --git a/p/themes/icons/bookmark.svg b/p/themes/icons/bookmark.svg
deleted file mode 100644
index 63a44908f..000000000
--- a/p/themes/icons/bookmark.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
-<g transform="translate(-41.000202,-397)">
-<path style="enable-background:accumulate;color:#000000;" d="m530.95,186.71c-0.77941,0.55189-3.1576-1.906-4.1125-1.9179-0.95532-0.0119-3.3949,2.3858-4.161,1.8149-0.76573-0.57072,0.83698-3.592,0.55319-4.5039-0.2839-0.91223-3.3182-2.4915-3.0119-3.3965,0.30617-0.90461,3.6749-0.31399,4.4544-0.86567,0.77986-0.5519,1.3442-3.9257,2.2995-3.914,0.95494,0.0116,1.4342,3.398,2.1998,3.9689,0.76588,0.57114,4.1489,0.0653,4.4331,0.97746,0.28402,0.9118-2.7885,2.414-3.0949,3.3186-0.30652,0.90489,1.22,3.966,0.44027,4.5182z" fill-rule="nonzero" transform="matrix(1.0472113,-0.00871584,0.00871584,1.0472113,-504.35434,220.15425)" fill="#f1c40f"/>
-</g>
-</svg> \ No newline at end of file
diff --git a/p/themes/icons/look.svg b/p/themes/icons/look.svg
index 79464d3ab..79464d3ab 100755..100644
--- a/p/themes/icons/look.svg
+++ b/p/themes/icons/look.svg
diff --git a/package-lock.json b/package-lock.json
index 26f72b172..5d15c2a3c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,47 +7,50 @@
"name": "freshrss",
"license": "AGPL-3.0",
"devDependencies": {
- "eslint": "^8.10.0",
+ "eslint": "^8.31.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
- "eslint-plugin-n": "^15.2.3",
- "eslint-plugin-promise": "^6.0.0",
+ "eslint-plugin-n": "^15.6.0",
+ "eslint-plugin-promise": "^6.1.1",
"markdownlint-cli": "^0.31.1",
- "rtlcss": "^3.5.0",
- "sass": "^1.52.3",
- "stylelint": "^14.9.0",
- "stylelint-config-recommended-scss": "^6.0.0",
+ "rtlcss": "^4.0.0",
+ "sass": "^1.57.0",
+ "stylelint": "^14.16.1",
+ "stylelint-config-recommended-scss": "^8.0.0",
"stylelint-order": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
}
},
"node_modules/@babel/code-frame": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
- "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"dev": true,
"dependencies": {
- "@babel/highlight": "^7.16.7"
+ "@babel/highlight": "^7.18.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
- "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==",
+ "version": "7.19.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
+ "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
- "version": "7.17.12",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.12.tgz",
- "integrity": "sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg==",
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
+ "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"dev": true,
"dependencies": {
- "@babel/helper-validator-identifier": "^7.16.7",
+ "@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
},
@@ -127,9 +130,9 @@
}
},
"node_modules/@csstools/selector-specificity": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz",
- "integrity": "sha512-aG20vknL4/YjQF9BSV7ts4EWm/yrjagAN7OWBNmlbEOUiu0llj4OGrFoOKK3g2vey4/p2omKCoHrWtPxSwV3HA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz",
+ "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==",
"dev": true,
"engines": {
"node": "^12 || ^14 || >=16"
@@ -139,20 +142,20 @@
"url": "https://opencollective.com/csstools"
},
"peerDependencies": {
- "postcss": "^8.3",
+ "postcss": "^8.2",
"postcss-selector-parser": "^6.0.10"
}
},
"node_modules/@eslint/eslintrc": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
- "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
+ "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
- "espree": "^9.3.2",
- "globals": "^13.15.0",
+ "espree": "^9.4.0",
+ "globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@@ -161,22 +164,38 @@
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
"node_modules/@humanwhocodes/config-array": {
- "version": "0.9.5",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
- "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==",
+ "version": "0.11.8",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+ "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
- "minimatch": "^3.0.4"
+ "minimatch": "^3.0.5"
},
"engines": {
"node": ">=10.10.0"
}
},
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
"node_modules/@humanwhocodes/object-schema": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
@@ -243,9 +262,9 @@
"dev": true
},
"node_modules/acorn": {
- "version": "8.7.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
- "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
+ "version": "8.8.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
+ "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -304,9 +323,9 @@
}
},
"node_modules/anymatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
- "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
@@ -323,15 +342,15 @@
"dev": true
},
"node_modules/array-includes": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz",
- "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==",
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+ "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.4",
- "es-abstract": "^1.19.5",
- "get-intrinsic": "^1.1.1",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
"is-string": "^1.0.7"
},
"engines": {
@@ -351,14 +370,32 @@
}
},
"node_modules/array.prototype.flat": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz",
- "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+ "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz",
+ "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
"es-shim-unscopables": "^1.0.0"
},
"engines": {
@@ -386,6 +423,18 @@
"node": ">=8"
}
},
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -432,6 +481,21 @@
"semver": "^7.0.0"
}
},
+ "node_modules/builtins/node_modules/semver": {
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+ "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -535,18 +599,6 @@
"node": ">= 6"
}
},
- "node_modules/clone-regexp": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz",
- "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==",
- "dev": true,
- "dependencies": {
- "is-regexp": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -566,9 +618,9 @@
"dev": true
},
"node_modules/colord": {
- "version": "2.9.2",
- "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
- "integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==",
+ "version": "2.9.3",
+ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
+ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"dev": true
},
"node_modules/commander": {
@@ -587,9 +639,9 @@
"dev": true
},
"node_modules/cosmiconfig": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
- "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dev": true,
"dependencies": {
"@types/parse-json": "^4.0.0",
@@ -664,9 +716,9 @@
}
},
"node_modules/decamelize-keys": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz",
- "integrity": "sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz",
+ "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==",
"dev": true,
"dependencies": {
"decamelize": "^1.1.0",
@@ -674,6 +726,9 @@
},
"engines": {
"node": ">=0.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/decamelize-keys/node_modules/map-obj": {
@@ -765,34 +820,44 @@
}
},
"node_modules/es-abstract": {
- "version": "1.20.1",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz",
- "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==",
+ "version": "1.21.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz",
+ "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==",
"dev": true,
"dependencies": {
+ "available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.2",
+ "es-set-tostringtag": "^2.0.1",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"function.prototype.name": "^1.1.5",
- "get-intrinsic": "^1.1.1",
+ "get-intrinsic": "^1.1.3",
"get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
"has": "^1.0.3",
"has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
- "internal-slot": "^1.0.3",
- "is-callable": "^1.2.4",
+ "internal-slot": "^1.0.4",
+ "is-array-buffer": "^3.0.1",
+ "is-callable": "^1.2.7",
"is-negative-zero": "^2.0.2",
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.2",
"is-string": "^1.0.7",
+ "is-typed-array": "^1.1.10",
"is-weakref": "^1.0.2",
- "object-inspect": "^1.12.0",
+ "object-inspect": "^1.12.2",
"object-keys": "^1.1.1",
- "object.assign": "^4.1.2",
+ "object.assign": "^4.1.4",
"regexp.prototype.flags": "^1.4.3",
- "string.prototype.trimend": "^1.0.5",
- "string.prototype.trimstart": "^1.0.5",
- "unbox-primitive": "^1.0.2"
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trimend": "^1.0.6",
+ "string.prototype.trimstart": "^1.0.6",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.9"
},
"engines": {
"node": ">= 0.4"
@@ -801,6 +866,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
+ "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/es-shim-unscopables": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
@@ -827,6 +906,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -840,13 +928,15 @@
}
},
"node_modules/eslint": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz",
- "integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==",
+ "version": "8.31.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz",
+ "integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==",
"dev": true,
"dependencies": {
- "@eslint/eslintrc": "^1.3.0",
- "@humanwhocodes/config-array": "^0.9.2",
+ "@eslint/eslintrc": "^1.4.1",
+ "@humanwhocodes/config-array": "^0.11.8",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@@ -856,18 +946,21 @@
"eslint-scope": "^7.1.1",
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
- "espree": "^9.3.2",
+ "espree": "^9.4.0",
"esquery": "^1.4.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
- "functional-red-black-tree": "^1.0.1",
- "glob-parent": "^6.0.1",
- "globals": "^13.15.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "grapheme-splitter": "^1.0.4",
"ignore": "^5.2.0",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-sdsl": "^4.1.4",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
@@ -878,8 +971,7 @@
"regexpp": "^3.2.0",
"strip-ansi": "^6.0.1",
"strip-json-comments": "^3.1.0",
- "text-table": "^0.2.0",
- "v8-compile-cache": "^2.0.3"
+ "text-table": "^0.2.0"
},
"bin": {
"eslint": "bin/eslint.js"
@@ -918,13 +1010,14 @@
}
},
"node_modules/eslint-import-resolver-node": {
- "version": "0.3.6",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
- "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==",
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
+ "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==",
"dev": true,
"dependencies": {
"debug": "^3.2.7",
- "resolve": "^1.20.0"
+ "is-core-module": "^2.11.0",
+ "resolve": "^1.22.1"
}
},
"node_modules/eslint-import-resolver-node/node_modules/debug": {
@@ -937,16 +1030,20 @@
}
},
"node_modules/eslint-module-utils": {
- "version": "2.7.3",
- "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz",
- "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==",
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz",
+ "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==",
"dev": true,
"dependencies": {
- "debug": "^3.2.7",
- "find-up": "^2.1.0"
+ "debug": "^3.2.7"
},
"engines": {
"node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
}
},
"node_modules/eslint-module-utils/node_modules/debug": {
@@ -958,73 +1055,6 @@
"ms": "^2.1.1"
}
},
- "node_modules/eslint-module-utils/node_modules/find-up": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
- "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==",
- "dev": true,
- "dependencies": {
- "locate-path": "^2.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/eslint-module-utils/node_modules/locate-path": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
- "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==",
- "dev": true,
- "dependencies": {
- "p-locate": "^2.0.0",
- "path-exists": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/eslint-module-utils/node_modules/p-limit": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
- "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
- "dev": true,
- "dependencies": {
- "p-try": "^1.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/eslint-module-utils/node_modules/p-locate": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
- "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==",
- "dev": true,
- "dependencies": {
- "p-limit": "^1.1.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/eslint-module-utils/node_modules/p-try": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
- "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/eslint-module-utils/node_modules/path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/eslint-plugin-es": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz",
@@ -1069,23 +1099,25 @@
}
},
"node_modules/eslint-plugin-import": {
- "version": "2.26.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
- "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
+ "version": "2.27.4",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.4.tgz",
+ "integrity": "sha512-Z1jVt1EGKia1X9CnBCkpAOhWy8FgQ7OmJ/IblEkT82yrFU/xJaxwujaTzLWqigewwynRQ9mmHfX9MtAfhxm0sA==",
"dev": true,
"dependencies": {
- "array-includes": "^3.1.4",
- "array.prototype.flat": "^1.2.5",
- "debug": "^2.6.9",
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "array.prototype.flatmap": "^1.3.0",
+ "debug": "^3.2.7",
"doctrine": "^2.1.0",
- "eslint-import-resolver-node": "^0.3.6",
- "eslint-module-utils": "^2.7.3",
+ "eslint-import-resolver-node": "^0.3.7",
+ "eslint-module-utils": "^2.7.4",
"has": "^1.0.3",
- "is-core-module": "^2.8.1",
+ "is-core-module": "^2.11.0",
"is-glob": "^4.0.3",
"minimatch": "^3.1.2",
- "object.values": "^1.1.5",
- "resolve": "^1.22.0",
+ "object.values": "^1.1.6",
+ "resolve": "^1.22.1",
+ "semver": "^6.3.0",
"tsconfig-paths": "^3.14.1"
},
"engines": {
@@ -1096,12 +1128,12 @@
}
},
"node_modules/eslint-plugin-import/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"dependencies": {
- "ms": "2.0.0"
+ "ms": "^2.1.1"
}
},
"node_modules/eslint-plugin-import/node_modules/doctrine": {
@@ -1116,26 +1148,20 @@
"node": ">=0.10.0"
}
},
- "node_modules/eslint-plugin-import/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "dev": true
- },
"node_modules/eslint-plugin-n": {
- "version": "15.2.3",
- "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.2.3.tgz",
- "integrity": "sha512-H+KC7U5R+3IWTeRnACm/4wlqLvS1Q7M6t7BGhn89qXDkZan8HTAEv3ouIONA0ifDwc2YzPFmyPzHuNLddNK4jw==",
+ "version": "15.6.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.6.1.tgz",
+ "integrity": "sha512-R9xw9OtCRxxaxaszTQmQAlPgM+RdGjaL1akWuY/Fv9fRAi8Wj4CUKc6iYVG8QNRjRuo8/BqVYIpfqberJUEacA==",
"dev": true,
"dependencies": {
"builtins": "^5.0.1",
"eslint-plugin-es": "^4.1.0",
"eslint-utils": "^3.0.0",
"ignore": "^5.1.1",
- "is-core-module": "^2.9.0",
+ "is-core-module": "^2.11.0",
"minimatch": "^3.1.2",
- "resolve": "^1.10.1",
- "semver": "^7.3.7"
+ "resolve": "^1.22.1",
+ "semver": "^7.3.8"
},
"engines": {
"node": ">=12.22.0"
@@ -1147,10 +1173,25 @@
"eslint": ">=7.0.0"
}
},
+ "node_modules/eslint-plugin-n/node_modules/semver": {
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+ "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/eslint-plugin-promise": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz",
- "integrity": "sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
+ "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1209,17 +1250,20 @@
}
},
"node_modules/espree": {
- "version": "9.3.2",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz",
- "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==",
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
+ "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
"dev": true,
"dependencies": {
- "acorn": "^8.7.1",
+ "acorn": "^8.8.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
"node_modules/esquery": {
@@ -1264,18 +1308,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/execall": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz",
- "integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==",
- "dev": true,
- "dependencies": {
- "clone-regexp": "^2.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -1283,9 +1315,9 @@
"dev": true
},
"node_modules/fast-glob": {
- "version": "3.2.11",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
- "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
+ "version": "3.2.12",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
+ "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
@@ -1323,15 +1355,18 @@
"dev": true
},
"node_modules/fastest-levenshtein": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
- "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==",
- "dev": true
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.9.1"
+ }
},
"node_modules/fastq": {
- "version": "1.13.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
- "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
@@ -1391,11 +1426,20 @@
}
},
"node_modules/flatted": {
- "version": "3.2.5",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
- "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -1440,12 +1484,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/functional-red-black-tree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
- "dev": true
- },
"node_modules/functions-have-names": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
@@ -1456,9 +1494,9 @@
}
},
"node_modules/get-intrinsic": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz",
- "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
+ "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
@@ -1574,9 +1612,9 @@
}
},
"node_modules/globals": {
- "version": "13.15.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz",
- "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==",
+ "version": "13.19.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
+ "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
"dev": true,
"dependencies": {
"type-fest": "^0.20.2"
@@ -1588,6 +1626,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+ "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
@@ -1614,6 +1667,24 @@
"integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==",
"dev": true
},
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/grapheme-splitter": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+ "dev": true
+ },
"node_modules/hard-rejection": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
@@ -1665,6 +1736,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
@@ -1717,18 +1800,18 @@
}
},
"node_modules/ignore": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
- "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/immutable": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
- "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.2.tgz",
+ "integrity": "sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==",
"dev": true
},
"node_modules/import-fresh": {
@@ -1791,21 +1874,21 @@
"dev": true
},
"node_modules/ini": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
- "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz",
+ "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==",
"dev": true,
"engines": {
- "node": ">=10"
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/internal-slot": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
- "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz",
+ "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==",
"dev": true,
"dependencies": {
- "get-intrinsic": "^1.1.0",
+ "get-intrinsic": "^1.1.3",
"has": "^1.0.3",
"side-channel": "^1.0.4"
},
@@ -1813,6 +1896,20 @@
"node": ">= 0.4"
}
},
+ "node_modules/is-array-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz",
+ "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-typed-array": "^1.1.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -1860,9 +1957,9 @@
}
},
"node_modules/is-callable": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
- "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"engines": {
"node": ">= 0.4"
@@ -1872,9 +1969,9 @@
}
},
"node_modules/is-core-module": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz",
- "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==",
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+ "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
"dev": true,
"dependencies": {
"has": "^1.0.3"
@@ -1964,6 +2061,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
@@ -1998,15 +2104,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-regexp": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz",
- "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==",
- "dev": true,
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/is-shared-array-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
@@ -2049,6 +2146,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-typed-array": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
+ "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-weakref": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -2067,6 +2183,16 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
+ "node_modules/js-sdsl": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz",
+ "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==",
+ "dev": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/js-sdsl"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -2104,9 +2230,9 @@
"dev": true
},
"node_modules/json5": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
- "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@@ -2131,9 +2257,9 @@
}
},
"node_modules/known-css-properties": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.25.0.tgz",
- "integrity": "sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==",
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.26.0.tgz",
+ "integrity": "sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg==",
"dev": true
},
"node_modules/levn": {
@@ -2389,10 +2515,13 @@
}
},
"node_modules/minimist": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
- "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
- "dev": true
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
+ "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
"node_modules/minimist-options": {
"version": "4.1.0",
@@ -2447,6 +2576,21 @@
"node": ">=10"
}
},
+ "node_modules/normalize-package-data/node_modules/semver": {
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+ "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -2475,14 +2619,14 @@
}
},
"node_modules/object.assign": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
- "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+ "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.0",
- "define-properties": "^1.1.3",
- "has-symbols": "^1.0.1",
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
"object-keys": "^1.1.1"
},
"engines": {
@@ -2493,14 +2637,14 @@
}
},
"node_modules/object.values": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz",
- "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==",
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
+ "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1"
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
},
"engines": {
"node": ">= 0.4"
@@ -2665,9 +2809,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.14",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
- "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
+ "version": "8.4.21",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
+ "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
"dev": true,
"funding": [
{
@@ -2717,25 +2861,31 @@
}
},
"node_modules/postcss-scss": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.4.tgz",
- "integrity": "sha512-aBBbVyzA8b3hUL0MGrpydxxXKXFZc5Eqva0Q3V9qsBOLEMsjb6w49WfpsoWzpEgcqJGW4t7Rio8WXVU9Gd8vWg==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz",
+ "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==",
"dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss-scss"
+ }
+ ],
"engines": {
"node": ">=12.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
"peerDependencies": {
- "postcss": "^8.3.3"
+ "postcss": "^8.4.19"
}
},
"node_modules/postcss-selector-parser": {
- "version": "6.0.10",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
- "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "version": "6.0.11",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz",
+ "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
@@ -2770,9 +2920,9 @@
}
},
"node_modules/punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.2.0.tgz",
+ "integrity": "sha512-LN6QV1IJ9ZhxWTNdktaPClrNfp8xdSAYS0Zk2ddX7XsXZAxckMHPCBcHRo0cTcEIgYPRiGEkmji3Idkh2yFtYw==",
"dev": true,
"engines": {
"node": ">=6"
@@ -3051,29 +3201,32 @@
}
},
"node_modules/rtlcss": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-3.5.0.tgz",
- "integrity": "sha512-wzgMaMFHQTnyi9YOwsx9LjOxYXJPzS8sYnFaKm6R5ysvTkwzHiB0vxnbHwchHQT65PTdBjDG21/kQBWI7q9O7A==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.0.0.tgz",
+ "integrity": "sha512-j6oypPP+mgFwDXL1JkLCtm6U/DQntMUqlv5SOhpgHhdIE+PmBcjrtAHIpXfbIup47kD5Sgja9JDsDF1NNOsBwQ==",
"dev": true,
"dependencies": {
- "find-up": "^5.0.0",
+ "escalade": "^3.1.1",
"picocolors": "^1.0.0",
- "postcss": "^8.3.11",
+ "postcss": "^8.4.6",
"strip-json-comments": "^3.1.1"
},
"bin": {
"rtlcss": "bin/rtlcss.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
}
},
"node_modules/run-con": {
- "version": "1.2.10",
- "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.10.tgz",
- "integrity": "sha512-n7PZpYmMM26ZO21dd8y3Yw1TRtGABjRtgPSgFS/nhzfvbJMXFtJhJVyEgayMiP+w/23craJjsnfDvx4W4ue/HQ==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.11.tgz",
+ "integrity": "sha512-NEMGsUT+cglWkzEr4IFK21P4Jca45HqiAbIIZIBdX5+UZTB24Mb/21iNGgz9xZa8tL6vbW7CXmq7MFN42+VjNQ==",
"dev": true,
"dependencies": {
"deep-extend": "^0.6.0",
- "ini": "~2.0.0",
- "minimist": "^1.2.5",
+ "ini": "~3.0.0",
+ "minimist": "^1.2.6",
"strip-json-comments": "~3.1.1"
},
"bin": {
@@ -3103,10 +3256,24 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/safe-regex-test": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+ "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/sass": {
- "version": "1.52.3",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.52.3.tgz",
- "integrity": "sha512-LNNPJ9lafx+j1ArtA7GyEJm9eawXN8KlA1+5dF6IZyoONg1Tyo/g+muOsENWJH/2Q1FHbbV4UwliU0cXMa/VIA==",
+ "version": "1.57.1",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz",
+ "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@@ -3121,18 +3288,12 @@
}
},
"node_modules/semver": {
- "version": "7.3.7",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
- "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true,
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
"bin": {
"semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
}
},
"node_modules/shebang-command": {
@@ -3238,9 +3399,9 @@
}
},
"node_modules/spdx-license-ids": {
- "version": "3.0.11",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz",
- "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==",
+ "version": "3.0.12",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz",
+ "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==",
"dev": true
},
"node_modules/string-width": {
@@ -3258,28 +3419,28 @@
}
},
"node_modules/string.prototype.trimend": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
- "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
+ "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.4",
- "es-abstract": "^1.19.5"
+ "es-abstract": "^1.20.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/string.prototype.trimstart": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz",
- "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
+ "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.4",
- "es-abstract": "^1.19.5"
+ "es-abstract": "^1.20.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3337,51 +3498,49 @@
"dev": true
},
"node_modules/stylelint": {
- "version": "14.9.1",
- "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.9.1.tgz",
- "integrity": "sha512-RdAkJdPiLqHawCSnu21nE27MjNXaVd4WcOHA4vK5GtIGjScfhNnaOuWR2wWdfKFAvcWQPOYe311iveiVKSmwsA==",
+ "version": "14.16.1",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.16.1.tgz",
+ "integrity": "sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==",
"dev": true,
"dependencies": {
- "@csstools/selector-specificity": "^2.0.1",
+ "@csstools/selector-specificity": "^2.0.2",
"balanced-match": "^2.0.0",
- "colord": "^2.9.2",
- "cosmiconfig": "^7.0.1",
+ "colord": "^2.9.3",
+ "cosmiconfig": "^7.1.0",
"css-functions-list": "^3.1.0",
"debug": "^4.3.4",
- "execall": "^2.0.0",
- "fast-glob": "^3.2.11",
- "fastest-levenshtein": "^1.0.12",
+ "fast-glob": "^3.2.12",
+ "fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^6.0.1",
- "get-stdin": "^8.0.0",
"global-modules": "^2.0.0",
"globby": "^11.1.0",
"globjoin": "^0.1.4",
"html-tags": "^3.2.0",
- "ignore": "^5.2.0",
+ "ignore": "^5.2.1",
"import-lazy": "^4.0.0",
"imurmurhash": "^0.1.4",
"is-plain-object": "^5.0.0",
- "known-css-properties": "^0.25.0",
+ "known-css-properties": "^0.26.0",
"mathml-tag-names": "^2.1.3",
"meow": "^9.0.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"picocolors": "^1.0.0",
- "postcss": "^8.4.14",
+ "postcss": "^8.4.19",
"postcss-media-query-parser": "^0.2.3",
"postcss-resolve-nested-selector": "^0.1.1",
"postcss-safe-parser": "^6.0.0",
- "postcss-selector-parser": "^6.0.10",
+ "postcss-selector-parser": "^6.0.11",
"postcss-value-parser": "^4.2.0",
"resolve-from": "^5.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"style-search": "^0.1.0",
- "supports-hyperlinks": "^2.2.0",
+ "supports-hyperlinks": "^2.3.0",
"svg-tags": "^1.0.0",
- "table": "^6.8.0",
+ "table": "^6.8.1",
"v8-compile-cache": "^2.3.0",
- "write-file-atomic": "^4.0.1"
+ "write-file-atomic": "^4.0.2"
},
"bin": {
"stylelint": "bin/stylelint.js"
@@ -3395,26 +3554,32 @@
}
},
"node_modules/stylelint-config-recommended": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz",
- "integrity": "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==",
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-9.0.0.tgz",
+ "integrity": "sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ==",
"dev": true,
"peerDependencies": {
- "stylelint": "^14.4.0"
+ "stylelint": "^14.10.0"
}
},
"node_modules/stylelint-config-recommended-scss": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-6.0.0.tgz",
- "integrity": "sha512-6QOe2/OzXV2AP5FE12A7+qtKdZik7Saf42SMMl84ksVBBPpTdrV+9HaCbPYiRMiwELY9hXCVdH4wlJ+YJb5eig==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-8.0.0.tgz",
+ "integrity": "sha512-BxjxEzRaZoQb7Iinc3p92GS6zRdRAkIuEu2ZFLTxJK2e1AIcCb5B5MXY9KOXdGTnYFZ+KKx6R4Fv9zU6CtMYPQ==",
"dev": true,
"dependencies": {
"postcss-scss": "^4.0.2",
- "stylelint-config-recommended": "^7.0.0",
+ "stylelint-config-recommended": "^9.0.0",
"stylelint-scss": "^4.0.0"
},
"peerDependencies": {
- "stylelint": "^14.4.0"
+ "postcss": "^8.3.3",
+ "stylelint": "^14.10.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ }
}
},
"node_modules/stylelint-order": {
@@ -3431,9 +3596,9 @@
}
},
"node_modules/stylelint-scss": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.2.0.tgz",
- "integrity": "sha512-HHHMVKJJ5RM9pPIbgJ/XA67h9H0407G68Rm69H4fzFbFkyDMcTV1Byep3qdze5+fJ3c0U7mJrbj6S0Fg072uZA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.3.0.tgz",
+ "integrity": "sha512-GvSaKCA3tipzZHoz+nNO7S02ZqOsdBzMiCx9poSmLlb3tdJlGddEX/8QzCOD8O7GQan9bjsvLMsO5xiw6IhhIQ==",
"dev": true,
"dependencies": {
"lodash": "^4.17.21",
@@ -3452,18 +3617,6 @@
"integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
"dev": true
},
- "node_modules/stylelint/node_modules/get-stdin": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
- "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/stylelint/node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -3486,9 +3639,9 @@
}
},
"node_modules/supports-hyperlinks": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz",
- "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
+ "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0",
@@ -3517,9 +3670,9 @@
"dev": true
},
"node_modules/table": {
- "version": "6.8.0",
- "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz",
- "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==",
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
+ "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
"dev": true,
"dependencies": {
"ajv": "^8.0.1",
@@ -3533,9 +3686,9 @@
}
},
"node_modules/table/node_modules/ajv": {
- "version": "8.11.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
- "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+ "version": "8.12.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -3617,6 +3770,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/typed-array-length": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
+ "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
@@ -3700,6 +3867,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/which-typed-array": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
+ "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
@@ -3716,16 +3903,16 @@
"dev": true
},
"node_modules/write-file-atomic": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz",
- "integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
+ "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
"dev": true,
"dependencies": {
"imurmurhash": "^0.1.4",
"signal-exit": "^3.0.7"
},
"engines": {
- "node": "^12.13.0 || ^14.15.0 || >=16"
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/yallist": {
@@ -3767,27 +3954,27 @@
},
"dependencies": {
"@babel/code-frame": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
- "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"dev": true,
"requires": {
- "@babel/highlight": "^7.16.7"
+ "@babel/highlight": "^7.18.6"
}
},
"@babel/helper-validator-identifier": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
- "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==",
+ "version": "7.19.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
+ "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"dev": true
},
"@babel/highlight": {
- "version": "7.17.12",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.12.tgz",
- "integrity": "sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg==",
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
+ "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"dev": true,
"requires": {
- "@babel/helper-validator-identifier": "^7.16.7",
+ "@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
},
@@ -3851,22 +4038,22 @@
}
},
"@csstools/selector-specificity": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz",
- "integrity": "sha512-aG20vknL4/YjQF9BSV7ts4EWm/yrjagAN7OWBNmlbEOUiu0llj4OGrFoOKK3g2vey4/p2omKCoHrWtPxSwV3HA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz",
+ "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==",
"dev": true,
"requires": {}
},
"@eslint/eslintrc": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
- "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
+ "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
- "espree": "^9.3.2",
- "globals": "^13.15.0",
+ "espree": "^9.4.0",
+ "globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@@ -3875,16 +4062,22 @@
}
},
"@humanwhocodes/config-array": {
- "version": "0.9.5",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
- "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==",
+ "version": "0.11.8",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+ "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
"dev": true,
"requires": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
- "minimatch": "^3.0.4"
+ "minimatch": "^3.0.5"
}
},
+ "@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true
+ },
"@humanwhocodes/object-schema": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
@@ -3942,9 +4135,9 @@
"dev": true
},
"acorn": {
- "version": "8.7.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
- "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
+ "version": "8.8.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
+ "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
"dev": true
},
"acorn-jsx": {
@@ -3982,9 +4175,9 @@
}
},
"anymatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
- "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"requires": {
"normalize-path": "^3.0.0",
@@ -3998,15 +4191,15 @@
"dev": true
},
"array-includes": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz",
- "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==",
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+ "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.4",
- "es-abstract": "^1.19.5",
- "get-intrinsic": "^1.1.1",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
"is-string": "^1.0.7"
}
},
@@ -4017,14 +4210,26 @@
"dev": true
},
"array.prototype.flat": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz",
- "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+ "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ }
+ },
+ "array.prototype.flatmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz",
+ "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
"es-shim-unscopables": "^1.0.0"
}
},
@@ -4040,6 +4245,12 @@
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true
},
+ "available-typed-arrays": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "dev": true
+ },
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -4078,6 +4289,17 @@
"dev": true,
"requires": {
"semver": "^7.0.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+ "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
}
},
"call-bind": {
@@ -4150,15 +4372,6 @@
}
}
},
- "clone-regexp": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz",
- "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==",
- "dev": true,
- "requires": {
- "is-regexp": "^2.0.0"
- }
- },
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4175,9 +4388,9 @@
"dev": true
},
"colord": {
- "version": "2.9.2",
- "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
- "integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==",
+ "version": "2.9.3",
+ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
+ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"dev": true
},
"commander": {
@@ -4193,9 +4406,9 @@
"dev": true
},
"cosmiconfig": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
- "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dev": true,
"requires": {
"@types/parse-json": "^4.0.0",
@@ -4244,9 +4457,9 @@
"dev": true
},
"decamelize-keys": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz",
- "integrity": "sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz",
+ "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==",
"dev": true,
"requires": {
"decamelize": "^1.1.0",
@@ -4323,34 +4536,55 @@
}
},
"es-abstract": {
- "version": "1.20.1",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz",
- "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==",
+ "version": "1.21.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz",
+ "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==",
"dev": true,
"requires": {
+ "available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.2",
+ "es-set-tostringtag": "^2.0.1",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"function.prototype.name": "^1.1.5",
- "get-intrinsic": "^1.1.1",
+ "get-intrinsic": "^1.1.3",
"get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
"has": "^1.0.3",
"has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
- "internal-slot": "^1.0.3",
- "is-callable": "^1.2.4",
+ "internal-slot": "^1.0.4",
+ "is-array-buffer": "^3.0.1",
+ "is-callable": "^1.2.7",
"is-negative-zero": "^2.0.2",
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.2",
"is-string": "^1.0.7",
+ "is-typed-array": "^1.1.10",
"is-weakref": "^1.0.2",
- "object-inspect": "^1.12.0",
+ "object-inspect": "^1.12.2",
"object-keys": "^1.1.1",
- "object.assign": "^4.1.2",
+ "object.assign": "^4.1.4",
"regexp.prototype.flags": "^1.4.3",
- "string.prototype.trimend": "^1.0.5",
- "string.prototype.trimstart": "^1.0.5",
- "unbox-primitive": "^1.0.2"
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trimend": "^1.0.6",
+ "string.prototype.trimstart": "^1.0.6",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.9"
+ }
+ },
+ "es-set-tostringtag": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
+ "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "has-tostringtag": "^1.0.0"
}
},
"es-shim-unscopables": {
@@ -4373,6 +4607,12 @@
"is-symbol": "^1.0.2"
}
},
+ "escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true
+ },
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -4380,13 +4620,15 @@
"dev": true
},
"eslint": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz",
- "integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==",
+ "version": "8.31.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz",
+ "integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==",
"dev": true,
"requires": {
- "@eslint/eslintrc": "^1.3.0",
- "@humanwhocodes/config-array": "^0.9.2",
+ "@eslint/eslintrc": "^1.4.1",
+ "@humanwhocodes/config-array": "^0.11.8",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@@ -4396,18 +4638,21 @@
"eslint-scope": "^7.1.1",
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
- "espree": "^9.3.2",
+ "espree": "^9.4.0",
"esquery": "^1.4.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
- "functional-red-black-tree": "^1.0.1",
- "glob-parent": "^6.0.1",
- "globals": "^13.15.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "grapheme-splitter": "^1.0.4",
"ignore": "^5.2.0",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-sdsl": "^4.1.4",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
@@ -4418,8 +4663,7 @@
"regexpp": "^3.2.0",
"strip-ansi": "^6.0.1",
"strip-json-comments": "^3.1.0",
- "text-table": "^0.2.0",
- "v8-compile-cache": "^2.0.3"
+ "text-table": "^0.2.0"
}
},
"eslint-config-standard": {
@@ -4430,13 +4674,14 @@
"requires": {}
},
"eslint-import-resolver-node": {
- "version": "0.3.6",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
- "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==",
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
+ "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==",
"dev": true,
"requires": {
"debug": "^3.2.7",
- "resolve": "^1.20.0"
+ "is-core-module": "^2.11.0",
+ "resolve": "^1.22.1"
},
"dependencies": {
"debug": {
@@ -4451,13 +4696,12 @@
}
},
"eslint-module-utils": {
- "version": "2.7.3",
- "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz",
- "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==",
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz",
+ "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==",
"dev": true,
"requires": {
- "debug": "^3.2.7",
- "find-up": "^2.1.0"
+ "debug": "^3.2.7"
},
"dependencies": {
"debug": {
@@ -4468,55 +4712,6 @@
"requires": {
"ms": "^2.1.1"
}
- },
- "find-up": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
- "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==",
- "dev": true,
- "requires": {
- "locate-path": "^2.0.0"
- }
- },
- "locate-path": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
- "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==",
- "dev": true,
- "requires": {
- "p-locate": "^2.0.0",
- "path-exists": "^3.0.0"
- }
- },
- "p-limit": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
- "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
- "dev": true,
- "requires": {
- "p-try": "^1.0.0"
- }
- },
- "p-locate": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
- "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==",
- "dev": true,
- "requires": {
- "p-limit": "^1.1.0"
- }
- },
- "p-try": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
- "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==",
- "dev": true
- },
- "path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
- "dev": true
}
}
},
@@ -4548,33 +4743,35 @@
}
},
"eslint-plugin-import": {
- "version": "2.26.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
- "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
+ "version": "2.27.4",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.4.tgz",
+ "integrity": "sha512-Z1jVt1EGKia1X9CnBCkpAOhWy8FgQ7OmJ/IblEkT82yrFU/xJaxwujaTzLWqigewwynRQ9mmHfX9MtAfhxm0sA==",
"dev": true,
"requires": {
- "array-includes": "^3.1.4",
- "array.prototype.flat": "^1.2.5",
- "debug": "^2.6.9",
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "array.prototype.flatmap": "^1.3.0",
+ "debug": "^3.2.7",
"doctrine": "^2.1.0",
- "eslint-import-resolver-node": "^0.3.6",
- "eslint-module-utils": "^2.7.3",
+ "eslint-import-resolver-node": "^0.3.7",
+ "eslint-module-utils": "^2.7.4",
"has": "^1.0.3",
- "is-core-module": "^2.8.1",
+ "is-core-module": "^2.11.0",
"is-glob": "^4.0.3",
"minimatch": "^3.1.2",
- "object.values": "^1.1.5",
- "resolve": "^1.22.0",
+ "object.values": "^1.1.6",
+ "resolve": "^1.22.1",
+ "semver": "^6.3.0",
"tsconfig-paths": "^3.14.1"
},
"dependencies": {
"debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"requires": {
- "ms": "2.0.0"
+ "ms": "^2.1.1"
}
},
"doctrine": {
@@ -4585,35 +4782,40 @@
"requires": {
"esutils": "^2.0.2"
}
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "dev": true
}
}
},
"eslint-plugin-n": {
- "version": "15.2.3",
- "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.2.3.tgz",
- "integrity": "sha512-H+KC7U5R+3IWTeRnACm/4wlqLvS1Q7M6t7BGhn89qXDkZan8HTAEv3ouIONA0ifDwc2YzPFmyPzHuNLddNK4jw==",
+ "version": "15.6.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.6.1.tgz",
+ "integrity": "sha512-R9xw9OtCRxxaxaszTQmQAlPgM+RdGjaL1akWuY/Fv9fRAi8Wj4CUKc6iYVG8QNRjRuo8/BqVYIpfqberJUEacA==",
"dev": true,
"requires": {
"builtins": "^5.0.1",
"eslint-plugin-es": "^4.1.0",
"eslint-utils": "^3.0.0",
"ignore": "^5.1.1",
- "is-core-module": "^2.9.0",
+ "is-core-module": "^2.11.0",
"minimatch": "^3.1.2",
- "resolve": "^1.10.1",
- "semver": "^7.3.7"
+ "resolve": "^1.22.1",
+ "semver": "^7.3.8"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+ "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
}
},
"eslint-plugin-promise": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz",
- "integrity": "sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
+ "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==",
"dev": true,
"requires": {}
},
@@ -4651,12 +4853,12 @@
"dev": true
},
"espree": {
- "version": "9.3.2",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz",
- "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==",
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
+ "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
"dev": true,
"requires": {
- "acorn": "^8.7.1",
+ "acorn": "^8.8.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.3.0"
}
@@ -4691,15 +4893,6 @@
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
- "execall": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz",
- "integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==",
- "dev": true,
- "requires": {
- "clone-regexp": "^2.1.0"
- }
- },
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4707,9 +4900,9 @@
"dev": true
},
"fast-glob": {
- "version": "3.2.11",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
- "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
+ "version": "3.2.12",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
+ "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
"dev": true,
"requires": {
"@nodelib/fs.stat": "^2.0.2",
@@ -4743,15 +4936,15 @@
"dev": true
},
"fastest-levenshtein": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
- "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==",
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
"dev": true
},
"fastq": {
- "version": "1.13.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
- "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
"dev": true,
"requires": {
"reusify": "^1.0.4"
@@ -4796,11 +4989,20 @@
}
},
"flatted": {
- "version": "3.2.5",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
- "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
+ "for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.3"
+ }
+ },
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4832,12 +5034,6 @@
"functions-have-names": "^1.2.2"
}
},
- "functional-red-black-tree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
- "dev": true
- },
"functions-have-names": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
@@ -4845,9 +5041,9 @@
"dev": true
},
"get-intrinsic": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz",
- "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
+ "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
"dev": true,
"requires": {
"function-bind": "^1.1.1",
@@ -4932,14 +5128,23 @@
}
},
"globals": {
- "version": "13.15.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz",
- "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==",
+ "version": "13.19.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
+ "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
"dev": true,
"requires": {
"type-fest": "^0.20.2"
}
},
+ "globalthis": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+ "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3"
+ }
+ },
"globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
@@ -4960,6 +5165,21 @@
"integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==",
"dev": true
},
+ "gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.3"
+ }
+ },
+ "grapheme-splitter": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+ "dev": true
+ },
"hard-rejection": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
@@ -4996,6 +5216,12 @@
"get-intrinsic": "^1.1.1"
}
},
+ "has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "dev": true
+ },
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
@@ -5027,15 +5253,15 @@
"dev": true
},
"ignore": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
- "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
"dev": true
},
"immutable": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
- "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.2.tgz",
+ "integrity": "sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==",
"dev": true
},
"import-fresh": {
@@ -5083,22 +5309,33 @@
"dev": true
},
"ini": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
- "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz",
+ "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==",
"dev": true
},
"internal-slot": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
- "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz",
+ "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==",
"dev": true,
"requires": {
- "get-intrinsic": "^1.1.0",
+ "get-intrinsic": "^1.1.3",
"has": "^1.0.3",
"side-channel": "^1.0.4"
}
},
+ "is-array-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz",
+ "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-typed-array": "^1.1.10"
+ }
+ },
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -5134,15 +5371,15 @@
}
},
"is-callable": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
- "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true
},
"is-core-module": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz",
- "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==",
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+ "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
"dev": true,
"requires": {
"has": "^1.0.3"
@@ -5199,6 +5436,12 @@
"has-tostringtag": "^1.0.0"
}
},
+ "is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true
+ },
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
@@ -5221,12 +5464,6 @@
"has-tostringtag": "^1.0.0"
}
},
- "is-regexp": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz",
- "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==",
- "dev": true
- },
"is-shared-array-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
@@ -5254,6 +5491,19 @@
"has-symbols": "^1.0.2"
}
},
+ "is-typed-array": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
+ "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+ "dev": true,
+ "requires": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
"is-weakref": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -5269,6 +5519,12 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
+ "js-sdsl": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz",
+ "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==",
+ "dev": true
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -5303,9 +5559,9 @@
"dev": true
},
"json5": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
- "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
@@ -5324,9 +5580,9 @@
"dev": true
},
"known-css-properties": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.25.0.tgz",
- "integrity": "sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==",
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.26.0.tgz",
+ "integrity": "sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg==",
"dev": true
},
"levn": {
@@ -5525,9 +5781,9 @@
}
},
"minimist": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
- "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
+ "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
"dev": true
},
"minimist-options": {
@@ -5569,6 +5825,17 @@
"is-core-module": "^2.5.0",
"semver": "^7.3.4",
"validate-npm-package-license": "^3.0.1"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+ "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
}
},
"normalize-path": {
@@ -5590,26 +5857,26 @@
"dev": true
},
"object.assign": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
- "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+ "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
"dev": true,
"requires": {
- "call-bind": "^1.0.0",
- "define-properties": "^1.1.3",
- "has-symbols": "^1.0.1",
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
"object-keys": "^1.1.1"
}
},
"object.values": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz",
- "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==",
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
+ "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1"
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
}
},
"once": {
@@ -5723,9 +5990,9 @@
"dev": true
},
"postcss": {
- "version": "8.4.14",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
- "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
+ "version": "8.4.21",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
+ "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
"dev": true,
"requires": {
"nanoid": "^3.3.4",
@@ -5753,16 +6020,16 @@
"requires": {}
},
"postcss-scss": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.4.tgz",
- "integrity": "sha512-aBBbVyzA8b3hUL0MGrpydxxXKXFZc5Eqva0Q3V9qsBOLEMsjb6w49WfpsoWzpEgcqJGW4t7Rio8WXVU9Gd8vWg==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz",
+ "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==",
"dev": true,
"requires": {}
},
"postcss-selector-parser": {
- "version": "6.0.10",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
- "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "version": "6.0.11",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz",
+ "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==",
"dev": true,
"requires": {
"cssesc": "^3.0.0",
@@ -5789,9 +6056,9 @@
"dev": true
},
"punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.2.0.tgz",
+ "integrity": "sha512-LN6QV1IJ9ZhxWTNdktaPClrNfp8xdSAYS0Zk2ddX7XsXZAxckMHPCBcHRo0cTcEIgYPRiGEkmji3Idkh2yFtYw==",
"dev": true
},
"queue-microtask": {
@@ -5981,26 +6248,26 @@
}
},
"rtlcss": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-3.5.0.tgz",
- "integrity": "sha512-wzgMaMFHQTnyi9YOwsx9LjOxYXJPzS8sYnFaKm6R5ysvTkwzHiB0vxnbHwchHQT65PTdBjDG21/kQBWI7q9O7A==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.0.0.tgz",
+ "integrity": "sha512-j6oypPP+mgFwDXL1JkLCtm6U/DQntMUqlv5SOhpgHhdIE+PmBcjrtAHIpXfbIup47kD5Sgja9JDsDF1NNOsBwQ==",
"dev": true,
"requires": {
- "find-up": "^5.0.0",
+ "escalade": "^3.1.1",
"picocolors": "^1.0.0",
- "postcss": "^8.3.11",
+ "postcss": "^8.4.6",
"strip-json-comments": "^3.1.1"
}
},
"run-con": {
- "version": "1.2.10",
- "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.10.tgz",
- "integrity": "sha512-n7PZpYmMM26ZO21dd8y3Yw1TRtGABjRtgPSgFS/nhzfvbJMXFtJhJVyEgayMiP+w/23craJjsnfDvx4W4ue/HQ==",
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.11.tgz",
+ "integrity": "sha512-NEMGsUT+cglWkzEr4IFK21P4Jca45HqiAbIIZIBdX5+UZTB24Mb/21iNGgz9xZa8tL6vbW7CXmq7MFN42+VjNQ==",
"dev": true,
"requires": {
"deep-extend": "^0.6.0",
- "ini": "~2.0.0",
- "minimist": "^1.2.5",
+ "ini": "~3.0.0",
+ "minimist": "^1.2.6",
"strip-json-comments": "~3.1.1"
}
},
@@ -6013,10 +6280,21 @@
"queue-microtask": "^1.2.2"
}
},
+ "safe-regex-test": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+ "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ }
+ },
"sass": {
- "version": "1.52.3",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.52.3.tgz",
- "integrity": "sha512-LNNPJ9lafx+j1ArtA7GyEJm9eawXN8KlA1+5dF6IZyoONg1Tyo/g+muOsENWJH/2Q1FHbbV4UwliU0cXMa/VIA==",
+ "version": "1.57.1",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz",
+ "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==",
"dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
@@ -6025,13 +6303,10 @@
}
},
"semver": {
- "version": "7.3.7",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
- "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
- "dev": true,
- "requires": {
- "lru-cache": "^6.0.0"
- }
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
},
"shebang-command": {
"version": "2.0.0",
@@ -6115,9 +6390,9 @@
}
},
"spdx-license-ids": {
- "version": "3.0.11",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz",
- "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==",
+ "version": "3.0.12",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz",
+ "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==",
"dev": true
},
"string-width": {
@@ -6132,25 +6407,25 @@
}
},
"string.prototype.trimend": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
- "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
+ "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.4",
- "es-abstract": "^1.19.5"
+ "es-abstract": "^1.20.4"
}
},
"string.prototype.trimstart": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz",
- "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
+ "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.4",
- "es-abstract": "^1.19.5"
+ "es-abstract": "^1.20.4"
}
},
"strip-ansi": {
@@ -6190,51 +6465,49 @@
"dev": true
},
"stylelint": {
- "version": "14.9.1",
- "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.9.1.tgz",
- "integrity": "sha512-RdAkJdPiLqHawCSnu21nE27MjNXaVd4WcOHA4vK5GtIGjScfhNnaOuWR2wWdfKFAvcWQPOYe311iveiVKSmwsA==",
+ "version": "14.16.1",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.16.1.tgz",
+ "integrity": "sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==",
"dev": true,
"requires": {
- "@csstools/selector-specificity": "^2.0.1",
+ "@csstools/selector-specificity": "^2.0.2",
"balanced-match": "^2.0.0",
- "colord": "^2.9.2",
- "cosmiconfig": "^7.0.1",
+ "colord": "^2.9.3",
+ "cosmiconfig": "^7.1.0",
"css-functions-list": "^3.1.0",
"debug": "^4.3.4",
- "execall": "^2.0.0",
- "fast-glob": "^3.2.11",
- "fastest-levenshtein": "^1.0.12",
+ "fast-glob": "^3.2.12",
+ "fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^6.0.1",
- "get-stdin": "^8.0.0",
"global-modules": "^2.0.0",
"globby": "^11.1.0",
"globjoin": "^0.1.4",
"html-tags": "^3.2.0",
- "ignore": "^5.2.0",
+ "ignore": "^5.2.1",
"import-lazy": "^4.0.0",
"imurmurhash": "^0.1.4",
"is-plain-object": "^5.0.0",
- "known-css-properties": "^0.25.0",
+ "known-css-properties": "^0.26.0",
"mathml-tag-names": "^2.1.3",
"meow": "^9.0.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"picocolors": "^1.0.0",
- "postcss": "^8.4.14",
+ "postcss": "^8.4.19",
"postcss-media-query-parser": "^0.2.3",
"postcss-resolve-nested-selector": "^0.1.1",
"postcss-safe-parser": "^6.0.0",
- "postcss-selector-parser": "^6.0.10",
+ "postcss-selector-parser": "^6.0.11",
"postcss-value-parser": "^4.2.0",
"resolve-from": "^5.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"style-search": "^0.1.0",
- "supports-hyperlinks": "^2.2.0",
+ "supports-hyperlinks": "^2.3.0",
"svg-tags": "^1.0.0",
- "table": "^6.8.0",
+ "table": "^6.8.1",
"v8-compile-cache": "^2.3.0",
- "write-file-atomic": "^4.0.1"
+ "write-file-atomic": "^4.0.2"
},
"dependencies": {
"balanced-match": {
@@ -6243,12 +6516,6 @@
"integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
"dev": true
},
- "get-stdin": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
- "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
- "dev": true
- },
"resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -6258,20 +6525,20 @@
}
},
"stylelint-config-recommended": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz",
- "integrity": "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==",
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-9.0.0.tgz",
+ "integrity": "sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ==",
"dev": true,
"requires": {}
},
"stylelint-config-recommended-scss": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-6.0.0.tgz",
- "integrity": "sha512-6QOe2/OzXV2AP5FE12A7+qtKdZik7Saf42SMMl84ksVBBPpTdrV+9HaCbPYiRMiwELY9hXCVdH4wlJ+YJb5eig==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-8.0.0.tgz",
+ "integrity": "sha512-BxjxEzRaZoQb7Iinc3p92GS6zRdRAkIuEu2ZFLTxJK2e1AIcCb5B5MXY9KOXdGTnYFZ+KKx6R4Fv9zU6CtMYPQ==",
"dev": true,
"requires": {
"postcss-scss": "^4.0.2",
- "stylelint-config-recommended": "^7.0.0",
+ "stylelint-config-recommended": "^9.0.0",
"stylelint-scss": "^4.0.0"
}
},
@@ -6286,9 +6553,9 @@
}
},
"stylelint-scss": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.2.0.tgz",
- "integrity": "sha512-HHHMVKJJ5RM9pPIbgJ/XA67h9H0407G68Rm69H4fzFbFkyDMcTV1Byep3qdze5+fJ3c0U7mJrbj6S0Fg072uZA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.3.0.tgz",
+ "integrity": "sha512-GvSaKCA3tipzZHoz+nNO7S02ZqOsdBzMiCx9poSmLlb3tdJlGddEX/8QzCOD8O7GQan9bjsvLMsO5xiw6IhhIQ==",
"dev": true,
"requires": {
"lodash": "^4.17.21",
@@ -6308,9 +6575,9 @@
}
},
"supports-hyperlinks": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz",
- "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
+ "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
"dev": true,
"requires": {
"has-flag": "^4.0.0",
@@ -6330,9 +6597,9 @@
"dev": true
},
"table": {
- "version": "6.8.0",
- "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz",
- "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==",
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
+ "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
"dev": true,
"requires": {
"ajv": "^8.0.1",
@@ -6343,9 +6610,9 @@
},
"dependencies": {
"ajv": {
- "version": "8.11.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
- "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+ "version": "8.12.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
@@ -6410,6 +6677,17 @@
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true
},
+ "typed-array-length": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
+ "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ }
+ },
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
@@ -6481,6 +6759,20 @@
"is-symbol": "^1.0.3"
}
},
+ "which-typed-array": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
+ "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+ "dev": true,
+ "requires": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0",
+ "is-typed-array": "^1.1.10"
+ }
+ },
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
@@ -6494,9 +6786,9 @@
"dev": true
},
"write-file-atomic": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz",
- "integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
+ "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
"dev": true,
"requires": {
"imurmurhash": "^0.1.4",
diff --git a/package.json b/package.json
index fe25a3f43..bb8173e0d 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,9 @@
"url": "https://github.com/FreshRSS/FreshRSS.git"
},
"license": "AGPL-3.0",
+ "engines": {
+ "node": ">=12"
+ },
"scripts": {
"eslint": "eslint --ext .js .",
"eslint_fix": "eslint --fix --ext .js .",
@@ -30,16 +33,16 @@
"fix": "npm run rtlcss && npm run stylelint_fix && npm run eslint_fix && npm run markdownlint_fix"
},
"devDependencies": {
- "eslint": "^8.10.0",
+ "eslint": "^8.31.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
- "eslint-plugin-n": "^15.2.3",
- "eslint-plugin-promise": "^6.0.0",
+ "eslint-plugin-n": "^15.6.0",
+ "eslint-plugin-promise": "^6.1.1",
"markdownlint-cli": "^0.31.1",
- "rtlcss": "^3.5.0",
- "sass": "^1.52.3",
- "stylelint": "^14.9.0",
- "stylelint-config-recommended-scss": "^6.0.0",
+ "rtlcss": "^4.0.0",
+ "sass": "^1.57.0",
+ "stylelint": "^14.16.1",
+ "stylelint-config-recommended-scss": "^8.0.0",
"stylelint-order": "^5.0.0"
},
"rtlcssConfig": {}
diff --git a/phpcs.xml b/phpcs.xml
index e29886ccb..838302e52 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -5,6 +5,7 @@
<arg name="tab-width" value="4"/>
<exclude-pattern>./.git/</exclude-pattern>
<exclude-pattern>./lib/SimplePie/</exclude-pattern>
+ <exclude-pattern>./lib/marienfressinaud/</exclude-pattern>
<exclude-pattern>./lib/phpgt/</exclude-pattern>
<exclude-pattern>./lib/phpmailer/</exclude-pattern>
<exclude-pattern>./lib/http-conditional.php</exclude-pattern>
diff --git a/phpstan.neon b/phpstan.neon
index 91509245f..846731c70 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -7,18 +7,16 @@ parameters:
paths:
- .
excludePaths:
- - .git/*
- - lib/phpmailer/*
- - lib/SimplePie/*
- - node_modules/*
- # TODO: include tests
- - tests/*
- - vendor/*
- scanDirectories:
- - lib/phpmailer/
- - lib/SimplePie/
+ analyse:
+ - lib/marienfressinaud/*
+ - lib/phpmailer/*
+ - lib/SimplePie/*
+ analyseAndScan:
+ - .git/*
+ - node_modules/*
+ # TODO: include tests
+ - tests/*
+ - vendor/*
bootstrapFiles:
- cli/_cli.php
- lib/favicons.php
- - lib/SimplePie/SimplePie.php
- - app/SQL/install.sql.sqlite.php
diff --git a/tests/app/Models/CategoryTest.php b/tests/app/Models/CategoryTest.php
index e4a921baa..93fbdbc63 100644
--- a/tests/app/Models/CategoryTest.php
+++ b/tests/app/Models/CategoryTest.php
@@ -30,4 +30,54 @@ class CategoryTest extends PHPUnit\Framework\TestCase {
);
}
+ public function test_feedOrdering() {
+ $feed_1 = $this->getMockBuilder(FreshRSS_Feed::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $feed_1->expects($this->any())
+ ->method('name')
+ ->willReturn('AAA');
+
+ $feed_2 = $this->getMockBuilder(FreshRSS_Feed::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $feed_2->expects($this->any())
+ ->method('name')
+ ->willReturn('ZZZ');
+
+ $feed_3 = $this->getMockBuilder(FreshRSS_Feed::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $feed_3->expects($this->any())
+ ->method('name')
+ ->willReturn('lll');
+
+ $category = new FreshRSS_Category('test', [
+ $feed_1,
+ $feed_2,
+ $feed_3,
+ ]);
+ $feeds = $category->feeds();
+
+ $this->assertCount(3, $feeds);
+ $this->assertEquals('AAA', $feeds[0]->name());
+ $this->assertEquals('lll', $feeds[1]->name());
+ $this->assertEquals('ZZZ', $feeds[2]->name());
+
+ $feed_4 = $this->getMockBuilder(FreshRSS_Feed::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $feed_4->expects($this->any())
+ ->method('name')
+ ->willReturn('BBB');
+
+ $category->addFeed($feed_4);
+ $feeds = $category->feeds();
+
+ $this->assertCount(4, $feeds);
+ $this->assertEquals('AAA', $feeds[0]->name());
+ $this->assertEquals('BBB', $feeds[1]->name());
+ $this->assertEquals('lll', $feeds[2]->name());
+ $this->assertEquals('ZZZ', $feeds[3]->name());
+ }
}
diff --git a/tests/app/Models/SearchTest.php b/tests/app/Models/SearchTest.php
index a2832c710..52c10244d 100644
--- a/tests/app/Models/SearchTest.php
+++ b/tests/app/Models/SearchTest.php
@@ -318,17 +318,17 @@ class SearchTest extends PHPUnit\Framework\TestCase {
],
[
'#tag Hello OR (author:Alice inurl:example) OR (f:3 intitle:World) OR L:12',
- ' ((e.tags LIKE ? AND (e.title LIKE ? OR e.content LIKE ?) )) OR ((e.author LIKE ? AND e.link LIKE ? )) OR' .
+ " ((TRIM(e.tags) || ' #' LIKE ? AND (e.title LIKE ? OR e.content LIKE ?) )) OR ((e.author LIKE ? AND e.link LIKE ? )) OR" .
' ((e.id_feed IN (?) AND e.title LIKE ? )) OR ((e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)) )) ',
- ['%tag%','%Hello%', '%Hello%', '%Alice%', '%example%', '3', '%World%', '12']
+ ['%tag #%','%Hello%', '%Hello%', '%Alice%', '%example%', '3', '%World%', '12']
],
[
'#tag Hello (author:Alice inurl:example) (f:3 intitle:World) label:Bleu',
- ' ((e.tags LIKE ? AND (e.title LIKE ? OR e.content LIKE ?) )) AND' .
+ " ((TRIM(e.tags) || ' #' LIKE ? AND (e.title LIKE ? OR e.content LIKE ?) )) AND" .
' ((e.author LIKE ? AND e.link LIKE ? )) AND' .
' ((e.id_feed IN (?) AND e.title LIKE ? )) AND' .
' ((e.id IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN (?)) )) ',
- ['%tag%', '%Hello%', '%Hello%', '%Alice%', '%example%', '3', '%World%', 'Bleu']
+ ['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', '3', '%World%', 'Bleu']
],
[
'!((author:Alice intitle:hello) OR (author:Bob intitle:world))',
@@ -339,6 +339,11 @@ class SearchTest extends PHPUnit\Framework\TestCase {
'(author:Alice intitle:hello) !(author:Bob intitle:world)',
' ((e.author LIKE ? AND e.title LIKE ? )) AND NOT ((e.author LIKE ? AND e.title LIKE ? )) ',
['%Alice%', '%hello%', '%Bob%', '%world%'],
+ ],
+ [
+ 'intitle:"\\(test\\)"',
+ '(e.title LIKE ? )',
+ ['%\\(test\\)%'],
]
];
}
diff --git a/tests/app/Models/UserQueryTest.php b/tests/app/Models/UserQueryTest.php
index c876740e6..7b1e88907 100644
--- a/tests/app/Models/UserQueryTest.php
+++ b/tests/app/Models/UserQueryTest.php
@@ -34,7 +34,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
->method('name')
->withAnyParameters()
->willReturn($category_name);
- $cat_dao = $this->createMock('FreshRSS_Searchable');
+ $cat_dao = $this->createMock('FreshRSS_CategoryDAO');
$cat_dao->expects($this->atLeastOnce())
->method('searchById')
->withAnyParameters()
@@ -60,7 +60,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
->method('name')
->withAnyParameters()
->willReturn($feed_name);
- $feed_dao = $this->createMock('FreshRSS_Searchable');
+ $feed_dao = $this->createMock('FreshRSS_FeedDAO');
$feed_dao->expects($this->atLeastOnce())
->method('searchById')
->withAnyParameters()
@@ -74,8 +74,8 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
public function test__construct_whenUnknownQuery_doesStoreParameters() {
$query = array('get' => 'q');
$user_query = new FreshRSS_UserQuery($query);
- $this->assertNull($user_query->getGetName());
- $this->assertNull($user_query->getGetType());
+ $this->assertEmpty($user_query->getGetName());
+ $this->assertEmpty($user_query->getGetType());
}
public function test__construct_whenName_storesName() {
@@ -93,7 +93,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
}
public function test__construct_whenState_storesState() {
- $state = 'some state';
+ $state = FreshRSS_Entry::STATE_ALL;
$query = array('state' => $state);
$user_query = new FreshRSS_UserQuery($query);
$this->assertEquals($state, $user_query->getState());
@@ -118,7 +118,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
'name' => 'some name',
'order' => 'some order',
'search' => 'some search',
- 'state' => 'some state',
+ 'state' => FreshRSS_Entry::STATE_ALL,
'url' => 'some url',
);
$user_query = new FreshRSS_UserQuery($query);
@@ -160,7 +160,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
public function testIsDeprecated_whenCategoryExists_returnFalse() {
$cat = $this->createMock('FreshRSS_Category');
- $cat_dao = $this->createMock('FreshRSS_Searchable');
+ $cat_dao = $this->createMock('FreshRSS_CategoryDAO');
$cat_dao->expects($this->atLeastOnce())
->method('searchById')
->withAnyParameters()
@@ -171,7 +171,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
}
public function testIsDeprecated_whenCategoryDoesNotExist_returnTrue() {
- $cat_dao = $this->createMock('FreshRSS_Searchable');
+ $cat_dao = $this->createMock('FreshRSS_CategoryDAO');
$cat_dao->expects($this->atLeastOnce())
->method('searchById')
->withAnyParameters()
@@ -183,7 +183,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
public function testIsDeprecated_whenFeedExists_returnFalse() {
$feed = $this->createMock('FreshRSS_Feed', array(), array('', false));
- $feed_dao = $this->createMock('FreshRSS_Searchable');
+ $feed_dao = $this->createMock('FreshRSS_FeedDAO');
$feed_dao->expects($this->atLeastOnce())
->method('searchById')
->withAnyParameters()
@@ -194,7 +194,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
}
public function testIsDeprecated_whenFeedDoesNotExist_returnTrue() {
- $feed_dao = $this->createMock('FreshRSS_Searchable');
+ $feed_dao = $this->createMock('FreshRSS_FeedDAO');
$feed_dao->expects($this->atLeastOnce())
->method('searchById')
->withAnyParameters()