Compare commits

...

148 Commits

Author SHA1 Message Date
Andrew Dolgov a86df7eac8 Merge branch 'bugfix/web-nginx-healthcheck' into 'master'
Use APP_BASE in the web-nginx health check URL.

See merge request tt-rss/tt-rss!21
2024-01-19 20:13:00 +00:00
wn_ 03c9d4f390 Use APP_BASE in the web-nginx health check URL. 2024-01-19 16:19:07 +00:00
Andrew Dolgov 283ad4ebea Merge branch 'feature/unused-var-cleanup' into 'master'
Clean up some unused variables.

See merge request tt-rss/tt-rss!19
2024-01-13 18:29:30 +00:00
Andrew Dolgov d334023267 Merge branch 'feature/reduce-fetch-error-message' into 'master'
Only include the exception message in 'UrlHelper::$fetch_last_error'.

See merge request tt-rss/tt-rss!20
2024-01-13 18:27:19 +00:00
wn_ 8ef2803b27 Only include the exception message in 'UrlHelper::$fetch_last_error'.
Before this the stack trace was included, which is a bit much.
2024-01-09 12:38:32 +00:00
Andrew Dolgov de214a01d2
shorten DIGEST_MIN_SCORE help text 2024-01-09 12:38:25 +03:00
Andrew Dolgov bcdfedeb8a
* mark get_pref/set_pref wrappers as deprecated
* add per-user preference for minimal score required for digest
2024-01-09 11:45:40 +03:00
Andrew Dolgov ea6cdcccb0
* mail test - fill user email address as default
* digest - fix some warnings
2024-01-09 11:28:32 +03:00
wn_ 8727fb3ba8 Clean up some unused variables.
This is essentially 1ccc0c8c1a without the renames and some other things related to Psalm.
2024-01-08 22:46:13 +00:00
Andrew Dolgov f0f22c23c5 Merge branch 'feature/urlhelper-fetch-do-assoc-opts' into 'master'
Update all UrlHelper::fetch() calls to use the associative array approach.

See merge request tt-rss/tt-rss!18
2023-12-31 09:08:03 +00:00
wn_ 90e7bf7cc3 Update all UrlHelper::fetch() calls to use the associative array approach.
The other approach (passing in individual params) was marked as deprecated a few years ago.
2023-12-30 15:39:17 +00:00
Andrew Dolgov a882eb13f7 Merge branch 'feature/early-fail-disallowed-redirects' into 'master'
Perform validation of redirect URLs during the redirect process.

See merge request tt-rss/tt-rss!17
2023-12-29 04:38:59 +00:00
wn_ 91a91dac15 Perform validation of redirect URLs during the redirect process.
Previously, validation was only done after all redirects and the final request had completed.  This approach ensures all redirects are to URLs that pass extended validation.
2023-12-29 00:41:52 +00:00
Andrew Dolgov 51cd02fc3e Merge branch 'feature/use-guzzle' into 'master'
Use Guzzle

See merge request tt-rss/tt-rss!16
2023-12-24 16:14:45 +00:00
wn_ 0ea9db3170 Fix specifying auth type in UrlHelper::fetch(), add a test for 403 auth retry. 2023-12-24 11:21:43 +00:00
wn_ 9a1f7c2ebf Appease PHPStan in UrlHelperTest 2023-12-23 19:58:39 +00:00
wn_ 3c171cc92c Add some tests for UrlHelper::fetch() 2023-12-23 19:52:56 +00:00
wn_ e33b0297d5 Ensure the feed name is easily visible when looking at the feeds with errors list. 2023-12-23 17:01:24 +00:00
wn_ 9132360d46 Rework content encoding error retrying in UrlHelper::fetch() 2023-12-23 16:34:39 +00:00
wn_ d82da74363 Clean up UrlHelper::resolve_redirects().
Also: this doesn't appear to be used... but maybe in some plugin?
2023-12-23 15:56:21 +00:00
wn_ ff59fbd460 Add back 'any auth' retry in UrlHelper::fetch() 2023-12-23 15:34:21 +00:00
wn_ e85d47dfd4 Use Guzzle 2023-12-22 16:51:23 +00:00
Andrew Dolgov d4ae6c67db Merge branch 'dont-sanitize-figure-tag' into 'master'
sanitizer: keep <figure> intact

See merge request tt-rss/tt-rss!15
2023-12-18 14:51:58 +00:00
Chih-Hsuan Yen f1a9ac9b15 sanitizer: add a test to make sure <figure> is intact
Somehow with the old approach, `<figure>` is rearranged into `<head>`,
and the latter is stripped by `Sanitizer::strip_harmful_tags()` (see
[1]). The issue is fixed by [2]. Here I added a test for the regression.

[1] https://community.tt-rss.org/t/unexpected-behavior-with-figure-tag/6244
[2] 67012f9dac
2023-12-18 22:46:35 +08:00
Andrew Dolgov 67012f9dac
Revert "Fix sanitizer with libxml2 >= 2.12.0"
This reverts commit d4da4dcc32.
2023-12-17 22:42:52 +03:00
Andrew Dolgov 14ad8b21d5
bump CI jobs & utility scripts to php83 2023-12-10 09:36:09 +03:00
Andrew Dolgov 4b3cf17d8d
Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2023-12-10 09:32:00 +03:00
Andrew Dolgov 1b31e6fd5b Merge branch 'feature/php-8.3' into 'master'
Bump to Alpine 3.19 and PHP 8.3.

See merge request tt-rss/tt-rss!14
2023-12-10 06:18:10 +00:00
wn_ 7883f024e7 Bump to Alpine 3.19 and PHP 8.3.
* https://alpinelinux.org/posts/Alpine-3.19.0-released.html
* https://www.php.net/releases/8.3/en.php
2023-12-07 12:38:23 +00:00
Andrew Dolgov 8f66f579e4
add coverage-filter 2023-12-02 18:04:55 +03:00
Andrew Dolgov 09898ccbc8
add phpunit code coverage driver 2023-12-02 18:03:06 +03:00
Andrew Dolgov 2b8e344532
add some unittest options for xmlrunner 2023-12-02 12:48:54 +03:00
Andrew Dolgov e453befab6
fix filename 2023-12-02 12:47:36 +03:00
Andrew Dolgov dbb6e7291e
enable unit test results for selenium 2023-12-02 12:44:21 +03:00
Andrew Dolgov eac9e7c103
collect phpunit artifacts 2023-12-02 11:42:12 +03:00
Andrew Dolgov 93bd96e356
add env prefixes 2023-12-02 11:38:25 +03:00
Andrew Dolgov 7005d6d5f3
add db vars 2023-12-02 11:36:09 +03:00
Andrew Dolgov 0621d22bbe
add cobertura args for phpunit-integration 2023-12-02 11:30:17 +03:00
Andrew Dolgov cc133d2c0a
disable local rules for integration tests 2023-12-02 11:16:52 +03:00
Andrew Dolgov e52eaf0e7b
add sanitizer integration test 2023-12-02 11:14:07 +03:00
Andrew Dolgov ce9847d317 Merge branch 'fix-sanitizer-new-libxml2' into 'master'
Fix sanitizer with libxml2 >= 2.12.0

See merge request tt-rss/tt-rss!12
2023-12-01 12:26:41 +00:00
Chih-Hsuan Yen d4da4dcc32 Fix sanitizer with libxml2 >= 2.12.0
Somehow with newer libxml2, `<?xml encoding="UTF-8">` no longer enforces
UTF-8. Instead, non-ASCII contents are treated as ISO-8859-1 and get
broken.

For example, `<p>中文</p>` becomes
`<p>&auml;&cedil;&shy;&aelig;&#150;&#135;</p>` (should be
`<p>&#20013;&#25991;</p>`).

Switching to another trick mentioned on [1] fixes the issue, and the
new trick still works with older libxml2 (tested 2.11.5).

As a side note, DOMDocument::loadHTML uses HTMLParser in libxml2 [2][3].

[1] https://stackoverflow.com/questions/8218230/php-domdocument-loadhtml-not-encoding-utf-8-correctly
[2] https://github.com/php/php-src/blob/php-8.1.26/ext/dom/document.c#L1855
[3] https://gnome.pages.gitlab.gnome.org/libxml2/devhelp/libxml2-HTMLparser.html
2023-11-26 21:04:56 +08:00
Andrew Dolgov 2c7e000120
set registry project 2023-11-25 20:21:10 +03:00
Andrew Dolgov 1fe1132a1a
use variable for fastcgi_pass to force resolver usage 2023-11-07 09:56:23 +03:00
Andrew Dolgov 61910acbcd
explicitly set resolver in the nginx container (configurable envvar) 2023-11-07 08:38:11 +03:00
Andrew Dolgov ff4248b09e
add wip UI/backend stuff to filter feed tree 2023-11-03 08:33:35 +03:00
Andrew Dolgov 0b7d021f8e
add wait-for-element to selenium test 2023-11-01 13:40:35 +03:00
Andrew Dolgov d4c972f551
remove .git before_scripts 2023-11-01 13:24:32 +03:00
Andrew Dolgov f48f1b0131
Revert "pass .git to docker context so self-built images would have some way to determine version without CI variables"
This reverts commit 5cfde4cada.
2023-11-01 13:24:04 +03:00
Andrew Dolgov 4ced03b4b6
forgot one job 2023-11-01 13:12:31 +03:00
Andrew Dolgov 9ce347d8d5
do the same for :publish jobs 2023-11-01 13:10:57 +03:00
Andrew Dolgov e777f2e292
fix yaml indents 2023-11-01 13:09:29 +03:00
Andrew Dolgov ee18936bfe
add .git to .dockerignore when building master images 2023-11-01 13:08:50 +03:00
Andrew Dolgov 5cfde4cada
pass .git to docker context so self-built images would have some way to determine version without CI variables 2023-11-01 13:06:40 +03:00
Andrew Dolgov 1be156408a
add some more phpunit api tests 2023-10-29 10:46:01 +03:00
Andrew Dolgov cfcab96e18
pass API_URL to phpunit-integration CLI 2023-10-29 10:01:14 +03:00
Andrew Dolgov 7cd2c5cac8
fix apitest 2023-10-29 09:42:53 +03:00
Andrew Dolgov adf3985afa
fix circular dependency 2023-10-29 09:25:01 +03:00
Andrew Dolgov afaef66783
reduce targets 2023-10-29 09:19:35 +03:00
Andrew Dolgov 8b72d9ab11
add phpunit integration (wip) 2023-10-29 09:14:18 +03:00
Andrew Dolgov 855695a862
add stuff necessary to run integration tests using phpunit 2023-10-28 18:45:09 +03:00
Andrew Dolgov 0ac8710ea1
add always-failing mock of api test 2023-10-28 18:08:42 +03:00
Andrew Dolgov 01c9869e2b
phpunit - skip integration tests 2023-10-28 18:07:54 +03:00
Andrew Dolgov d2424b9e4b
use python unittest for selenium tests 2023-10-28 11:11:13 +03:00
Andrew Dolgov a1a2fe40f6
add a separate interface for auth modules w/ change_password() method 2023-10-27 22:29:03 +03:00
Andrew Dolgov 925256c81f
unify test class naming 2023-10-27 22:10:28 +03:00
Andrew Dolgov 5a7c5b8249
Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2023-10-27 22:07:41 +03:00
Andrew Dolgov 5920ac814c
replace some dirname horrors with a separate unit-tested method 2023-10-27 22:07:28 +03:00
Andrew Dolgov 2af5f73480 Merge branch 'bugfix/psr-4-renames' into 'master'
Fix class names in some more places.

See merge request tt-rss/tt-rss!10
2023-10-26 15:05:07 +00:00
wn_ c7e1caf223 Fix class names in some more places.
Related to the PSR-4 move via 865ecc8796
2023-10-26 15:01:43 +00:00
Andrew Dolgov 8c9c69921f
make phpstan happy 2023-10-25 18:04:42 +03:00
Andrew Dolgov 3181272619
add healthcheck public method, map by default to /healthz 2023-10-25 17:53:49 +03:00
Andrew Dolgov 865ecc8796
move to psr-4 autoloader 2023-10-25 12:55:09 +03:00
Andrew Dolgov 0a5507d3bd
Revert "api: escape newlines in headline content HTML object"
This reverts commit ed43a73369.
2023-10-24 22:58:10 +03:00
Andrew Dolgov 69c1c62992
add a workaround for make_self_url() when invoked off /api/ endpoint, add unit tests for this method 2023-10-24 22:27:27 +03:00
Andrew Dolgov de2830b241
disable xdebug tracing 2023-10-24 21:55:59 +03:00
Andrew Dolgov ed43a73369
api: escape newlines in headline content HTML object 2023-10-24 21:35:48 +03:00
Andrew Dolgov e31636bf97
Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2023-10-24 17:50:30 +03:00
Andrew Dolgov 3d5308a6e5
add stub opentelemetry classes in case it is disabled 2023-10-24 17:50:00 +03:00
Andrew Dolgov 30b36e0034 Update docker-compose.yml 2023-10-24 14:22:09 +00:00
Andrew Dolgov 1e3b7f7a43
Revert "add a self url path hack to strip request path directories (needed for /api/index.php)"
This reverts commit 9826d2f075.
2023-10-23 23:39:28 +03:00
Andrew Dolgov 994f376f42
Revert "make phpstan happy"
This reverts commit deb441e9e3.
2023-10-23 23:39:21 +03:00
Andrew Dolgov deb441e9e3
make phpstan happy 2023-10-23 23:16:54 +03:00
Andrew Dolgov 9826d2f075
add a self url path hack to strip request path directories (needed for /api/index.php) 2023-10-23 23:10:17 +03:00
Andrew Dolgov e956632c5c
set demo webroot values 2023-10-23 09:43:25 +03:00
Andrew Dolgov 7af2938aea
demo - enable auto restart 2023-10-22 22:02:52 +03:00
Andrew Dolgov c28955c8ba
remove helm debug, hide demo job behind CI var 2023-10-22 19:27:42 +03:00
Andrew Dolgov a7f3543516
we don't need a separate demo stage now 2023-10-22 19:26:31 +03:00
Andrew Dolgov 761c3826d1
set imageTag 2023-10-22 19:24:19 +03:00
Andrew Dolgov de39d97e1f
move demo to later stage 2023-10-22 19:20:48 +03:00
Andrew Dolgov 1bfae41e6d
add demo k8s job 2023-10-22 19:18:27 +03:00
Andrew Dolgov efd5d79dde
make sure we fail properly 2023-10-22 13:51:24 +03:00
Andrew Dolgov db05575b2d
add configurable ns 2023-10-22 13:42:41 +03:00
Andrew Dolgov ce3eb32076
un-mock test, use SELENIUM_IMAGE 2023-10-22 13:35:01 +03:00
Andrew Dolgov 752c692170
use CI_COMMIT_SHORT_SHA for selenium test mock 2023-10-22 12:46:39 +03:00
Andrew Dolgov 8d3f570ee9
Merge branch 'master' into protected/selenium 2023-10-22 12:20:38 +03:00
Andrew Dolgov 7bba4ae558
remove startup checks for SELF_URL_PATH, rely on auto-detection instead 2023-10-22 12:19:05 +03:00
Andrew Dolgov 382d01e8db
update test filename 2023-10-22 11:19:56 +03:00
Andrew Dolgov 487635ca28
add integration branch job 2023-10-22 10:59:39 +03:00
Andrew Dolgov bde94dbf4b
add selenium mock 2023-10-22 10:57:58 +03:00
Andrew Dolgov 322296d6a0
fix local compose file typo, wait a bit before curling login page 2023-10-22 10:35:35 +03:00
Andrew Dolgov ccb4a4d337
fix previous 2023-10-22 10:24:14 +03:00
Andrew Dolgov b0f96dbb5a
force create cache directories on app startup 2023-10-22 10:22:47 +03:00
Andrew Dolgov aec8cdd0c8
enable updater by default 2023-10-22 10:11:24 +03:00
Andrew Dolgov cb90393a7e
compose tweaks 2023-10-22 09:55:07 +03:00
Andrew Dolgov 028afdd7d5
add simple dev compose 2023-10-22 09:40:08 +03:00
Andrew Dolgov 6b1b496248
test: run curl to get login page 2023-10-21 20:59:26 +03:00
Andrew Dolgov d744209df7
move phpdoc to publish stage 2023-10-21 20:32:31 +03:00
Andrew Dolgov eac076fcd6
set phpdoc to always run 2023-10-21 20:22:59 +03:00
Andrew Dolgov e7ddbbb2ce
add publish jobs 2023-10-21 20:17:32 +03:00
Andrew Dolgov ff818a75f0
test stub 2023-10-21 19:55:15 +03:00
Andrew Dolgov 03e956132d
switch to html2text() instead of strip_tags() when preparing FTS index 2023-10-21 10:51:24 +03:00
Andrew Dolgov 2b61052e87
cosmetic fix for root span name 2023-10-21 10:25:29 +03:00
Andrew Dolgov cf18bc576e
fix previous 2023-10-21 10:25:03 +03:00
Andrew Dolgov 3bf275e445
stop whining if _SESSION etc are not defined 2023-10-21 10:24:23 +03:00
Andrew Dolgov 492c4eecfb
show logged in user as root span name 2023-10-21 10:19:53 +03:00
Andrew Dolgov 93bb473bce
make phpstan happy, run phpstan on all files on task startup 2023-10-21 10:02:49 +03:00
Andrew Dolgov 6e025103d3
a bit more tracing 2023-10-20 23:44:56 +03:00
Andrew Dolgov 350177df39
add placeholder instrumentation for public 2023-10-20 23:39:30 +03:00
Andrew Dolgov d3fadc0bd0
stop calling spans scopes 2023-10-20 22:39:41 +03:00
Andrew Dolgov bf6e3c381b
make tracer field non-static 2023-10-20 21:34:36 +03:00
Andrew Dolgov 7092a1e85d
OPENTELEMETRY_HOST -> OPENTELEMETRY_ENDPOINT 2023-10-20 21:27:10 +03:00
Andrew Dolgov 62ca093b75
make phpstan & watcher happy, stop running phpstan on vendor/ 2023-10-20 21:22:03 +03:00
Andrew Dolgov cdd7ad020e
jaeger-client -> opentelemetry 2023-10-20 21:13:39 +03:00
Andrew Dolgov 45a9ff0c88
unharcode proxy registry 2023-10-19 18:18:21 +03:00
Andrew Dolgov 6c75ea17da
Revert "Revert "exp: switch to kaniko""
This reverts commit b07ad642de.
2023-10-19 09:47:01 +03:00
Andrew Dolgov b07ad642de
Revert "exp: switch to kaniko"
This reverts commit 56315b39b4.
2023-10-19 09:21:49 +03:00
Andrew Dolgov 56315b39b4
exp: switch to kaniko 2023-10-17 16:38:44 +03:00
Andrew Dolgov 89f5af62d8
update phpdoc image 2023-10-14 15:18:32 +03:00
Andrew Dolgov 9556519e67
fix content_preview not shown in JSON shared feed 2023-10-11 17:34:01 +03:00
Andrew Dolgov c779e2ba0d
batch feed editor: don't try to save feed_url or title, those aren't in the dialog 2023-10-04 18:32:35 +03:00
Andrew Dolgov 40df94c169
fix feed_language being unnecessarily quoted in batch feed editor 2023-10-04 18:27:31 +03:00
Andrew Dolgov e29fe626e1
enable fpm status page 2023-10-02 09:36:26 +03:00
Andrew Dolgov b15f185e3d
Revert "CI: use nexus alpine proxy"
This reverts commit afd04d141c.
2023-09-22 19:11:28 +03:00
Andrew Dolgov f489f620d0
phpstan fix 2023-09-18 18:52:22 +03:00
Andrew Dolgov dd6ac57a07
feed debugger: add content regexp matches to filter debug output 2023-09-18 11:45:51 +03:00
Andrew Dolgov 03526d8151
gitignore phpstan-tmp 2023-09-01 11:42:53 +03:00
Andrew Dolgov 8535305cfc
phpstan: set tmp dir 2023-09-01 11:22:36 +03:00
Andrew Dolgov afd04d141c
CI: use nexus alpine proxy 2023-08-28 09:58:29 +03:00
Andrew Dolgov 485bfe327a
phpstan: exclude intervention from plugins/ 2023-08-28 09:23:17 +03:00
Andrew Dolgov e2ab00c889
Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2023-08-12 09:01:22 +03:00
Andrew Dolgov 83f5ab5c79
fix basename() being passed a NULL value 2023-08-12 09:00:57 +03:00
Andrew Dolgov faefedb950 Merge branch 'protected/dockerignore-test' into 'master'
add .dockerignore

See merge request tt-rss/tt-rss!9
2023-08-06 16:19:16 +00:00
Andrew Dolgov adba0aa8d2
add .dockerignore 2023-08-06 19:02:38 +03:00
Andrew Dolgov ba6a912abd
use non-deprecated variant of get_schema_version() 2023-08-03 07:24:48 +03:00
Andrew Dolgov bd95325f8d
phpstan: exclude psr/log 2023-08-03 07:24:29 +03:00
Andrew Dolgov 1d788eddf8
* logger: add optional HTML output
* feed debugger: add checkbox to dump feed XML
2023-08-02 09:10:05 +03:00
Andrew Dolgov 3d255d861c
use nginx envsubst to make tt-rss root configurable 2023-07-28 21:23:57 +03:00
1362 changed files with 123787 additions and 25840 deletions

View File

@ -1,22 +1,25 @@
FROM registry.fakecake.org/docker.io/alpine:3.18
ARG PROXY_REGISTRY
FROM ${PROXY_REGISTRY}alpine:3.19
EXPOSE 9000/tcp
ENV SCRIPT_ROOT=/opt/tt-rss
ENV SRC_DIR=/src/tt-rss/
RUN apk add --no-cache dcron php82 php82-fpm php82-phar php82-sockets php82-pecl-apcu \
php82-pdo php82-gd php82-pgsql php82-pdo_pgsql php82-xmlwriter php82-opcache \
php82-mbstring php82-intl php82-xml php82-curl php82-simplexml \
php82-session php82-tokenizer php82-dom php82-fileinfo php82-ctype \
php82-json php82-iconv php82-pcntl php82-posix php82-zip php82-exif \
php82-openssl git postgresql-client sudo php82-pecl-xdebug rsync tzdata && \
sed -i 's/\(memory_limit =\) 128M/\1 256M/' /etc/php82/php.ini && \
RUN apk add --no-cache dcron php83 php83-fpm php83-phar php83-sockets php83-pecl-apcu \
php83-pdo php83-gd php83-pgsql php83-pdo_pgsql php83-xmlwriter php83-opcache \
php83-mbstring php83-intl php83-xml php83-curl php83-simplexml \
php83-session php83-tokenizer php83-dom php83-fileinfo php83-ctype \
php83-json php83-iconv php83-pcntl php83-posix php83-zip php83-exif \
php83-openssl git postgresql-client sudo php83-pecl-xdebug rsync tzdata && \
sed -i 's/\(memory_limit =\) 128M/\1 256M/' /etc/php83/php.ini && \
sed -i -e 's/^listen = 127.0.0.1:9000/listen = 9000/' \
-e 's/;\(clear_env\) = .*/\1 = no/i' \
-e 's/;\(pm.status_path = \/status\)/\1/i' \
-e 's/;\(pm.status_listen\) = .*/\1 = 9001/i' \
-e 's/^\(user\|group\) = .*/\1 = app/i' \
-e 's/;\(php_admin_value\[error_log\]\) = .*/\1 = \/tmp\/error.log/' \
-e 's/;\(php_admin_flag\[log_errors\]\) = .*/\1 = on/' \
/etc/php82/php-fpm.d/www.conf && \
/etc/php83/php-fpm.d/www.conf && \
mkdir -p /var/www ${SCRIPT_ROOT}/config.d
ARG CI_COMMIT_BRANCH
@ -31,15 +34,17 @@ ENV CI_COMMIT_TIMESTAMP=${CI_COMMIT_TIMESTAMP}
ARG CI_COMMIT_SHA
ENV CI_COMMIT_SHA=${CI_COMMIT_SHA}
ADD --chmod=0755 startup.sh ${SCRIPT_ROOT}
ADD --chmod=0755 updater.sh ${SCRIPT_ROOT}
ADD --chmod=0755 dcron.sh ${SCRIPT_ROOT}
ADD --chmod=0755 backup.sh /etc/periodic/weekly/backup
ADD .docker/app/startup.sh ${SCRIPT_ROOT}
ADD .docker/app/updater.sh ${SCRIPT_ROOT}
ADD .docker/app/dcron.sh ${SCRIPT_ROOT}
ADD .docker/app/backup.sh /etc/periodic/weekly/backup
ADD index.php ${SCRIPT_ROOT}
ADD config.docker.php ${SCRIPT_ROOT}
RUN chmod 0755 ${SCRIPT_ROOT}/*.sh
COPY --from=app-src . ${SRC_DIR}
ADD .docker/app/index.php ${SCRIPT_ROOT}
ADD .docker/app/config.docker.php ${SCRIPT_ROOT}
COPY . ${SRC_DIR}
ARG ORIGIN_REPO_XACCEL=https://git.tt-rss.org/fox/ttrss-nginx-xaccel.git
@ -62,6 +67,7 @@ ENV ADMIN_USER_ACCESS_LEVEL=""
ENV AUTO_CREATE_USER=""
ENV AUTO_CREATE_USER_PASS=""
ENV AUTO_CREATE_USER_ACCESS_LEVEL="0"
ENV AUTO_CREATE_USER_ENABLE_API=""
# TODO: remove prefix from container variables not used by tt-rss itself:
#
@ -81,7 +87,7 @@ ENV TTRSS_DB_HOST="db"
ENV TTRSS_DB_PORT="5432"
ENV TTRSS_MYSQL_CHARSET="UTF8"
ENV TTRSS_PHP_EXECUTABLE="/usr/bin/php82"
ENV TTRSS_PHP_EXECUTABLE="/usr/bin/php83"
ENV TTRSS_PLUGINS="auth_internal, note, nginx_xaccel"
CMD ${SCRIPT_ROOT}/startup.sh

View File

@ -24,13 +24,14 @@ export PGPASSWORD=$TTRSS_DB_PASS
[ ! -e /var/www/html/index.php ] && cp ${SCRIPT_ROOT}/index.php /var/www/html
if [ ! -d $DST_DIR ]; then
if [ -z $SKIP_RSYNC_ON_STARTUP ]; then
if [ ! -d $DST_DIR ]; then
mkdir -p $DST_DIR
chown $OWNER_UID:$OWNER_GID $DST_DIR
sudo -u app rsync -a \
$SRC_DIR/ $DST_DIR/
else
else
chown -R $OWNER_UID:$OWNER_GID $DST_DIR
sudo -u app rsync -a --delete \
@ -46,9 +47,12 @@ else
sudo -u app rsync -a --delete \
$SRC_DIR/plugins.local/nginx_xaccel \
$DST_DIR/plugins.local/nginx_xaccel
fi
else
echo "warning: working copy in $DST_DIR won't be updated, make sure you know what you're doing."
fi
for d in cache lock feed-icons plugins.local themes.local; do
for d in cache lock feed-icons plugins.local themes.local templates.local cache/export cache/feeds cache/images cache/upload; do
sudo -u app mkdir -p $DST_DIR/$d
done
@ -61,7 +65,7 @@ sudo -u app cp ${SCRIPT_ROOT}/config.docker.php $DST_DIR/config.php
chmod 644 $DST_DIR/config.php
chown -R $OWNER_UID:$OWNER_GID $DST_DIR \
/var/log/php82
/var/log/php83
if [ -z "$TTRSS_NO_STARTUP_PLUGIN_UPDATES" ]; then
echo updating all local plugins...
@ -100,9 +104,9 @@ if [ ! -z "${TTRSS_XDEBUG_ENABLED}" ]; then
fi
echo enabling xdebug with the following parameters:
env | grep TTRSS_XDEBUG
cat > /etc/php82/conf.d/50_xdebug.ini <<EOF
cat > /etc/php83/conf.d/50_xdebug.ini <<EOF
zend_extension=xdebug.so
xdebug.mode=develop,trace,debug
xdebug.mode=debug
xdebug.start_with_request = yes
xdebug.client_port = ${TTRSS_XDEBUG_PORT}
xdebug.client_host = ${TTRSS_XDEBUG_HOST}
@ -110,17 +114,17 @@ EOF
fi
sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \
/etc/php82/php.ini
/etc/php83/php.ini
sed -i.bak "s/^\(pm.max_children\) = \(.*\)/\1 = ${PHP_WORKER_MAX_CHILDREN}/" \
/etc/php82/php-fpm.d/www.conf
/etc/php83/php-fpm.d/www.conf
sudo -Eu app php82 $DST_DIR/update.php --update-schema=force-yes
sudo -Eu app php83 $DST_DIR/update.php --update-schema=force-yes
if [ ! -z "$ADMIN_USER_PASS" ]; then
sudo -Eu app php82 $DST_DIR/update.php --user-set-password "admin:$ADMIN_USER_PASS"
sudo -Eu app php83 $DST_DIR/update.php --user-set-password "admin:$ADMIN_USER_PASS"
else
if sudo -Eu app php82 $DST_DIR/update.php --user-check-password "admin:password"; then
if sudo -Eu app php83 $DST_DIR/update.php --user-check-password "admin:password"; then
RANDOM_PASS=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 16 ; echo '')
echo "*****************************************************************************"
@ -128,17 +132,23 @@ else
echo "* If you want to set it manually, use ADMIN_USER_PASS environment variable. *"
echo "*****************************************************************************"
sudo -Eu app php82 $DST_DIR/update.php --user-set-password "admin:$RANDOM_PASS"
sudo -Eu app php83 $DST_DIR/update.php --user-set-password "admin:$RANDOM_PASS"
fi
fi
if [ ! -z "$ADMIN_USER_ACCESS_LEVEL" ]; then
sudo -Eu app php82 $DST_DIR/update.php --user-set-access-level "admin:$ADMIN_USER_ACCESS_LEVEL"
sudo -Eu app php83 $DST_DIR/update.php --user-set-access-level "admin:$ADMIN_USER_ACCESS_LEVEL"
fi
if [ ! -z "$AUTO_CREATE_USER" ]; then
sudo -Eu app /bin/sh -c "php82 $DST_DIR/update.php --user-exists $AUTO_CREATE_USER ||
php82 $DST_DIR/update.php --force-yes --user-add \"$AUTO_CREATE_USER:$AUTO_CREATE_USER_PASS:$AUTO_CREATE_USER_ACCESS_LEVEL\""
sudo -Eu app /bin/sh -c "php83 $DST_DIR/update.php --user-exists $AUTO_CREATE_USER ||
php83 $DST_DIR/update.php --force-yes --user-add \"$AUTO_CREATE_USER:$AUTO_CREATE_USER_PASS:$AUTO_CREATE_USER_ACCESS_LEVEL\""
if [ ! -z "$AUTO_CREATE_USER_ENABLE_API" ]; then
# TODO: remove || true later
sudo -Eu app /bin/sh -c "php83 $DST_DIR/update.php --user-enable-api \"$AUTO_CREATE_USER:$AUTO_CREATE_USER_ENABLE_API\"" || true
fi
fi
rm -f /tmp/error.log && mkfifo /tmp/error.log && chown app:app /tmp/error.log
@ -150,4 +160,4 @@ unset AUTO_CREATE_USER_PASS
touch $DST_DIR/.app_is_ready
exec /usr/sbin/php-fpm82 --nodaemonize --force-stderr
exec /usr/sbin/php-fpm83 --nodaemonize --force-stderr

View File

@ -21,7 +21,7 @@ while ! pg_isready -h $TTRSS_DB_HOST -U $TTRSS_DB_USER; do
done
sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \
/etc/php82/php.ini
/etc/php83/php.ini
DST_DIR=/var/www/html/tt-rss
@ -30,4 +30,4 @@ while [ ! -s $DST_DIR/config.php -a -e $DST_DIR/.app_is_ready ]; do
sleep 3
done
sudo -E -u app /usr/bin/php82 /var/www/html/tt-rss/update_daemon2.php
sudo -E -u app /usr/bin/php83 /var/www/html/tt-rss/update_daemon2.php

View File

@ -1,13 +1,27 @@
FROM registry.fakecake.org/docker.io/nginx:alpine
ARG PROXY_REGISTRY
FROM ${PROXY_REGISTRY}nginx:alpine
HEALTHCHECK CMD curl --fail http://localhost/tt-rss/index.php || exit 1
HEALTHCHECK CMD curl --fail http://localhost${APP_BASE}/index.php || exit 1
COPY nginx.conf /etc/nginx/templates/nginx.conf.template
COPY .docker/web-nginx/nginx.conf /etc/nginx/templates/nginx.conf.template
# By default, nginx will send the php requests to "app" server, but this server
# name can be overridden at runtime by passing an APP_UPSTREAM env var
ENV APP_UPSTREAM=${APP_UPSTREAM:-app}
# Webroot (defaults to /var/www/html)
ENV APP_WEB_ROOT=${APP_WEB_ROOT:-/var/www/html}
# Base location for tt-rss (defaults to /tt-rss)
ENV APP_BASE=${APP_BASE:-/tt-rss}
# Resolver for nginx (kube-dns.kube-system.svc.cluster.local for k8s)
ENV RESOLVER=${RESOLVER:-127.0.0.11}
# In order to make tt-rss appear on website root without /tt-rss/ set above as follows in .env:
# APP_WEB_ROOT=/var/www/html/tt-rss
# APP_BASE=
# It's necessary to set the following NGINX_ENVSUBST_OUTPUT_DIR env var to tell
# nginx to replace the env vars of /etc/nginx/templates/nginx.conf.template
# and put the result in /etc/nginx/nginx.conf (instead of /etc/nginx/conf.d/nginx.conf)

View File

@ -16,25 +16,24 @@ http {
index index.php;
upstream app {
server ${APP_UPSTREAM}:9000;
}
resolver ${RESOLVER} valid=5s;
server {
listen 80;
listen [::]:80;
root ${APP_WEB_ROOT};
root /var/www/html;
location /tt-rss/cache {
location ${APP_BASE}/cache {
aio threads;
internal;
}
location /tt-rss/backups {
location ${APP_BASE}/backups {
internal;
}
rewrite ${APP_BASE}/healthz ${APP_BASE}/public.php?op=healthcheck;
location ~ \.php$ {
# regex to split $uri to $fastcgi_script_name and $fastcgi_path
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
@ -50,7 +49,9 @@ http {
fastcgi_index index.php;
include fastcgi.conf;
fastcgi_pass app;
set $backend "${APP_UPSTREAM}:9000";
fastcgi_pass $backend;
}
location / {

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
.git/
cache/
plugins.local/
templates.local/
themes.local/

47
.env-dist Normal file
View File

@ -0,0 +1,47 @@
# Copy this file to .env before building the container. Put any local modifications here.
# Run FPM under this UID/GID.
# OWNER_UID=1000
# OWNER_GID=1000
# FPM settings.
#PHP_WORKER_MAX_CHILDREN=5
#PHP_WORKER_MEMORY_LIMIT=256M
# ADMIN_USER_* settings are applied on every startup.
# Set admin user password to this value. If not set, random password will be generated on startup, look for it in the 'app' container logs.
#ADMIN_USER_PASS=
# Sets admin user access level to this value. Valid values:
# -2 - forbidden to login
# -1 - readonly
# 0 - default user
# 10 - admin
#ADMIN_USER_ACCESS_LEVEL=
# Auto create another user (in addition to built-in admin) unless it already exists.
#AUTO_CREATE_USER=
#AUTO_CREATE_USER_PASS=
#AUTO_CREATE_USER_ACCESS_LEVEL=0
# Default database credentials.
TTRSS_DB_USER=postgres
TTRSS_DB_NAME=postgres
TTRSS_DB_PASS=password
# This is a fallback value for PHP CLI SAPI, it should be set to a fully-qualified tt-rss URL
# TTRSS_SELF_URL_PATH=http://example.com/tt-rss
# You can customize other config.php defines by setting overrides here. See tt-rss/.docker/app/Dockerfile for complete list. Examples:
# TTRSS_PLUGINS=auth_remote
# TTRSS_SINGLE_USER_MODE=true
# TTRSS_SESSION_COOKIE_LIFETIME=2592000
# TTRSS_FORCE_ARTICLE_PURGE=30
# ...
# Bind exposed port to 127.0.0.1 to run behind reverse proxy on the same host. If you plan expose the container, remove "127.0.0.1:".
HTTP_PORT=127.0.0.1:8280
#HTTP_PORT=8280

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
Thumbs.db
/.env
/docker-compose.override.yml
/.app_is_ready
/messages.mo
/node_modules
@ -12,3 +14,5 @@ Thumbs.db
/.vscode/settings.json
/vendor/**/.git
/.phpunit.result.cache
/.phpstan-tmp
/.tools/

View File

@ -1,20 +1,28 @@
stages:
- lint
- build
- test
- publish
variables:
ESLINT_PATHS: js plugins
REGISTRY_PROJECT: cthulhoo
include:
- project: 'ci/ci-templates'
ref: master
file: .ci-build-docker.yml
file: .ci-build-docker-kaniko.yml
- project: 'ci/ci-templates'
ref: master
file: .ci-lint-common.yml
- project: 'ci/ci-templates'
ref: master
file: .ci-integration-test.yml
phpunit:
extends: .phpunit
variables:
PHPUNIT_ARGS: --exclude integration --coverage-filter classes --coverage-filter include
eslint:
extends: .eslint
@ -22,39 +30,96 @@ eslint:
phpstan:
extends: .phpstan
ttrss-web-nginx:
extends: .build-master
variables:
BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/web-nginx
ttrss-web-nginx:branch:
extends: .build-branch
variables:
BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/web-nginx
ttrss-fpm-pgsql-static:
extends: .build-master
variables:
BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/app
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/web-nginx/Dockerfile
ttrss-fpm-pgsql-static:branch:
extends: .build-branch
variables:
BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/app
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/app/Dockerfile
ttrss-web-nginx:
extends: .build-master-commit-only
variables:
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/web-nginx/Dockerfile
ttrss-fpm-pgsql-static:
extends: .build-master-commit-only
variables:
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/app/Dockerfile
phpdoc:
image:
name: ${CI_DOCKER_IMAGE}
stage: build
image: ${PHP_IMAGE}
stage: publish
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE != "web" && $PHPDOC_DEPLOY_SSH_KEY != null
changes:
- '**/*.php'
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $PHPDOC_DEPLOY_SSH_KEY != null
when: manual
script:
- php81 /phpDocumentor.phar -d classes -d include -t phpdoc --visibility=public
- php83 /phpDocumentor.phar -d classes -d include -t phpdoc --visibility=public
- mkdir -p ~/.ssh &&
cp ${PHPDOC_DEPLOY_SSH_KEY} ~/.ssh/id_ed25519 &&
chmod 0600 ~/.ssh/id_ed25519
- rsync -av -e 'ssh -o StrictHostKeyChecking=no' phpdoc/ ${PHPDOC_DEPLOY_HOST}:phpdoc/
phpunit-integration:
image: ${PHP_IMAGE}
variables:
TEST_HELM_REPO: https://gitlab.tt-rss.org/tt-rss/helm-charts/tt-rss
extends: .integration-test
script:
- export K8S_NAMESPACE=$(kubectl get pods -o=custom-columns=NS:.metadata.namespace | tail -1)
- export API_URL="http://tt-rss-${CI_COMMIT_SHORT_SHA}-app.$K8S_NAMESPACE.svc.cluster.local/tt-rss/api/"
- export TTRSS_DB_HOST=tt-rss-${CI_COMMIT_SHORT_SHA}-app.$K8S_NAMESPACE.svc.cluster.local
- export TTRSS_DB_USER=postgres
- export TTRSS_DB_NAME=postgres
- export TTRSS_DB_PASS=password
- php83 vendor/bin/phpunit --group integration --do-not-cache-result --log-junit phpunit-report.xml --coverage-cobertura phpunit-coverage.xml --coverage-text --colors=never
artifacts:
when: always
reports:
junit: phpunit-report.xml
coverage_report:
coverage_format: cobertura
path: phpunit-coverage.xml
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
selenium:
image: ${SELENIUM_IMAGE}
variables:
TEST_HELM_REPO: https://gitlab.tt-rss.org/tt-rss/helm-charts/tt-rss
SELENIUM_GRID_ENDPOINT: http://selenium-hub.selenium-grid.svc.cluster.local:4444/wd/hub
extends: .integration-test
script:
- export K8S_NAMESPACE=$(kubectl get pods -o=custom-columns=NS:.metadata.namespace | tail -1)
- python3 tests/integration/selenium_test.py
needs:
- job: phpunit-integration
artifacts:
when: always
reports:
junit: selenium-report.xml
ttrss-web-nginx:publish:
stage: publish
extends: .build-master
variables:
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/web-nginx/Dockerfile
ttrss-fpm-pgsql-static:publish:
stage: publish
extends: .build-master
variables:
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/app/Dockerfile
update-demo:
stage: publish
image: ${HELM_IMAGE}
variables:
HELM_REPO: https://gitlab.tt-rss.org/tt-rss/helm-charts/tt-rss
script:
- git clone ${HELM_REPO} chart
- helm upgrade --atomic --install tt-rss-demo chart --values .helm/values-demo.yaml --set imageTag=${CI_COMMIT_SHORT_SHA}
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_REGISTRY_USER != null && $UPDATE_DEMO == "true"

18
.helm/values-demo.yaml Normal file
View File

@ -0,0 +1,18 @@
imageTag: latest
user:
name: demo
password: demo
access_level: 10
virtualservice:
suffix: k3s.kake
additional_domains:
- demo.tt-rss.org
web:
root: /var/www/html/tt-rss
base: ""
restart:
enabled: true

18
.vscode/tasks.json vendored
View File

@ -3,11 +3,14 @@
"tasks": [
{
"type": "shell",
"label": "phpstan 8.1 (watcher)",
"label": "phpstan (watcher)",
"isBackground": true,
"problemMatcher": {
"fileLocation": ["relative", "${workspaceRoot}"],
"owner": "phpstan-watcher-8.1",
"fileLocation": [
"relative",
"${workspaceRoot}"
],
"owner": "phpstan-watcher",
"pattern": {
"regexp": "^/app/(.*?):([0-9\\?]*):(.*)$",
"file": 1,
@ -18,10 +21,15 @@
"activeOnStart": true,
"beginsPattern": "Using configuration file",
"endsPattern": "All done"
}
},
"command": "${workspaceRoot}/utils/phpstan-watcher.sh",
"command": "chmod +x ${workspaceRoot}/utils/phpstan-watcher.sh && ${workspaceRoot}/utils/phpstan-watcher.sh"
},
{
"type": "shell",
"label": "phpunit",
"command": "chmod +x ${workspaceRoot}/utils/phpunit.sh && ${workspaceRoot}/utils/phpunit.sh",
"problemMatcher": []
},
{
"type": "gulp",

View File

@ -30,17 +30,14 @@
$op = (string)clean($op);
$method = (string)clean($method);
$scope = Tracer::start(__FILE__, ['tags' => json_encode($_REQUEST)]);
startup_gettext();
$script_started = microtime(true);
if (!init_plugins()) {
$scope->close();
return;
}
$span = OpenTelemetry\API\Trace\Span::getCurrent();
header("Content-Type: text/json; charset=utf-8");
if (Config::get(Config::SINGLE_USER_MODE)) {
@ -52,8 +49,7 @@
header("Content-Type: text/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
$scope->getSpan()->setTag('error', Errors::E_UNAUTHORIZED);
$scope->close();
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
return;
}
UserHelper::load_user_plugins($_SESSION["uid"]);
@ -62,8 +58,7 @@
if (Config::is_migration_needed()) {
print Errors::to_json(Errors::E_SCHEMA_MISMATCH);
$scope->getSpan()->setTag('error', Errors::E_SCHEMA_MISMATCH);
$scope->close();
$span->setAttribute('error', Errors::E_SCHEMA_MISMATCH);
return;
}
@ -115,7 +110,7 @@
$op = "pluginhandler";
} */
$op = str_replace("-", "_", $op);
// $op = str_replace(, "_", $op);
$override = PluginHost::getInstance()->lookup_handler($op, $method);
@ -126,8 +121,7 @@
header("Content-Type: text/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
$scope->getSpan()->setTag('error', Errors::E_UNAUTHORIZED);
$scope->close();
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
return;
}
@ -140,18 +134,16 @@
}
if (implements_interface($handler, 'IHandler')) {
$h_scope = Tracer::start("construct/$op");
$span->addEvent("construct/$op");
$handler->__construct($_REQUEST);
$h_scope->close();
if (validate_csrf($csrf_token) || $handler->csrf_ignore($method)) {
$b_scope = Tracer::start("before/$method");
$span->addEvent("before/$method");
$before = $handler->before($method);
$b_scope->close();
if ($before) {
$m_scope = Tracer::start("method/$method");
$span->addEvent("method/$method");
if ($method && method_exists($handler, $method)) {
$reflection = new ReflectionMethod($handler, $method);
@ -161,7 +153,7 @@
user_error("Refusing to invoke method $method of handler $op which has required parameters.", E_USER_WARNING);
header("Content-Type: text/json");
$m_scope->getSpan()->setTag('error', Errors::E_UNAUTHORIZED);
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
print Errors::to_json(Errors::E_UNAUTHORIZED);
}
} else {
@ -170,24 +162,19 @@
} else {
header("Content-Type: text/json");
$m_scope->getSpan()->setTag('error', Errors::E_UNKNOWN_METHOD);
$span->setAttribute('error', Errors::E_UNKNOWN_METHOD);
print Errors::to_json(Errors::E_UNKNOWN_METHOD, ["info" => get_class($handler) . "->$method"]);
}
}
$m_scope->close();
$a_scope = Tracer::start("after/$method");
$span->addEvent("after/$method");
$handler->after();
$a_scope->close();
$scope->close();
return;
} else {
header("Content-Type: text/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
$scope->getSpan()->setTag('error', Errors::E_UNAUTHORIZED);
$scope->close();
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
return;
}
} else {
@ -195,8 +182,7 @@
header("Content-Type: text/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
$scope->getSpan()->setTag('error', Errors::E_UNAUTHORIZED);
$scope->close();
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
return;
}
}
@ -205,5 +191,4 @@
header("Content-Type: text/json");
print Errors::to_json(Errors::E_UNKNOWN_METHOD, [ "info" => (isset($handler) ? get_class($handler) : "UNKNOWN:".$op) . "->$method"]);
$scope->getSpan()->setTag('error', Errors::E_UNKNOWN_METHOD);
$scope->close();
$span->setAttribute('error', Errors::E_UNKNOWN_METHOD);

0
cache/export/.empty vendored Executable file → Normal file
View File

0
cache/images/.empty vendored Executable file → Normal file
View File

14
classes/api.php → classes/API.php Executable file → Normal file
View File

@ -691,20 +691,6 @@ class API extends Handler {
}
}
$params = array(
"feed" => $feed_id,
"limit" => $limit,
"view_mode" => $view_mode,
"cat_view" => $is_cat,
"search" => $search,
"override_order" => $order,
"offset" => $offset,
"since_id" => $since_id,
"include_children" => $include_nested,
"check_first_id" => $check_first_id,
"skip_first_id_check" => $skip_first_id_check
);
$qfh_ret = [];
if (!$is_cat && is_numeric($feed_id) && $feed_id < PLUGIN_FEED_BASE_INDEX && $feed_id > LABEL_BASE_INDEX) {

32
classes/article.php → classes/Article.php Executable file → Normal file
View File

@ -90,7 +90,7 @@ class Article extends Handler_Protected {
SET tsvector_combined = to_tsvector( :ts_content)
WHERE id = :id");
$params = [
":ts_content" => mb_substr(strip_tags($content ), 0, 900000),
":ts_content" => mb_substr(\Soundasleep\Html2Text::convert($content), 0, 900000),
":id" => $ref_id];
$sth->execute($params);
}
@ -135,7 +135,7 @@ class Article extends Handler_Protected {
SET tsvector_combined = to_tsvector( :ts_content)
WHERE id = :id");
$params = [
":ts_content" => mb_substr(strip_tags($content ), 0, 900000),
":ts_content" => mb_substr(\Soundasleep\Html2Text::convert($content), 0, 900000),
":id" => $ref_id];
$sth->execute($params);
}
@ -298,7 +298,7 @@ class Article extends Handler_Protected {
* @return array{'formatted': string, 'entries': array<int, array<string, mixed>>}
*/
static function _format_enclosures(int $id, bool $always_display_enclosures, string $article_content, bool $hide_images = false): array {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$enclosures = self::_get_enclosures($id);
$enclosures_formatted = "";
@ -326,7 +326,7 @@ class Article extends Handler_Protected {
$enclosures_formatted, $enclosures, $id, $always_display_enclosures, $article_content, $hide_images);
if (!empty($enclosures_formatted)) {
$scope->close();
$span->end();
return [
'formatted' => $enclosures_formatted,
'entries' => []
@ -370,7 +370,7 @@ class Article extends Handler_Protected {
}
}
$scope->close();
$span->end();
return $rv;
}
@ -378,7 +378,7 @@ class Article extends Handler_Protected {
* @return array<int, string>
*/
static function _get_tags(int $id, int $owner_uid = 0, ?string $tag_cache = null): array {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$a_id = $id;
@ -427,7 +427,7 @@ class Article extends Handler_Protected {
$sth->execute([$tags_str, $id, $owner_uid]);
}
$scope->close();
$span->end();
return $tags;
}
@ -522,7 +522,7 @@ class Article extends Handler_Protected {
* @return array<int, array<int, int|string>>
*/
static function _get_labels(int $id, ?int $owner_uid = null): array {
$scope = Tracer::start(__METHOD__, []);
$span = Tracer::start(__METHOD__);
$rv = array();
@ -569,7 +569,7 @@ class Article extends Handler_Protected {
else
Labels::update_cache($owner_uid, $id, array("no-labels" => 1));
$scope->close();
$span->end();
return $rv;
}
@ -581,7 +581,7 @@ class Article extends Handler_Protected {
* @return array<int, Article::ARTICLE_KIND_*|string>
*/
static function _get_image(array $enclosures, string $content, string $site_url, array $headline) {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$article_image = "";
$article_stream = "";
@ -606,7 +606,7 @@ class Article extends Handler_Protected {
foreach ($elems as $e) {
if ($e->nodeName == "iframe") {
$matches = [];
if ($rrr = preg_match("/\/embed\/([\w-]+)/", $e->getAttribute("src"), $matches)) {
if (preg_match("/\/embed\/([\w-]+)/", $e->getAttribute("src"), $matches)) {
$article_image = "https://img.youtube.com/vi/" . $matches[1] . "/hqdefault.jpg";
$article_stream = "https://youtu.be/" . $matches[1];
$article_kind = Article::ARTICLE_KIND_YOUTUBE;
@ -660,7 +660,7 @@ class Article extends Handler_Protected {
if ($article_stream && $cache->exists(sha1($article_stream)))
$article_stream = $cache->get_url(sha1($article_stream));
$scope->close();
$span->end();
return [$article_image, $article_stream, $article_kind];
}
@ -675,7 +675,7 @@ class Article extends Handler_Protected {
if (count($article_ids) == 0)
return [];
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$entries = ORM::for_table('ttrss_entries')
->table_alias('e')
@ -696,7 +696,7 @@ class Article extends Handler_Protected {
}
}
$scope->close();
$span->end();
return array_unique($rv);
}
@ -709,7 +709,7 @@ class Article extends Handler_Protected {
if (count($article_ids) == 0)
return [];
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$entries = ORM::for_table('ttrss_entries')
->table_alias('e')
@ -723,7 +723,7 @@ class Article extends Handler_Protected {
array_push($rv, $entry->feed_id);
}
$scope->close();
$span->end();
return array_unique($rv);
}

View File

@ -46,10 +46,7 @@ class Config {
* garbage unicode characters with this option, try setting it to a blank string. */
const MYSQL_CHARSET = "MYSQL_CHARSET";
/** this should be set to a fully qualified URL used to access
* your tt-rss instance over the net, such as: https://example.com/tt-rss/
* if your tt-rss instance is behind a reverse proxy, use external URL.
* tt-rss will likely help you pick correct value for this on startup */
/** this is a fallback falue for the CLI SAPI, it should be set to a fully-qualified tt-rss URL */
const SELF_URL_PATH = "SELF_URL_PATH";
/** operate in single user mode, disables all functionality related to
@ -193,10 +190,10 @@ class Config {
const HTTP_429_THROTTLE_INTERVAL = "HTTP_429_THROTTLE_INTERVAL";
/** host running Jaeger collector to receive traces (disabled if empty) */
const JAEGER_REPORTING_HOST = "JAEGER_REPORTING_HOST";
const OPENTELEMETRY_ENDPOINT = "OPENTELEMETRY_ENDPOINT";
/** Jaeger service name */
const JAEGER_SERVICE_NAME = "JAEGER_SERVICE_NAME";
const OPENTELEMETRY_SERVICE = "OPENTELEMETRY_SERVICE";
/** default values for all global configuration options */
private const _DEFAULTS = [
@ -207,7 +204,7 @@ class Config {
Config::DB_PASS => [ "", Config::T_STRING ],
Config::DB_PORT => [ "5432", Config::T_STRING ],
Config::MYSQL_CHARSET => [ "UTF8", Config::T_STRING ],
Config::SELF_URL_PATH => [ "", Config::T_STRING ],
Config::SELF_URL_PATH => [ "https://example.com/tt-rss", Config::T_STRING ],
Config::SINGLE_USER_MODE => [ "", Config::T_BOOL ],
Config::SIMPLE_UPDATE_MODE => [ "", Config::T_BOOL ],
Config::PHP_EXECUTABLE => [ "/usr/bin/php", Config::T_STRING ],
@ -255,8 +252,8 @@ class Config {
Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)',
Config::T_STRING ],
Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ],
Config::JAEGER_REPORTING_HOST => [ "", Config::T_STRING ],
Config::JAEGER_SERVICE_NAME => [ "tt-rss", Config::T_STRING ],
Config::OPENTELEMETRY_ENDPOINT => [ "", Config::T_STRING ],
Config::OPENTELEMETRY_SERVICE => [ "tt-rss", Config::T_STRING ],
];
/** @var Config|null */
@ -322,7 +319,7 @@ class Config {
* @return array<string, mixed>|string
*/
private function _get_version(bool $as_string = true) {
$root_dir = dirname(__DIR__);
$root_dir = self::get_self_dir();
if (empty($this->version)) {
$this->version["status"] = -1;
@ -416,7 +413,7 @@ class Config {
private function _get_migrations() : Db_Migrations {
if (empty($this->migrations)) {
$this->migrations = new Db_Migrations();
$this->migrations->initialize(dirname(__DIR__) . "/sql", "ttrss_version", true, self::SCHEMA_VERSION);
$this->migrations->initialize(self::get_self_dir() . "/sql", "ttrss_version", true, self::SCHEMA_VERSION);
}
return $this->migrations;
@ -474,24 +471,22 @@ class Config {
return $instance->_get($param);
}
/** this returns Config::SELF_URL_PATH sans trailing slash */
static function get_self_url() : string {
return preg_replace("#/*$#", "", self::get(Config::SELF_URL_PATH));
}
static function is_server_https() : bool {
return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
(!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https');
}
/** generates reference self_url_path (no trailing slash) */
static function make_self_url() : string {
/** returns fully-qualified external URL to tt-rss (no trailing slash)
* SELF_URL_PATH configuration variable is used as a fallback for the CLI SAPI
* */
static function get_self_url(bool $always_detect = false) : string {
if (!$always_detect && php_sapi_name() == "cli") {
return self::get(Config::SELF_URL_PATH);
} else {
$proto = self::is_server_https() ? 'https' : 'http';
$self_url_path = $proto . '://' . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$self_url_path = preg_replace("/\w+\.php(\?.*$)?$/", "", $self_url_path);
#$self_url_path = preg_replace("/(\?.*$)?$/", "", $self_url_path);
$self_url_path = preg_replace("/(\/api\/{1,})?(\w+\.php)?(\?.*$)?$/", "", $self_url_path);
if (substr($self_url_path, -1) === "/") {
return substr($self_url_path, 0, -1);
@ -499,7 +494,7 @@ class Config {
return $self_url_path;
}
}
}
/* sanity check stuff */
/** checks for mysql tables not using InnoDB (tt-rss is incompatible with MyISAM)
@ -620,27 +615,9 @@ class Config {
// skip check for CLI scripts so that we could install database schema if it is missing.
if (php_sapi_name() != "cli") {
if (self::get_schema_version() < 0) {
array_push($errors, "Base database schema is missing. Either load it manually or perform a migration (<code>update.php --update-schema</code>)");
}
$ref_self_url_path = self::make_self_url();
if ($ref_self_url_path) {
$ref_self_url_path = preg_replace("/\w+\.php$/", "", $ref_self_url_path);
}
if (self::get_self_url() == "http://example.org/tt-rss") {
$hint = $ref_self_url_path ? "(possible value: <b>$ref_self_url_path</b>)" : "";
array_push($errors,
"Please set SELF_URL_PATH to the correct value for your server: $hint");
}
if (self::get_self_url() != $ref_self_url_path) {
array_push($errors,
"Please set SELF_URL_PATH to the correct value detected for your server: <b>$ref_self_url_path</b> (you're using: <b>" . self::get_self_url() . "</b>)");
}
}
if (self::get(Config::DB_TYPE) == "mysql") {
@ -666,7 +643,9 @@ class Config {
}
}
if (count($errors) > 0 && php_sapi_name() != "cli") { ?>
if (count($errors) > 0 && php_sapi_name() != "cli") {
http_response_code(503); ?>
<!DOCTYPE html>
<html>
<head>
@ -724,4 +703,9 @@ class Config {
static function get_user_agent(): string {
return sprintf(self::get(self::HTTP_USER_AGENT), self::get_version());
}
static function get_self_dir() : string {
return dirname(__DIR__); # we're in classes/Config.php
}
}

View File

@ -145,7 +145,7 @@ class Counters {
* @return array<int, array<string, int|string>>
*/
private static function get_feeds(array $feed_ids = null): array {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$ret = [];
@ -212,7 +212,7 @@ class Counters {
}
$scope->close();
$span->end();
return $ret;
}
@ -221,7 +221,7 @@ class Counters {
* @return array<int, array<string, int|string>>
*/
private static function get_global(): array {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$ret = [
[
@ -239,7 +239,7 @@ class Counters {
"counter" => $subcribed_feeds
]);
$scope->close();
$span->end();
return $ret;
}
@ -248,7 +248,7 @@ class Counters {
* @return array<int, array<string, int|string>>
*/
private static function get_virt(): array {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$ret = [];
@ -295,7 +295,7 @@ class Counters {
}
}
$scope->close();
$span->end();
return $ret;
}
@ -304,7 +304,7 @@ class Counters {
* @return array<int, array<string, int|string>>
*/
static function get_labels(array $label_ids = null): array {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$ret = [];
@ -356,7 +356,7 @@ class Counters {
array_push($ret, $cv);
}
$scope->close();
$span->end();
return $ret;
}
}

0
classes/db.php → classes/Db.php Executable file → Normal file
View File

View File

@ -35,7 +35,7 @@ class Db_Migrations {
$sth = $this->pdo->query("SELECT * FROM {$this->migrations_table}");
if ($res = $sth->fetch()) {
if ($sth->fetch()) {
$sth = $this->pdo->prepare("UPDATE {$this->migrations_table} SET schema_version = ?");
} else {
$sth = $this->pdo->prepare("INSERT INTO {$this->migrations_table} (schema_version) VALUES (?)");

View File

@ -5,6 +5,8 @@ class Debug {
const LOG_VERBOSE = 1;
const LOG_EXTENDED = 2;
const SEPARATOR = "<-{log-separator}->";
const ALL_LOG_LEVELS = [
Debug::LOG_DISABLED,
Debug::LOG_NORMAL,
@ -35,6 +37,7 @@ class Debug {
private static bool $enabled = false;
private static bool $quiet = false;
private static ?string $logfile = null;
private static bool $enable_html = false;
private static int $loglevel = self::LOG_NORMAL;
@ -82,6 +85,10 @@ class Debug {
}
}
public static function enable_html(bool $enable) : void {
self::$enable_html = $enable;
}
/**
* @param Debug::LOG_* $level log level
*/
@ -94,6 +101,13 @@ class Debug {
$ts = "$ts/" . posix_getpid();
}
$orig_message = $message;
if ($message === self::SEPARATOR) {
$message = self::$enable_html ? "<hr/>" :
"=================================================================================================================================";
}
if (self::$logfile) {
$fp = fopen(self::$logfile, 'a+');
@ -132,7 +146,15 @@ class Debug {
}
}
if (self::$enable_html) {
if ($orig_message === self::SEPARATOR) {
print "$message\n";
} else {
print "<span class='log-timestamp'>$ts</span> <span class='log-message'>$message</span>\n";
}
} else {
print "[$ts] $message\n";
}
return true;
}

View File

@ -2,7 +2,7 @@
class Digest
{
static function send_headlines_digests(): void {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$user_limit = 15; // amount of users to process (e.g. emails to send out)
$limit = 1000; // maximum amount of headlines to include
@ -17,7 +17,7 @@ class Digest
$pdo = Db::pdo();
$res = $pdo->query("SELECT id,email FROM ttrss_users
$res = $pdo->query("SELECT id, login, email FROM ttrss_users
WHERE email != '' AND (last_digest_sent IS NULL OR $interval_qpart)");
while ($line = $res->fetch()) {
@ -77,7 +77,7 @@ class Digest
}
}
$scope->close();
$span->end();
Debug::log("All done.");
}
@ -92,7 +92,8 @@ class Digest
$tpl->readTemplateFromFile("digest_template_html.txt");
$tpl_t->readTemplateFromFile("digest_template.txt");
$user_tz_string = get_pref(Prefs::USER_TIMEZONE, $user_id);
$user_tz_string = Prefs::get(Prefs::USER_TIMEZONE, $user_id);
$min_score = Prefs::get(Prefs::DIGEST_MIN_SCORE, $user_id);
if ($user_tz_string == 'Automatic')
$user_tz_string = 'GMT';
@ -136,10 +137,10 @@ class Digest
AND $interval_qpart
AND ttrss_user_entries.owner_uid = :user_id
AND unread = true
AND score >= 0
AND score >= :min_score
ORDER BY ttrss_feed_categories.title, ttrss_feeds.title, score DESC, date_updated DESC
LIMIT " . (int)$limit);
$sth->execute([':user_id' => $user_id]);
$sth->execute([':user_id' => $user_id, ':min_score' => $min_score]);
$headlines_count = 0;
$headlines = array();
@ -191,7 +192,7 @@ class Digest
$tpl_t->addBlock('article');
if ($headlines[$i]['feed_title'] != $headlines[$i + 1]['feed_title']) {
if (!isset($headlines[$i + 1]) || $headlines[$i]['feed_title'] != $headlines[$i + 1]['feed_title']) {
$tpl->addBlock('feed');
$tpl_t->addBlock('feed');
}

View File

@ -206,7 +206,7 @@ class DiskCache implements Cache_Adapter {
}
public function __construct(string $dir) {
foreach (PluginHost::getInstance()->get_plugins() as $n => $p) {
foreach (PluginHost::getInstance()->get_plugins() as $p) {
if (implements_interface($p, "Cache_Adapter")) {
/** @var Cache_Adapter $p */
@ -221,9 +221,11 @@ class DiskCache implements Cache_Adapter {
}
public function remove(string $filename): bool {
$scope = Tracer::start(__METHOD__, ['filename' => $filename]);
$span = Tracer::start(__METHOD__);
$span->setAttribute('file.name', $filename);
$rc = $this->adapter->remove($filename);
$scope->close();
$span->end();
return $rc;
}
@ -243,14 +245,16 @@ class DiskCache implements Cache_Adapter {
return $this->adapter->make_dir();
}
/** @param string|null $filename null means check that cache directory itself is writable */
public function is_writable(?string $filename = null): bool {
return $this->adapter->is_writable(basename($filename));
return $this->adapter->is_writable($filename ? basename($filename) : null);
}
public function exists(string $filename): bool {
$scope = Tracer::start(__METHOD__, ['filename' => $filename]);
$span = OpenTelemetry\API\Trace\Span::getCurrent();
$span->addEvent("DiskCache::exists: $filename");
$rc = $this->adapter->exists(basename($filename));
$scope->close();
return $rc;
}
@ -259,9 +263,11 @@ class DiskCache implements Cache_Adapter {
* @return int|false -1 if the file doesn't exist, false if an error occurred, size in bytes otherwise
*/
public function get_size(string $filename) {
$scope = Tracer::start(__METHOD__, ['filename' => $filename]);
$span = Tracer::start(__METHOD__);
$span->setAttribute('file.name', $filename);
$rc = $this->adapter->get_size(basename($filename));
$scope->close();
$span->end();
return $rc;
}
@ -272,9 +278,9 @@ class DiskCache implements Cache_Adapter {
* @return int|false Bytes written or false if an error occurred.
*/
public function put(string $filename, $data) {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$rc = $this->adapter->put(basename($filename), $data);
$scope->close();
$span->end();
return $rc;
}
@ -320,7 +326,8 @@ class DiskCache implements Cache_Adapter {
}
public function send(string $filename) {
$scope = Tracer::start(__METHOD__, ['filename' => $filename]);
$span = Tracer::start(__METHOD__);
$span->setAttribute('file.name', $filename);
$filename = basename($filename);
@ -328,8 +335,8 @@ class DiskCache implements Cache_Adapter {
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
echo "File not found.";
$scope->getSpan()->setTag('error', '404 not found');
$scope->close();
$span->setAttribute('error', '404 not found');
$span->end();
return false;
}
@ -339,8 +346,8 @@ class DiskCache implements Cache_Adapter {
if (($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '') == $gmt_modified || ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') == $file_mtime) {
header('HTTP/1.1 304 Not Modified');
$scope->getSpan()->setTag('error', '304 not modified');
$scope->close();
$span->setAttribute('error', '304 not modified');
$span->end();
return false;
}
@ -359,8 +366,8 @@ class DiskCache implements Cache_Adapter {
print "Stored file has disallowed content type ($mimetype)";
$scope->getSpan()->setTag('error', '400 disallowed content type');
$scope->close();
$span->setAttribute('error', '400 disallowed content type');
$span->end();
return false;
}
@ -382,11 +389,11 @@ class DiskCache implements Cache_Adapter {
header_remove("Pragma");
$scope->getSpan()->setTag('mimetype', $mimetype);
$span->setAttribute('mimetype', $mimetype);
$rc = $this->adapter->send($filename);
$scope->close();
$span->end();
return $rc;
}
@ -417,12 +424,13 @@ class DiskCache implements Cache_Adapter {
// plugins work on original source URLs used before caching
// NOTE: URLs should be already absolutized because this is called after sanitize()
static public function rewrite_urls(string $str): string {
$scope = Tracer::start(__METHOD__);
$span = OpenTelemetry\API\Trace\Span::getCurrent();
$span->addEvent("DiskCache::rewrite_urls");
$res = trim($str);
if (!$res) {
$scope->close();
$span->end();
return '';
}
@ -436,7 +444,7 @@ class DiskCache implements Cache_Adapter {
$need_saving = false;
foreach ($entries as $entry) {
$e_scope = Tracer::start('entry', ['tagName' => $entry->tagName]);
$span->addEvent("entry: " . $entry->tagName);
foreach (array('src', 'poster') as $attr) {
if ($entry->hasAttribute($attr)) {
@ -469,8 +477,6 @@ class DiskCache implements Cache_Adapter {
$entry->setAttribute("srcset", RSSUtils::encode_srcset($matches));
}
$e_scope->close();
}
if ($need_saving) {
@ -481,8 +487,6 @@ class DiskCache implements Cache_Adapter {
}
}
$scope->close();
return $res;
}
}

View File

View File

0
classes/feeditem/rss.php → classes/FeedItem_RSS.php Executable file → Normal file
View File

View File

@ -52,7 +52,6 @@ class FeedParser {
}
function init() : void {
$root = $this->doc->firstChild;
$xpath = new DOMXPath($this->doc);
$xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
$xpath->registerNamespace('atom03', 'http://purl.org/atom/ns#');

82
classes/feeds.php → classes/Feeds.php Executable file → Normal file
View File

@ -65,7 +65,8 @@ class Feeds extends Handler_Protected {
$disable_cache = false;
$scope = Tracer::start(__METHOD__, [], func_get_args());
$span = Tracer::start(__METHOD__);
$span->setAttribute('func.args', json_encode(func_get_args()));
$reply = [];
$rgba_cache = [];
@ -168,7 +169,7 @@ class Feeds extends Handler_Protected {
$reply['search_query'] = [$search, $search_language];
$reply['vfeed_group_enabled'] = $vfeed_group_enabled;
$p_scope = Tracer::start('plugin_menu_items');
$span->addEvent('plugin_menu_items');
$plugin_menu_items = "";
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2,
@ -202,15 +203,13 @@ class Feeds extends Handler_Protected {
},
$feed, $cat_view, $qfh_ret);
$p_scope->close();
$a_scope = Tracer::start('articles');
$span->addEvent('articles');
$headlines_count = 0;
if ($result instanceof PDOStatement) {
while ($line = $result->fetch(PDO::FETCH_ASSOC)) {
$aa_scope = Tracer::start('article', ['id' => $line['id']]);
$span->addEvent('article: ' . $line['id']);
++$headlines_count;
@ -370,7 +369,7 @@ class Feeds extends Handler_Protected {
//setting feed headline background color, needs to change text color based on dark/light
$fav_color = $line['favicon_avg_color'] ?? false;
$c_scope = Tracer::start('colors');
$span->addEvent("colors");
require_once "colors.php";
@ -386,7 +385,7 @@ class Feeds extends Handler_Protected {
$line['feed_bg_color'] = 'rgba(' . implode(",", $rgba_cache[$feed_id]) . ',0.3)';
}
$c_scope->close();
$span->addEvent("HOOK_RENDER_ARTICLE_CDM");
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_CDM,
function ($result, $plugin) use (&$line) {
@ -403,13 +402,9 @@ class Feeds extends Handler_Protected {
unset($line[$k]);
array_push($reply['content'], $line);
$aa_scope->close();
}
}
$a_scope->close();
if (!$headlines_count) {
if ($result instanceof PDOStatement) {
@ -469,7 +464,7 @@ class Feeds extends Handler_Protected {
}
}
$scope->close();
$span->end();
return array($topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply);
}
@ -708,6 +703,23 @@ class Feeds extends Handler_Protected {
body.css_loading * {
display : none;
}
.feed-xml {
color : green;
}
.log-timestamp {
color : gray;
}
.log-timestamp::before {
content: "["
}
.log-timestamp::after {
content: "]"
}
</style>
<script>
dojoConfig = {
@ -738,7 +750,7 @@ class Feeds extends Handler_Protected {
<h1>Feed Debugger: <?= "$feed_id: " . $this->_get_title($feed_id) ?></h1>
<div class="content">
<form method="post" action="" dojoType="dijit.form.Form">
<?= \Controls\hidden_tag("op", "feeds") ?>
<?= \Controls\hidden_tag("op", "Feeds") ?>
<?= \Controls\hidden_tag("method", "updatedebugger") ?>
<?= \Controls\hidden_tag("csrf_token", $csrf_token) ?>
<?= \Controls\hidden_tag("action", "do_update") ?>
@ -759,6 +771,10 @@ class Feeds extends Handler_Protected {
<label class="checkbox"><?= \Controls\checkbox_tag("force_rehash", isset($_REQUEST["force_rehash"])) ?> Force rehash</label>
</fieldset>
<fieldset class="narrow">
<label class="checkbox"><?= \Controls\checkbox_tag("dump_feed_xml", isset($_REQUEST["dump_feed_xml"])) ?> Dump feed XML</label>
</fieldset>
<?= \Controls\submit_tag("Continue") ?>
</form>
@ -767,7 +783,7 @@ class Feeds extends Handler_Protected {
<pre><?php
if ($do_update) {
RSSUtils::update_rss_feed($feed_id, true);
RSSUtils::update_rss_feed($feed_id, true, true);
}
?></pre>
@ -962,7 +978,9 @@ class Feeds extends Handler_Protected {
* @throws PDOException
*/
static function _get_counters($feed, bool $is_cat = false, bool $unread_only = false, ?int $owner_uid = null): int {
$scope = Tracer::start(__METHOD__, [], func_get_args());
$span = OpenTelemetry\API\Trace\Span::getCurrent();
$span->addEvent(__METHOD__ . ": $feed ($is_cat)");
$n_feed = (int) $feed;
$need_entries = false;
@ -986,14 +1004,14 @@ class Feeds extends Handler_Protected {
$handler = PluginHost::getInstance()->get_feed_handler($feed_id);
if (implements_interface($handler, 'IVirtualFeed')) {
/** @var IVirtualFeed $handler */
$scope->close();
//$span->end();
return $handler->get_unread($feed_id);
} else {
$scope->close();
//$span->end();
return 0;
}
} else if ($n_feed == Feeds::FEED_RECENTLY_READ) {
$scope->close();
//$span->end();
return 0;
// tags
} else if ($feed != "0" && $n_feed == 0) {
@ -1007,7 +1025,7 @@ class Feeds extends Handler_Protected {
$row = $sth->fetch();
// Handle 'SUM()' returning null if there are no results
$scope->close();
//$span->end();
return $row["count"] ?? 0;
} else if ($n_feed == Feeds::FEED_STARRED) {
@ -1041,7 +1059,7 @@ class Feeds extends Handler_Protected {
$label_id = Labels::feed_to_label_id($feed);
$scope->close();
//$span->end();
return self::_get_label_unread($label_id, $owner_uid);
}
@ -1061,7 +1079,7 @@ class Feeds extends Handler_Protected {
$sth->execute([$owner_uid]);
$row = $sth->fetch();
$scope->close();
//$span->end();
return $row["unread"];
} else {
@ -1074,7 +1092,7 @@ class Feeds extends Handler_Protected {
$sth->execute([$feed, $owner_uid]);
$row = $sth->fetch();
$scope->close();
//$span->end();
return $row["unread"];
}
}
@ -1114,8 +1132,6 @@ class Feeds extends Handler_Protected {
return ["code" => 8];
}
$pdo = Db::pdo();
$url = UrlHelper::validate($url);
if (!$url) return ["code" => 2];
@ -1127,7 +1143,7 @@ class Feeds extends Handler_Protected {
},
$url, $auth_login, $auth_pass);
$contents = UrlHelper::fetch($url, false, $auth_login, $auth_pass);
$contents = UrlHelper::fetch(['url' => $url, 'login' => $auth_login, 'pass' => $auth_pass]);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_SUBSCRIBE_FEED,
function ($result) use (&$contents) {
@ -1259,8 +1275,6 @@ class Feeds extends Handler_Protected {
*/
static function _find_by_title(string $title, bool $cat = false, int $owner_uid = 0) {
$res = false;
if ($cat) {
$res = ORM::for_table('ttrss_feed_categories')
->where('owner_uid', $owner_uid ? $owner_uid : $_SESSION['uid'])
@ -1472,7 +1486,8 @@ class Feeds extends Handler_Protected {
*/
static function _get_headlines($params): array {
$scope = Tracer::start(__METHOD__, [], func_get_args());
$span = Tracer::start(__METHOD__);
$span->setAttribute('func.args', json_encode(func_get_args()));
$pdo = Db::pdo();
@ -1716,7 +1731,6 @@ class Feeds extends Handler_Protected {
$vfeed_query_part = $override_vfeed;
}
$feed_title = "";
$feed_site_url = "";
$last_error = "";
$last_updated = "";
@ -1966,7 +1980,7 @@ class Feeds extends Handler_Protected {
$res = $pdo->query($query);
}
$scope->close();
$span->end();
return array($res, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words, $first_id, $vfeed_query_part != "", $query_error_override);
}
@ -2138,7 +2152,7 @@ class Feeds extends Handler_Protected {
}
static function _clear_access_keys(int $owner_uid): void {
$key = ORM::for_table('ttrss_access_keys')
ORM::for_table('ttrss_access_keys')
->where('owner_uid', $owner_uid)
->delete_many();
}
@ -2149,7 +2163,7 @@ class Feeds extends Handler_Protected {
* @see Handler_Public#generate_syndicated_feed()
*/
static function _update_access_key(string $feed_id, bool $is_cat, int $owner_uid): ?string {
$key = ORM::for_table('ttrss_access_keys')
ORM::for_table('ttrss_access_keys')
->where('owner_uid', $owner_uid)
->where('feed_id', $feed_id)
->where('is_cat', $is_cat)
@ -2193,8 +2207,6 @@ class Feeds extends Handler_Protected {
if (!$purge_interval) $purge_interval = self::_get_purge_interval($feed_id);
$pdo = Db::pdo();
$owner_uid = false;
$rows_deleted = 0;
$sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");

View File

@ -28,7 +28,7 @@ class Handler implements IHandler {
/**
* @param mixed $p
*/
protected static function _param_to_bool($p): bool {
public static function _param_to_bool($p): bool {
$p = clean($p);
return $p && ($p !== "f" && $p !== "false");
}

View File

@ -192,7 +192,7 @@ class Handler_Public extends Handler {
while ($line = $result->fetch()) {
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content_preview"]), 100, '...'));
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
$line["tags"] = Article::_get_tags($line["id"], $owner_uid);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
@ -834,5 +834,11 @@ class Handler_Public extends Handler {
exit;
}
// implicit Config::sanity_check() does the actual checking */
public function healthcheck() : void {
header("Content-Type: text/plain");
print "OK";
}
}
?>

4
classes/IAuthModule2.php Normal file
View File

@ -0,0 +1,4 @@
<?php
interface IAuthModule2 extends IAuthModule {
function change_password(int $owner_uid, string $old_password, string $new_password) : string;
}

0
classes/logger.php → classes/Logger.php Executable file → Normal file
View File

0
classes/logger/sql.php → classes/Logger_SQL.php Executable file → Normal file
View File

View File

@ -12,7 +12,7 @@ class Mailer {
$to_address = $params["to_address"];
$subject = $params["subject"];
$message = $params["message"];
$message_html = $params["message_html"] ?? "";
// $message_html = $params["message_html"] ?? "";
$from_name = $params["from_name"] ?? Config::get(Config::SMTP_FROM_NAME);
$from_address = $params["from_address"] ?? Config::get(Config::SMTP_FROM_ADDRESS);
$additional_headers = $params["headers"] ?? [];

View File

@ -612,8 +612,6 @@ class OPML extends Handler_Protected {
function opml_import(int $owner_uid, string $filename = "") {
if (!$owner_uid) return;
$doc = false;
if (!$filename) {
if ($_FILES['opml_file']['error'] != 0) {
print_error(T_sprintf("Upload failed with error code %d",
@ -644,8 +642,6 @@ class OPML extends Handler_Protected {
return false;
}
$loaded = false;
$doc = new DOMDocument();
if (version_compare(PHP_VERSION, '8.0.0', '<')) {

58
classes/pluginhost.php → classes/PluginHost.php Executable file → Normal file
View File

@ -339,12 +339,15 @@ class PluginHost {
*/
function chain_hooks_callback(string $hook, Closure $callback, &...$args): void {
$method = strtolower((string)$hook);
$scope = Tracer::start(__METHOD__, ['hook' => $hook]);
$span = OpenTelemetry\API\Trace\Span::getCurrent();
$span->addEvent("chain_hooks_callback: $hook");
foreach ($this->get_hooks((string)$hook) as $plugin) {
//Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
$p_scope = Tracer::start("$hook - " . get_class($plugin));
//$p_span = Tracer::start("$hook - " . get_class($plugin));
$span->addEvent("$hook - " . get_class($plugin));
try {
if ($callback($plugin->$method(...$args), $plugin))
@ -355,10 +358,10 @@ class PluginHost {
user_error($err, E_USER_WARNING);
}
$p_scope->close();
//$p_span->end();
}
$scope->close();
//$span->end();
}
/**
@ -424,6 +427,8 @@ class PluginHost {
* @param PluginHost::KIND_* $kind
*/
function load_all(int $kind, int $owner_uid = null, bool $skip_init = false): void {
$span = Tracer::start(__METHOD__);
$span->setAttribute('func.args', json_encode(func_get_args()));
$plugins = [...(glob("plugins/*") ?: []), ...(glob("plugins.local/*") ?: [])];
$plugins = array_filter($plugins, "is_dir");
@ -432,13 +437,16 @@ class PluginHost {
asort($plugins);
$this->load(join(",", $plugins), (int)$kind, $owner_uid, $skip_init);
$span->end();
}
/**
* @param PluginHost::KIND_* $kind
*/
function load(string $classlist, int $kind, int $owner_uid = null, bool $skip_init = false): void {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$span->setAttribute('func.args', json_encode(func_get_args()));
$plugins = explode(",", $classlist);
@ -448,16 +456,15 @@ class PluginHost {
$class = trim($class);
$class_file = strtolower(basename(clean($class)));
$p_scope = Tracer::start("loading $class_file");
$span->addEvent("$class_file: load");
// try system plugin directory first
$file = dirname(__DIR__) . "/plugins/$class_file/init.php";
$file = Config::get_self_dir() . "/plugins/$class_file/init.php";
if (!file_exists($file)) {
$file = dirname(__DIR__) . "/plugins.local/$class_file/init.php";
$file = Config::get_self_dir() . "/plugins.local/$class_file/init.php";
if (!file_exists($file)) {
$p_scope->close();
continue;
}
}
@ -476,8 +483,7 @@ class PluginHost {
$_SESSION["safe_mode"] = 1;
$p_scope->getSpan()->setTag('error', 'plugin is blacklisted');
$p_scope->close();
$span->setAttribute('error', 'plugin is blacklisted');
continue;
}
@ -489,8 +495,7 @@ class PluginHost {
} catch (Error $err) {
user_error($err, E_USER_WARNING);
$p_scope->getSpan()->setTag('error', $err);
$p_scope->close();
$span->setAttribute('error', $err);
continue;
}
@ -501,8 +506,7 @@ class PluginHost {
if ($plugin_api < self::API_VERSION) {
user_error("Plugin $class is not compatible with current API version (need: " . self::API_VERSION . ", got: $plugin_api)", E_USER_WARNING);
$p_scope->getSpan()->setTag('error', 'plugin is not compatible with API version');
$p_scope->close();
$span->setAttribute('error', 'plugin is not compatible with API version');
continue;
}
@ -511,7 +515,7 @@ class PluginHost {
_bind_textdomain_codeset($class, "UTF-8");
}
$i_scope = Tracer::start('init and register plugin');
$span->addEvent("$class_file: initialize");
try {
switch ($kind) {
@ -537,17 +541,12 @@ class PluginHost {
} catch (Error $err) {
user_error($err, E_USER_WARNING);
}
$i_scope->close();
}
}
$p_scope->close();
}
$this->load_data();
$scope->close();
$span->end();
}
function is_system(Plugin $plugin): bool {
@ -640,26 +639,28 @@ class PluginHost {
}
private function load_data(): void {
$scope = Tracer::start(__METHOD__);
$span = OpenTelemetry\API\Trace\Span::getCurrent();
$span->addEvent('load plugin data');
if ($this->owner_uid && !$this->data_loaded && get_schema_version() > 100) {
if ($this->owner_uid && !$this->data_loaded && Config::get_schema_version() > 100) {
$sth = $this->pdo->prepare("SELECT name, content FROM ttrss_plugin_storage
WHERE owner_uid = ?");
$sth->execute([$this->owner_uid]);
while ($line = $sth->fetch()) {
$span->addEvent($line["name"] . ': unserialize');
$this->storage[$line["name"]] = unserialize($line["content"]);
}
$this->data_loaded = true;
}
$scope->close();
}
private function save_data(string $plugin): void {
if ($this->owner_uid) {
$scope = Tracer::start(__METHOD__);
$span = OpenTelemetry\API\Trace\Span::getCurrent();
$span->addEvent(__METHOD__ . ": $plugin");
if (!$this->pdo_data)
$this->pdo_data = Db::instance()->pdo_connect();
@ -687,7 +688,6 @@ class PluginHost {
}
$this->pdo_data->commit();
$scope->close();
}
}
@ -889,7 +889,7 @@ class PluginHost {
}
/**
* handled by classes/pluginhandler.php, requires valid session
* handled by classes/PluginHandler.php, requires valid session
*
* @param array<int|string, mixed> $params
*/

39
classes/pref/feeds.php → classes/Pref_Feeds.php Executable file → Normal file
View File

@ -39,12 +39,7 @@ class Pref_Feeds extends Handler_Protected {
/**
* @return array<int, array<string, bool|int|string>>
*/
private function get_category_items(int $cat_id): array {
if (clean($_REQUEST['mode'] ?? 0) != 2)
$search = $_SESSION["prefs_feed_search"] ?? "";
else
$search = "";
private function get_category_items(int $cat_id, string $search): array {
// first one is set by API
$show_empty_cats = self::_param_to_bool($_REQUEST['force_show_empty'] ?? false) ||
@ -64,7 +59,7 @@ class Pref_Feeds extends Handler_Protected {
'id' => 'CAT:' . $feed_category->id,
'bare_id' => (int)$feed_category->id,
'name' => $feed_category->title,
'items' => $this->get_category_items($feed_category->id),
'items' => $this->get_category_items($feed_category->id, $search),
'checkbox' => false,
'type' => 'category',
'unread' => -1,
@ -121,7 +116,7 @@ class Pref_Feeds extends Handler_Protected {
if (clean($_REQUEST['mode'] ?? 0) != 2)
$search = $_SESSION["prefs_feed_search"] ?? "";
else
$search = "";
$search = $_REQUEST['search'] ?? '';
$root = array();
$root['id'] = 'root';
@ -226,7 +221,7 @@ class Pref_Feeds extends Handler_Protected {
'bare_id' => (int) $feed_category->id,
'auxcounter' => -1,
'name' => $feed_category->title,
'items' => $this->get_category_items($feed_category->id),
'items' => $this->get_category_items($feed_category->id, $search),
'checkbox' => false,
'type' => 'category',
'unread' => -1,
@ -620,7 +615,7 @@ class Pref_Feeds extends Handler_Protected {
?>
<?= \Controls\hidden_tag("ids", $feed_ids) ?>
<?= \Controls\hidden_tag("op", "pref-feeds") ?>
<?= \Controls\hidden_tag("op", "Pref_Feeds") ?>
<?= \Controls\hidden_tag("method", "batchEditSave") ?>
<div dojoType="dijit.layout.TabContainer" style="height : 450px">
@ -782,16 +777,6 @@ class Pref_Feeds extends Handler_Protected {
$qparams = [];
switch ($k) {
case "title":
$qpart = "title = ?";
$qparams = [$feed_title];
break;
case "feed_url":
$qpart = "feed_url = ?";
$qparams = [$this->pdo->quote($feed_url)];
break;
case "update_interval":
$qpart = "update_interval = ?";
$qparams = [$upd_intl];
@ -851,7 +836,7 @@ class Pref_Feeds extends Handler_Protected {
case "feed_language":
$qpart = "feed_language = ?";
$qparams = [$this->pdo->quote($feed_language)];
$qparams = [$feed_language];
break;
}
@ -969,7 +954,7 @@ class Pref_Feeds extends Handler_Protected {
</div>
<div style="padding : 0px" dojoType="dijit.layout.ContentPane" region="center">
<div dojoType="fox.PrefFeedStore" jsId="feedStore"
url="backend.php?op=pref-feeds&method=getfeedtree">
url="backend.php?op=Pref_Feeds&method=getfeedtree">
</div>
<div dojoType="lib.CheckBoxStoreModel" jsId="feedModel" store="feedStore"
@ -998,7 +983,7 @@ class Pref_Feeds extends Handler_Protected {
<label class='dijitButton'><?= __("Choose file...") ?>
<input style='display : none' id='opml_file' name='opml_file' type='file'>
</label>
<input type='hidden' name='op' value='pref-feeds'>
<input type='hidden' name='op' value='Pref_Feeds'>
<input type='hidden' name='csrf_token' value="<?= $_SESSION['csrf_token'] ?>">
<input type='hidden' name='method' value='importOpml'>
<button dojoType='dijit.form.Button' class='alt-primary' onclick="return Helpers.OPML.import()" type="submit">
@ -1090,6 +1075,9 @@ class Pref_Feeds extends Handler_Protected {
* @return array<string, mixed>
*/
private function feedlist_init_cat(int $cat_id): array {
$span = OpenTelemetry\API\Trace\Span::getCurrent();
$span->addEvent(__METHOD__ . ": $cat_id");
return [
'id' => 'CAT:' . $cat_id,
'items' => array(),
@ -1104,7 +1092,8 @@ class Pref_Feeds extends Handler_Protected {
* @return array<string, mixed>
*/
private function feedlist_init_feed(int $feed_id, ?string $title = null, bool $unread = false, string $error = '', string $updated = ''): array {
$scope = Tracer::start(__METHOD__, []);
$span = OpenTelemetry\API\Trace\Span::getCurrent();
$span->addEvent(__METHOD__ . ": $feed_id");
if (!$title)
$title = Feeds::_get_title($feed_id, false);
@ -1112,8 +1101,6 @@ class Pref_Feeds extends Handler_Protected {
if ($unread === false)
$unread = Feeds::_get_counters($feed_id, false, true);
$scope->close();
return [
'id' => 'FEED:' . $feed_id,
'name' => $title,

2
classes/pref/filters.php → classes/Pref_Filters.php Executable file → Normal file
View File

@ -696,7 +696,7 @@ class Pref_Filters extends Handler_Protected {
</div>
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
<div dojoType="fox.PrefFilterStore" jsId="filterStore"
url="backend.php?op=pref-filters&method=getfiltertree">
url="backend.php?op=Pref_Filters&method=getfiltertree">
</div>
<div dojoType="lib.CheckBoxStoreModel" jsId="filterModel" store="filterStore"
query="{id:'root'}" rootId="root" rootLabel="Filters"

View File

@ -199,7 +199,7 @@ class Pref_Labels extends Handler_Protected {
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
<div dojoType='dojo.data.ItemFileWriteStore' jsId='labelStore'
url='backend.php?op=pref-labels&method=getlabeltree'>
url='backend.php?op=Pref_Labels&method=getlabeltree'>
</div>
<div dojoType='lib.CheckBoxStoreModel' jsId='labelModel' store='labelStore'

View File

@ -74,6 +74,7 @@ class Pref_Prefs extends Handler_Protected {
Prefs::DIGEST_ENABLE,
Prefs::DIGEST_CATCHUP,
Prefs::DIGEST_PREFERRED_TIME,
Prefs::DIGEST_MIN_SCORE,
],
__('Advanced') => [
Prefs::BLACKLISTED_TAGS,
@ -127,6 +128,7 @@ class Pref_Prefs extends Handler_Protected {
Prefs::DEBUG_HEADLINE_IDS => array(__("Show article and feed IDs"), __("In the headlines buffer")),
Prefs::DISABLE_CONDITIONAL_COUNTERS => array(__("Disable conditional counter updates"), __("May increase server load")),
Prefs::CDM_ENABLE_GRID => array(__("Grid view"), __("On wider screens, if always expanded")),
Prefs::DIGEST_MIN_SCORE => array(__("Required score"), __("Include articles with this or above score")),
];
// hidden in the main prefs UI (use to hide things that have description set above)
@ -174,7 +176,8 @@ class Pref_Prefs extends Handler_Protected {
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
if (method_exists($authenticator, "change_password")) {
if (implements_interface($authenticator, "IAuthModule2")) {
/** @var IAuthModule2 $authenticator */
print format_notice($authenticator->change_password($_SESSION["uid"], $old_pw, $new_pw));
} else {
print "ERROR: ".format_error("Function not supported by authentication module.");
@ -285,7 +288,7 @@ class Pref_Prefs extends Handler_Protected {
?>
<form dojoType='dijit.form.Form'>
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
<?= \Controls\hidden_tag("method", "changePersonalData") ?>
<script type="dojo/method" event="onSubmit" args="evt">
@ -325,16 +328,14 @@ class Pref_Prefs extends Handler_Protected {
$authenticator = false;
}
$otp_enabled = UserHelper::is_otp_enabled($_SESSION["uid"]);
if ($authenticator && method_exists($authenticator, "change_password")) {
if ($authenticator && implements_interface($authenticator, "IAuthModule2")) {
?>
<div style='display : none' id='pwd_change_infobox'></div>
<form dojoType='dijit.form.Form'>
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
<?= \Controls\hidden_tag("method", "changepassword") ?>
<!-- TODO: return JSON the backend call -->
@ -426,7 +427,7 @@ class Pref_Prefs extends Handler_Protected {
?>
<form dojoType='dijit.form.Form'>
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
<?= \Controls\hidden_tag("method", "otpdisable") ?>
<!-- TODO: return JSON from the backend call -->
@ -473,7 +474,7 @@ class Pref_Prefs extends Handler_Protected {
<form dojoType='dijit.form.Form'>
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
<?= \Controls\hidden_tag("method", "otpenable") ?>
<fieldset>
@ -689,7 +690,7 @@ class Pref_Prefs extends Handler_Protected {
}
} else if (in_array($pref_name, [Prefs::FRESH_ARTICLE_MAX_AGE,
Prefs::PURGE_OLD_DAYS, Prefs::LONG_DATE_FORMAT, Prefs::SHORT_DATE_FORMAT])) {
Prefs::PURGE_OLD_DAYS, Prefs::LONG_DATE_FORMAT, Prefs::SHORT_DATE_FORMAT, Prefs::DIGEST_MIN_SCORE])) {
if ($pref_name == Prefs::PURGE_OLD_DAYS && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
$attributes = ["disabled" => true, "required" => true];
@ -746,7 +747,7 @@ class Pref_Prefs extends Handler_Protected {
private function index_prefs(): void {
?>
<form dojoType='dijit.form.Form' id='changeSettingsForm'>
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
<?= \Controls\hidden_tag("method", "saveconfig") ?>
<script type="dojo/method" event="onSubmit" args="evt, quit">
@ -836,7 +837,7 @@ class Pref_Prefs extends Handler_Protected {
?>
<form dojoType="dijit.form.Form" id="changePluginsForm">
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
<?= \Controls\hidden_tag("method", "setplugins") ?>
<div dojoType="dijit.layout.BorderContainer" gutters="false">
@ -936,7 +937,7 @@ class Pref_Prefs extends Handler_Protected {
<script type='dojo/method' event='onSelected' args='evt'>
if (this.domNode.querySelector('.loading'))
window.setTimeout(() => {
xhr.post("backend.php", {op: 'pref-prefs', method: 'index_auth'}, (reply) => {
xhr.post("backend.php", {op: 'Pref_Prefs', method: 'index_auth'}, (reply) => {
this.attr('content', reply);
});
}, 100);
@ -1061,7 +1062,7 @@ class Pref_Prefs extends Handler_Protected {
* @return array<int, array{'plugin': string, 'rv': array{'stdout': false|string, 'stderr': false|string, 'git_status': int, 'need_update': bool}|null}>
*/
static function _get_updated_plugins(): array {
$root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
$root_dir = Config::get_self_dir();
$plugin_dirs = array_filter(glob("$root_dir/plugins.local/*"), "is_dir");
$rv = [];
@ -1185,7 +1186,7 @@ class Pref_Prefs extends Handler_Protected {
$plugin_name = basename(clean($_REQUEST['plugin']));
$status = 0;
$plugin_dir = dirname(dirname(__DIR__)) . "/plugins.local/$plugin_name";
$plugin_dir = Config::get_self_dir() . "/plugins.local/$plugin_name";
if (is_dir($plugin_dir)) {
$status = $this->_recursive_rmdir($plugin_dir);
@ -1199,7 +1200,7 @@ class Pref_Prefs extends Handler_Protected {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
$plugin_name = basename(clean($_REQUEST['plugin']));
$all_plugins = $this->_get_available_plugins();
$plugin_dir = dirname(dirname(__DIR__)) . "/plugins.local";
$plugin_dir = Config::get_self_dir() . "/plugins.local";
$work_dir = "$plugin_dir/plugin-installer";
@ -1224,13 +1225,10 @@ class Pref_Prefs extends Handler_Protected {
$proc = proc_open("git clone " . escapeshellarg($plugin['clone_url']) . " " . $tmp_dir,
$descriptorspec, $pipes, sys_get_temp_dir());
$status = 0;
if (is_resource($proc)) {
$rv["stdout"] = stream_get_contents($pipes[1]);
$rv["stderr"] = stream_get_contents($pipes[2]);
$status = proc_close($proc);
$rv["git_status"] = $status;
$rv["git_status"] = proc_close($proc);
// yeah I know about mysterious RC = -1
if (file_exists("$tmp_dir/init.php")) {
@ -1306,7 +1304,7 @@ class Pref_Prefs extends Handler_Protected {
function checkForPluginUpdates(): void {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::CHECK_FOR_UPDATES) && Config::get(Config::CHECK_FOR_PLUGIN_UPDATES)) {
$plugin_name = $_REQUEST["name"] ?? "";
$root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
$root_dir = Config::get_self_dir();
$rv = empty($plugin_name) ? self::_get_updated_plugins() : [
["plugin" => $plugin_name, "rv" => self::_plugin_needs_update($root_dir, $plugin_name)],
@ -1324,8 +1322,7 @@ class Pref_Prefs extends Handler_Protected {
$plugins = array_filter($plugins, 'strlen');
}
# we're in classes/pref/
$root_dir = dirname(dirname(__DIR__));
$root_dir = Config::get_self_dir();
$rv = [];
@ -1548,7 +1545,7 @@ class Pref_Prefs extends Handler_Protected {
}
function deleteAppPasswords(): void {
$passwords = ORM::for_table('ttrss_app_passwords')
ORM::for_table('ttrss_app_passwords')
->where('owner_uid', $_SESSION['uid'])
->where_in('id', $_REQUEST['ids'] ?? [])
->delete_many();

View File

@ -194,12 +194,16 @@ class Pref_System extends Handler_Administrative {
}
</script>
<?= \Controls\hidden_tag("op", "pref-system") ?>
<?= \Controls\hidden_tag("op", "Pref_System") ?>
<?= \Controls\hidden_tag("method", "sendTestEmail") ?>
<?php
$user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]);
?>
<fieldset>
<label><?= __("To:") ?></label>
<?= \Controls\input_tag("mail_address", "", "text", ['required' => 1]) ?>
<?= \Controls\input_tag("mail_address",$user->email, "text", ['required' => 1]) ?>
<?= \Controls\submit_tag(__("Send test email")) ?>
<span style="display: none; margin-left : 10px" class="alert alert-error" id="mail-test-result">...</span>
</fieldset>
@ -210,7 +214,7 @@ class Pref_System extends Handler_Administrative {
<script type='dojo/method' event='onSelected' args='evt'>
if (this.domNode.querySelector('.loading'))
window.setTimeout(() => {
xhr.post("backend.php", {op: 'pref-system', method: 'getphpinfo'}, (reply) => {
xhr.post("backend.php", {op: 'Pref_System', method: 'getphpinfo'}, (reply) => {
this.attr('content', `<div class='phpinfo'>${reply}</div>`);
});
}, 200);

View File

@ -61,6 +61,7 @@ class Prefs {
const DISABLE_CONDITIONAL_COUNTERS = "DISABLE_CONDITIONAL_COUNTERS";
const WIDESCREEN_MODE = "WIDESCREEN_MODE";
const CDM_ENABLE_GRID = "CDM_ENABLE_GRID";
const DIGEST_MIN_SCORE = "DIGEST_MIN_SCORE";
private const _DEFAULTS = [
Prefs::PURGE_OLD_DAYS => [ 60, Config::T_INT ],
@ -122,6 +123,7 @@ class Prefs {
Prefs::DISABLE_CONDITIONAL_COUNTERS => [ false, Config::T_BOOL ],
Prefs::WIDESCREEN_MODE => [ false, Config::T_BOOL ],
Prefs::CDM_ENABLE_GRID => [ false, Config::T_BOOL ],
Prefs::DIGEST_MIN_SCORE => [ 0, Config::T_INT ],
];
const _PROFILE_BLACKLIST = [
@ -138,6 +140,7 @@ class Prefs {
//Prefs::SORT_HEADLINES_BY_FEED_DATE,
Prefs::SSL_CERT_SERIAL,
Prefs::DIGEST_PREFERRED_TIME,
Prefs::DIGEST_MIN_SCORE,
Prefs::_PREFS_MIGRATED
];
@ -247,7 +250,7 @@ class Prefs {
/**
* @return bool|int|null|string
*/
static function get(string $pref_name, int $owner_uid, ?int $profile_id) {
static function get(string $pref_name, int $owner_uid, ?int $profile_id = null) {
return self::get_instance()->_get($pref_name, $owner_uid, $profile_id);
}
@ -260,8 +263,6 @@ class Prefs {
list ($def_val, $type_hint) = self::_DEFAULTS[$pref_name];
$cached_value = $this->_get_cache($pref_name, $owner_uid, $profile_id);
if ($this->_is_cached($pref_name, $owner_uid, $profile_id)) {
$cached_value = $this->_get_cache($pref_name, $owner_uid, $profile_id);
return Config::cast_to($cached_value, $type_hint);
@ -343,7 +344,7 @@ class Prefs {
$value = Config::cast_to($value, $type_hint);
if ($value == $this->_get($pref_name, $owner_uid, $profile_id))
return false;
return true; // no need to actually set this to the same value, let's just say we did
$this->_set_cache($pref_name, $value, $owner_uid, $profile_id);

26
classes/rpc.php → classes/RPC.php Executable file → Normal file
View File

@ -106,7 +106,7 @@ class RPC extends Handler_Protected {
}
function getAllCounters(): void {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
@$seq = (int) $_REQUEST['seq'];
@ -134,7 +134,7 @@ class RPC extends Handler_Protected {
'seq' => $seq
];
$scope->close();
$span->end();
print json_encode($reply);
}
@ -176,6 +176,8 @@ class RPC extends Handler_Protected {
}
function sanityCheck(): void {
$span = Tracer::start(__METHOD__);
$_SESSION["hasSandbox"] = self::_param_to_bool($_REQUEST["hasSandbox"] ?? false);
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
@ -207,6 +209,8 @@ class RPC extends Handler_Protected {
} else {
print Errors::to_json($error, $error_params);
}
$span->end();
}
/*function completeLabels() {
@ -250,6 +254,7 @@ class RPC extends Handler_Protected {
}
static function updaterandomfeed_real(): void {
$span = Tracer::start(__METHOD__);
$default_interval = (int) Prefs::get_default(Prefs::DEFAULT_UPDATE_INTERVAL);
@ -340,6 +345,7 @@ class RPC extends Handler_Protected {
print json_encode(array("message" => "NOTHING_TO_UPDATE"));
}
$span->end();
}
function updaterandomfeed(): void {
@ -395,6 +401,8 @@ class RPC extends Handler_Protected {
}
function log(): void {
$span = Tracer::start(__METHOD__);
$msg = clean($_REQUEST['msg'] ?? "");
$file = basename(clean($_REQUEST['file'] ?? ""));
$line = (int) clean($_REQUEST['line'] ?? 0);
@ -406,9 +414,13 @@ class RPC extends Handler_Protected {
echo json_encode(array("message" => "HOST_ERROR_LOGGED"));
}
$span->end();
}
function checkforupdates(): void {
$span = Tracer::start(__METHOD__);
$rv = ["changeset" => [], "plugins" => []];
$version = Config::get_version(false);
@ -434,6 +446,8 @@ class RPC extends Handler_Protected {
$rv["plugins"] = Pref_Prefs::_get_updated_plugins();
}
$span->end();
print json_encode($rv);
}
@ -441,6 +455,8 @@ class RPC extends Handler_Protected {
* @return array<string, mixed>
*/
private function _make_init_params(): array {
$span = Tracer::start(__METHOD__);
$params = array();
foreach ([Prefs::ON_CATCHUP_SHOW_NEXT_FEED, Prefs::HIDE_READ_FEEDS,
@ -493,6 +509,8 @@ class RPC extends Handler_Protected {
$params["icon_blank"] = $this->image_to_base64("images/blank_icon.gif");
$params["labels"] = Labels::get_all($_SESSION["uid"]);
$span->end();
return $params;
}
@ -512,6 +530,8 @@ class RPC extends Handler_Protected {
* @return array<string, mixed>
*/
static function _make_runtime_info(): array {
$span = Tracer::start(__METHOD__);
$data = array();
$pdo = Db::pdo();
@ -577,6 +597,8 @@ class RPC extends Handler_Protected {
}
}
$span->end();
return $data;
}

81
classes/rssutils.php → classes/RSSUtils.php Executable file → Normal file
View File

@ -1,5 +1,6 @@
<?php
class RSSUtils {
/**
* @param array<string, mixed> $article
*/
@ -68,7 +69,7 @@ class RSSUtils {
* @param array<string, false|string> $options
*/
static function update_daemon_common(int $limit = 0, array $options = []): int {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
if (!$limit) $limit = Config::get(Config::DAEMON_FEED_LIMIT);
@ -285,7 +286,7 @@ class RSSUtils {
// Send feed digests by email if needed.
Digest::send_headlines_digests();
$scope->close();
$span->end();
return $nf;
}
@ -351,9 +352,12 @@ class RSSUtils {
}
}
static function update_rss_feed(int $feed, bool $no_cache = false) : bool {
static function update_rss_feed(int $feed, bool $no_cache = false, bool $html_output = false) : bool {
$scope = Tracer::start(__METHOD__, [], func_get_args());
$span = Tracer::start(__METHOD__);
$span->setAttribute('func.args', json_encode(func_get_args()));
Debug::enable_html($html_output);
Debug::log("start", Debug::LOG_VERBOSE);
$pdo = Db::pdo();
@ -388,19 +392,19 @@ class RSSUtils {
if ($user) {
if ($user->access_level == UserHelper::ACCESS_LEVEL_READONLY) {
Debug::log("error: denied update for $feed: permission denied by owner access level");
$scope->close();
$span->end();
return false;
}
} else {
// this would indicate database corruption of some kind
Debug::log("error: owner not found for feed: $feed");
$scope->close();
$span->end();
return false;
}
} else {
Debug::log("error: feeds table record not found for feed: $feed");
$scope->close();
$span->end();
return false;
}
@ -424,6 +428,7 @@ class RSSUtils {
$rss_hash = false;
$force_refetch = isset($_REQUEST["force_refetch"]);
$dump_feed_xml = isset($_REQUEST["dump_feed_xml"]);
$feed_data = "";
Debug::log("running HOOK_FETCH_FEED handlers...", Debug::LOG_VERBOSE);
@ -558,7 +563,7 @@ class RSSUtils {
$feed_obj->save();
}
$scope->close();
$span->end();
return $error_message == "";
}
@ -569,6 +574,14 @@ class RSSUtils {
$pff_owner_uid = $feed_obj->owner_uid;
$pff_feed_url = $feed_obj->feed_url;
if ($dump_feed_xml) {
Debug::log("feed data before hooks:", Debug::LOG_VERBOSE);
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
print("<code class='feed-xml'>" . htmlspecialchars($feed_data). "</code>\n");
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
}
$start_ts = microtime(true);
$pluginhost->chain_hooks_callback(PluginHost::HOOK_FEED_FETCHED,
function ($result, $plugin) use (&$feed_data, $start_ts) {
@ -583,6 +596,14 @@ class RSSUtils {
Debug::log("feed data has not been modified by a plugin.", Debug::LOG_VERBOSE);
}
if ($dump_feed_xml) {
Debug::log("feed data after hooks:", Debug::LOG_VERBOSE);
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
print("<code class='feed-xml'>" . htmlspecialchars($feed_data). "</code>\n");
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
}
$rss = new FeedParser($feed_data);
$rss->init();
@ -684,7 +705,7 @@ class RSSUtils {
]);
$feed_obj->save();
$scope->close();
$span->end();
return true; // no articles
}
@ -693,12 +714,11 @@ class RSSUtils {
$tstart = time();
foreach ($items as $item) {
$a_scope = Tracer::start('article');
$a_span = Tracer::start('article');
$pdo->beginTransaction();
Debug::log("=================================================================================================================================",
Debug::LOG_VERBOSE);
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
if (Debug::get_loglevel() >= 3) {
print_r($item);
@ -853,7 +873,7 @@ class RSSUtils {
$pdo->commit();
$entry_obj = ORM::for_table('ttrss_entries')
ORM::for_table('ttrss_entries')
->find_one($base_entry_id)
->set('date_updated', Db::NOW())
->save();
@ -1008,7 +1028,7 @@ class RSSUtils {
WHERE guid IN (?, ?, ?)");
$csth->execute([$entry_guid, $entry_guid_hashed, $entry_guid_hashed_compat]);
if (!$row = $csth->fetch()) {
if (!$csth->fetch()) {
Debug::log("base guid [$entry_guid or $entry_guid_hashed] not found, creating...", Debug::LOG_VERBOSE);
@ -1164,7 +1184,7 @@ class RSSUtils {
if (Config::get(Config::DB_TYPE) == "pgsql") {
$params[":ts_lang"] = $feed_language;
$params[":ts_content"] = mb_substr(strip_tags($entry_title . " " . $entry_content), 0, 900000);
$params[":ts_content"] = mb_substr(strip_tags($entry_title) . " " . \Soundasleep\Html2Text::convert($entry_content), 0, 900000);
}
$sth->execute($params);
@ -1287,11 +1307,10 @@ class RSSUtils {
Debug::log("article processed.", Debug::LOG_VERBOSE);
$pdo->commit();
$a_scope->close();
$a_span->end();
}
Debug::log("=================================================================================================================================",
Debug::LOG_VERBOSE);
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
Debug::log("purging feed...", Debug::LOG_VERBOSE);
@ -1329,12 +1348,12 @@ class RSSUtils {
unset($rss);
Debug::log("update failed.", Debug::LOG_VERBOSE);
$scope->close();
$span->end();
return false;
}
Debug::log("update done.", Debug::LOG_VERBOSE);
$scope->close();
$span->end();
return true;
}
@ -1499,7 +1518,7 @@ class RSSUtils {
* @return array<int, array<string, string>> An array of filter action arrays with keys "type" and "param"
*/
static function get_article_filters(array $filters, string $title, string $content, string $link, string $author, array $tags, array &$matched_rules = null, array &$matched_filters = null): array {
$scope = Tracer::start(__METHOD__);
$span = Tracer::start(__METHOD__);
$matches = array();
@ -1508,6 +1527,7 @@ class RSSUtils {
$inverse = $filter["inverse"] ?? false;
$filter_match = false;
$last_processed_rule = false;
$regexp_matches = [];
foreach ($filter["rules"] as $rule) {
$match = false;
@ -1521,32 +1541,32 @@ class RSSUtils {
switch ($rule["type"]) {
case "title":
$match = @preg_match("/$reg_exp/iu", $title);
$match = @preg_match("/$reg_exp/iu", $title, $regexp_matches);
break;
case "content":
// we don't need to deal with multiline regexps
$content = (string)preg_replace("/[\r\n\t]/", "", $content);
$match = @preg_match("/$reg_exp/iu", $content);
$match = @preg_match("/$reg_exp/iu", $content, $regexp_matches);
break;
case "both":
// we don't need to deal with multiline regexps
$content = (string)preg_replace("/[\r\n\t]/", "", $content);
$match = (@preg_match("/$reg_exp/iu", $title) || @preg_match("/$reg_exp/iu", $content));
$match = (@preg_match("/$reg_exp/iu", $title, $regexp_matches) || @preg_match("/$reg_exp/iu", $content, $regexp_matches));
break;
case "link":
$match = @preg_match("/$reg_exp/iu", $link);
$match = @preg_match("/$reg_exp/iu", $link, $regexp_matches);
break;
case "author":
$match = @preg_match("/$reg_exp/iu", $author);
$match = @preg_match("/$reg_exp/iu", $author, $regexp_matches);
break;
case "tag":
if (count($tags) == 0)
array_push($tags, ''); // allow matching if there are no tags
foreach ($tags as $tag) {
if (@preg_match("/$reg_exp/iu", $tag)) {
if (@preg_match("/$reg_exp/iu", $tag, $regexp_matches)) {
$match = true;
break;
}
@ -1572,6 +1592,8 @@ class RSSUtils {
if ($inverse) $filter_match = !$filter_match;
if ($filter_match) {
$last_processed_rule["regexp_matches"] = $regexp_matches;
if (is_array($matched_rules)) array_push($matched_rules, $last_processed_rule);
if (is_array($matched_filters)) array_push($matched_filters, $filter);
@ -1584,7 +1606,7 @@ class RSSUtils {
}
}
$scope->close();
$span->end();
return $matches;
}
@ -1724,7 +1746,6 @@ class RSSUtils {
/** migrates favicons from legacy storage in feed-icons/ to cache/feed-icons/using new naming (sans .ico suffix) */
static function migrate_feed_icons() : void {
$old_dir = Config::get(Config::ICONS_DIR);
$new_dir = Config::get(Config::CACHE_DIR) . '/feed-icons';
$dh = opendir($old_dir);
@ -1985,7 +2006,7 @@ class RSSUtils {
$favicon_urls = [];
if ($html = @UrlHelper::fetch($url)) {
if ($html = @UrlHelper::fetch(['url' => $url])) {
$doc = new DOMDocument();
if (@$doc->loadHTML($html)) {

View File

@ -63,7 +63,8 @@ class Sanitizer {
* @return false|string The HTML, or false if an error occurred.
*/
public static function sanitize(string $str, ?bool $force_remove_images = false, int $owner = null, string $site_url = null, array $highlight_words = null, int $article_id = null) {
$scope = Tracer::start(__METHOD__);
$span = OpenTelemetry\API\Trace\Span::getCurrent();
$span->addEvent("Sanitizer::sanitize");
if (!$owner && isset($_SESSION["uid"]))
$owner = $_SESSION["uid"];
@ -224,8 +225,6 @@ class Sanitizer {
$res = $doc->saveHTML();
$scope->close();
/* strip everything outside of <body>...</body> */
$res_frag = array();

216
classes/Tracer.php Normal file
View File

@ -0,0 +1,216 @@
<?php
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\API\Trace\SpanContextInterface;
use OpenTelemetry\API\Trace\SpanInterface;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\TraceFlags;
use OpenTelemetry\API\Trace\TraceStateInterface;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\Context\ContextKey;
use OpenTelemetry\Context\ContextKeyInterface;
use OpenTelemetry\Context\ImplicitContextKeyedInterface;
use OpenTelemetry\Context\ScopeInterface;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SemConv\ResourceAttributes;
class DummyContextInterface implements ContextInterface {
/** @var DummyContextInterface */
private static $instance;
public function __construct() {
self::$instance = $this;
}
/** @phpstan-ignore-next-line */
public static function createKey(string $key): ContextKeyInterface { return new ContextKey(); }
public static function getCurrent(): ContextInterface { return self::$instance; }
public function activate(): ScopeInterface { return new DummyScopeInterface(); }
public function with(ContextKeyInterface $key, $value): ContextInterface { return $this; }
public function withContextValue(ImplicitContextKeyedInterface $value): ContextInterface { return $this; }
public function get(ContextKeyInterface $key) { return new ContextKey(); }
}
class DummySpanContextInterface implements SpanContextInterface {
/** @var DummySpanContextInterface $instance */
private static $instance;
public function __construct() {
self::$instance = $this;
}
public static function createFromRemoteParent(string $traceId, string $spanId, int $traceFlags = TraceFlags::DEFAULT, ?TraceStateInterface $traceState = null): SpanContextInterface { return self::$instance; }
public static function getInvalid(): SpanContextInterface { return self::$instance; }
public static function create(string $traceId, string $spanId, int $traceFlags = TraceFlags::DEFAULT, ?TraceStateInterface $traceState = null): SpanContextInterface { return self::$instance; }
public function getTraceId(): string { return ""; }
public function getTraceIdBinary(): string { return ""; }
public function getSpanId(): string { return ""; }
public function getSpanIdBinary(): string { return ""; }
public function getTraceFlags(): int { return 0; }
public function getTraceState(): ?TraceStateInterface { return null; }
public function isValid(): bool { return false; }
public function isRemote(): bool { return false; }
public function isSampled(): bool { return false; }
}
class DummyScopeInterface implements ScopeInterface {
public function detach(): int { return 0; }
}
class DummySpanInterface implements SpanInterface {
/** @var DummySpanInterface $instance */
private static $instance;
public function __construct() {
self::$instance = $this;
}
public static function fromContext(ContextInterface $context): SpanInterface { return self::$instance; }
public static function getCurrent(): SpanInterface { return self::$instance; }
public static function getInvalid(): SpanInterface { return self::$instance; }
public static function wrap(SpanContextInterface $spanContext): SpanInterface { return self::$instance; }
public function getContext(): SpanContextInterface { return new DummySpanContextInterface(); }
public function isRecording(): bool { return false; }
/** @phpstan-ignore-next-line */
public function setAttribute(string $key, $value): SpanInterface { return self::$instance; }
/** @phpstan-ignore-next-line */
public function setAttributes(iterable $attributes): SpanInterface { return self::$instance; }
/** @phpstan-ignore-next-line */
public function addEvent(string $name, iterable $attributes = [], ?int $timestamp = null): SpanInterface { return $this; }
/** @phpstan-ignore-next-line */
public function recordException(Throwable $exception, iterable $attributes = []): SpanInterface { return $this; }
public function updateName(string $name): SpanInterface { return $this; }
public function setStatus(string $code, ?string $description = null): SpanInterface { return $this; }
public function end(?int $endEpochNanos = null): void { }
public function activate(): ScopeInterface { return new DummyScopeInterface(); }
public function storeInContext(ContextInterface $context): ContextInterface { return new DummyContextInterface(); }
}
class Tracer {
/** @var Tracer $instance */
private static $instance = null;
/** @var OpenTelemetry\SDK\Trace\TracerProviderInterface $tracerProvider */
private $tracerProvider = null;
/** @var OpenTelemetry\API\Trace\TracerInterface $tracer */
private $tracer = null;
public function __construct() {
$OPENTELEMETRY_ENDPOINT = Config::get(Config::OPENTELEMETRY_ENDPOINT);
if ($OPENTELEMETRY_ENDPOINT) {
$transport = (new OtlpHttpTransportFactory())->create($OPENTELEMETRY_ENDPOINT, 'application/x-protobuf');
$exporter = new SpanExporter($transport);
$resource = ResourceInfoFactory::emptyResource()->merge(
ResourceInfo::create(Attributes::create(
[ResourceAttributes::SERVICE_NAME => Config::get(Config::OPENTELEMETRY_SERVICE)]
), ResourceAttributes::SCHEMA_URL),
);
$this->tracerProvider = TracerProvider::builder()
->addSpanProcessor(new SimpleSpanProcessor($exporter))
->setResource($resource)
->setSampler(new ParentBased(new AlwaysOnSampler()))
->build();
$this->tracer = $this->tracerProvider->getTracer('io.opentelemetry.contrib.php');
$context = TraceContextPropagator::getInstance()->extract(getallheaders());
$span = $this->tracer->spanBuilder($_SESSION['name'] ?? 'not logged in')
->setParent($context)
->setSpanKind(SpanKind::KIND_SERVER)
->setAttribute('php.request', json_encode($_REQUEST))
->setAttribute('php.server', json_encode($_SERVER))
->setAttribute('php.session', json_encode($_SESSION ?? []))
->startSpan();
$scope = $span->activate();
register_shutdown_function(function() use ($span, $scope) {
$span->end();
$scope->detach();
$this->tracerProvider->shutdown();
});
}
}
/**
* @param string $name
* @return OpenTelemetry\API\Trace\SpanInterface
*/
private function _start(string $name) {
if ($this->tracer != null) {
$span = $this->tracer
->spanBuilder($name)
->setSpanKind(SpanKind::KIND_SERVER)
->startSpan();
$span->activate();
} else {
$span = new DummySpanInterface();
}
return $span;
}
/**
* @param string $name
* @return OpenTelemetry\API\Trace\SpanInterface
*/
public static function start(string $name) {
return self::get_instance()->_start($name);
}
public static function get_instance() : Tracer {
if (self::$instance == null)
self::$instance = new self();
return self::$instance;
}
}

505
classes/UrlHelper.php Normal file
View File

@ -0,0 +1,505 @@
<?php
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
class UrlHelper {
const EXTRA_HREF_SCHEMES = [
"magnet",
"mailto",
"tel"
];
const EXTRA_SCHEMES_BY_CONTENT_TYPE = [
"application/x-bittorrent" => [ "magnet" ],
];
static string $fetch_last_error;
static int $fetch_last_error_code;
static string $fetch_last_error_content;
static string $fetch_last_content_type;
static string $fetch_last_modified;
static string $fetch_effective_url;
static string $fetch_effective_ip_addr;
public static ?GuzzleHttp\ClientInterface $client = null;
private static function get_client(): GuzzleHttp\ClientInterface {
if (self::$client == null) {
self::$client = new GuzzleHttp\Client([
GuzzleHttp\RequestOptions::COOKIES => false,
GuzzleHttp\RequestOptions::PROXY => Config::get(Config::HTTP_PROXY) ?: null,
]);
}
return self::$client;
}
/**
* @param array<string, string|int> $parts
*/
static function build_url(array $parts): string {
$tmp = $parts['scheme'] . "://" . $parts['host'];
if (isset($parts['path'])) $tmp .= $parts['path'];
if (isset($parts['query'])) $tmp .= '?' . $parts['query'];
if (isset($parts['fragment'])) $tmp .= '#' . $parts['fragment'];
return $tmp;
}
/**
* Converts a (possibly) relative URL to a absolute one, using provided base URL.
* Provides some exceptions for additional schemes like data: if called with owning element/attribute.
*
* @param string $base_url Base URL (i.e. from where the document is)
* @param string $rel_url Possibly relative URL in the document
* @param string $owner_element Owner element tag name (i.e. "a") (optional)
* @param string $owner_attribute Owner attribute (i.e. "href") (optional)
* @param string $content_type URL content type as specified by enclosures, etc.
*
* @return false|string Absolute URL or false on failure (either during URL parsing or validation)
*/
public static function rewrite_relative($base_url,
$rel_url,
string $owner_element = "",
string $owner_attribute = "",
string $content_type = "") {
$rel_parts = parse_url($rel_url);
if (!$rel_url) return $base_url;
/**
* If parse_url failed to parse $rel_url return false to match the current "invalid thing" behavior
* of UrlHelper::validate().
*
* TODO: There are many places where a string return value is assumed. We should either update those
* to account for the possibility of failure, or look into updating this function's return values.
*/
if ($rel_parts === false) {
return false;
}
if (!empty($rel_parts['host']) && !empty($rel_parts['scheme'])) {
return self::validate($rel_url);
// protocol-relative URL (rare but they exist)
} else if (strpos($rel_url, "//") === 0) {
return self::validate("https:" . $rel_url);
// allow some extra schemes for A href
} else if (in_array($rel_parts["scheme"] ?? "", self::EXTRA_HREF_SCHEMES, true) &&
$owner_element == "a" &&
$owner_attribute == "href") {
return $rel_url;
// allow some extra schemes for links with feed-specified content type i.e. enclosures
} else if ($content_type &&
isset(self::EXTRA_SCHEMES_BY_CONTENT_TYPE[$content_type]) &&
in_array($rel_parts["scheme"], self::EXTRA_SCHEMES_BY_CONTENT_TYPE[$content_type])) {
return $rel_url;
// allow limited subset of inline base64-encoded images for IMG elements
} else if (($rel_parts["scheme"] ?? "") == "data" &&
preg_match('%^image/(webp|gif|jpg|png|svg);base64,%', $rel_parts["path"]) &&
$owner_element == "img" &&
$owner_attribute == "src") {
return $rel_url;
} else {
$base_parts = parse_url($base_url);
$rel_parts['host'] = $base_parts['host'] ?? "";
$rel_parts['scheme'] = $base_parts['scheme'] ?? "";
if ($rel_parts['path'] ?? "") {
// we append dirname() of base path to relative URL path as per RFC 3986 section 5.2.2
$base_path = with_trailing_slash(dirname($base_parts['path'] ?? ""));
// 1. absolute relative path (/test.html) = no-op, proceed as is
// 2. dotslash relative URI (./test.html) - strip "./", append base path
if (strpos($rel_parts['path'], './') === 0) {
$rel_parts['path'] = $base_path . substr($rel_parts['path'], 2);
// 3. anything else relative (test.html) - append dirname() of base path
} else if (strpos($rel_parts['path'], '/') !== 0) {
$rel_parts['path'] = $base_path . $rel_parts['path'];
}
//$rel_parts['path'] = str_replace("/./", "/", $rel_parts['path']);
//$rel_parts['path'] = str_replace("//", "/", $rel_parts['path']);
}
return self::validate(self::build_url($rel_parts));
}
}
/** extended filtering involves validation for safe ports and loopback
* @return false|string false if something went wrong, otherwise the URL string
*/
static function validate(string $url, bool $extended_filtering = false) {
$url = clean($url);
# fix protocol-relative URLs
if (strpos($url, "//") === 0)
$url = "https:" . $url;
$tokens = parse_url($url);
// this isn't really necessary because filter_var(... FILTER_VALIDATE_URL) requires host and scheme
// as per https://php.watch/versions/7.3/filter-var-flag-deprecation but it might save time
if (empty($tokens['host']))
return false;
if (!in_array(strtolower($tokens['scheme']), ['http', 'https']))
return false;
//convert IDNA hostname to punycode if possible
if (function_exists("idn_to_ascii")) {
if (mb_detect_encoding($tokens['host']) != 'ASCII') {
if (defined('IDNA_NONTRANSITIONAL_TO_ASCII') && defined('INTL_IDNA_VARIANT_UTS46')) {
$tokens['host'] = idn_to_ascii($tokens['host'], IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
} else {
$tokens['host'] = idn_to_ascii($tokens['host']);
}
// if `idn_to_ascii` failed
if ($tokens['host'] === false) {
return false;
}
}
}
// separate set of tokens with urlencoded 'path' because filter_var() rightfully fails on non-latin characters
// (used for validation only, we actually request the original URL, in case of urlencode breaking it)
$tokens_filter_var = $tokens;
if ($tokens['path'] ?? false) {
$tokens_filter_var['path'] = implode("/",
array_map("rawurlencode",
array_map("rawurldecode",
explode("/", $tokens['path']))));
}
$url = self::build_url($tokens);
$url_filter_var = self::build_url($tokens_filter_var);
if (filter_var($url_filter_var, FILTER_VALIDATE_URL) === false)
return false;
if ($extended_filtering) {
if (!in_array($tokens['port'] ?? '', [80, 443, '']))
return false;
if (strtolower($tokens['host']) == 'localhost' || $tokens['host'] == '::1' || strpos($tokens['host'], '127.') === 0)
return false;
}
return $url;
}
/**
* @return false|string
*/
static function resolve_redirects(string $url, int $timeout) {
$span = Tracer::start(__METHOD__);
$span->setAttribute('func.args', json_encode(func_get_args()));
$client = self::get_client();
try {
$response = $client->request('HEAD', $url, [
GuzzleHttp\RequestOptions::CONNECT_TIMEOUT => $timeout ?: Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT),
GuzzleHttp\RequestOptions::TIMEOUT => $timeout ?: Config::get(Config::FILE_FETCH_TIMEOUT),
GuzzleHttp\RequestOptions::ALLOW_REDIRECTS => ['max' => 10, 'track_redirects' => true],
GuzzleHttp\RequestOptions::HTTP_ERRORS => false,
GuzzleHttp\RequestOptions::HEADERS => [
'User-Agent' => Config::get_user_agent(),
'Connection' => 'close',
],
]);
} catch (Exception $ex) {
$span->setAttribute('error', (string) $ex);
$span->end();
return false;
}
// If a history header value doesn't exist there was no redirection and the original URL is fine.
$history_header = $response->getHeader(GuzzleHttp\RedirectMiddleware::HISTORY_HEADER);
$span->end();
return ($history_header ? end($history_header) : $url);
}
/**
* @param array<string, bool|int|string>|string $options
* @return false|string false if something went wrong, otherwise string contents
*/
// TODO: max_size currently only works for CURL transfers
// TODO: multiple-argument way is deprecated, first parameter is a hash now
public static function fetch($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false, 8: $encoding = false,
9: $auth_type = "basic" */) {
$span = Tracer::start(__METHOD__);
$span->setAttribute('func.args', json_encode(func_get_args()));
self::$fetch_last_error = "";
self::$fetch_last_error_code = -1;
self::$fetch_last_error_content = "";
self::$fetch_last_content_type = "";
self::$fetch_last_modified = "";
self::$fetch_effective_url = "";
self::$fetch_effective_ip_addr = "";
if (!is_array($options)) {
// falling back on compatibility shim
$option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent", "encoding", "auth_type" ];
$tmp = [];
for ($i = 0; $i < func_num_args(); $i++) {
$tmp[$option_names[$i]] = func_get_arg($i);
}
$options = $tmp;
/*$options = array(
"url" => func_get_arg(0),
"type" => @func_get_arg(1),
"login" => @func_get_arg(2),
"pass" => @func_get_arg(3),
"post_query" => @func_get_arg(4),
"timeout" => @func_get_arg(5),
"timestamp" => @func_get_arg(6),
"useragent" => @func_get_arg(7),
"encoding" => @func_get_arg(8),
"auth_type" => @func_get_arg(9),
); */
}
$url = $options["url"];
$type = isset($options["type"]) ? $options["type"] : false;
$login = isset($options["login"]) ? $options["login"] : false;
$pass = isset($options["pass"]) ? $options["pass"] : false;
$auth_type = isset($options["auth_type"]) ? $options["auth_type"] : "basic";
$post_query = isset($options["post_query"]) ? $options["post_query"] : false;
$timeout = isset($options["timeout"]) ? $options["timeout"] : false;
$last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
$useragent = isset($options["useragent"]) ? $options["useragent"] : false;
$followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
$max_size = isset($options["max_size"]) ? $options["max_size"] : Config::get(Config::MAX_DOWNLOAD_FILE_SIZE); // in bytes
$http_accept = isset($options["http_accept"]) ? $options["http_accept"] : false;
$http_referrer = isset($options["http_referrer"]) ? $options["http_referrer"] : false;
$encoding = isset($options["encoding"]) ? $options["encoding"] : false;
$url = ltrim($url, ' ');
$url = str_replace(' ', '%20', $url);
Debug::log("[UrlHelper] fetching: $url", Debug::LOG_EXTENDED);
$url = self::validate($url, true);
if (!$url) {
self::$fetch_last_error = 'Requested URL failed extended validation.';
$span->setAttribute('error', self::$fetch_last_error);
$span->end();
return false;
}
$url_host = parse_url($url, PHP_URL_HOST);
$ip_addr = gethostbyname($url_host);
if (!$ip_addr || strpos($ip_addr, '127.') === 0) {
self::$fetch_last_error = "URL hostname failed to resolve or resolved to a loopback address ($ip_addr)";
$span->setAttribute('error', self::$fetch_last_error);
$span->end();
return false;
}
$req_options = [
GuzzleHttp\RequestOptions::CONNECT_TIMEOUT => $timeout ?: Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT),
GuzzleHttp\RequestOptions::TIMEOUT => $timeout ?: Config::get(Config::FILE_FETCH_TIMEOUT),
GuzzleHttp\RequestOptions::HEADERS => [
'User-Agent' => $useragent ?: Config::get_user_agent(),
],
'curl' => [],
];
if ($followlocation) {
$req_options[GuzzleHttp\RequestOptions::ALLOW_REDIRECTS] = [
'max' => 20,
'track_redirects' => true,
'on_redirect' => function(RequestInterface $request, ResponseInterface $response, UriInterface $uri) {
if (!self::validate($uri, true)) {
self::$fetch_effective_url = (string) $uri;
throw new GuzzleHttp\Exception\RequestException('URL received during redirection failed extended validation.',
$request, $response);
}
},
];
} else {
$req_options[GuzzleHttp\RequestOptions::ALLOW_REDIRECTS] = false;
}
if ($last_modified && !$post_query)
$req_options[GuzzleHttp\RequestOptions::HEADERS]['If-Modified-Since'] = $last_modified;
if ($http_accept)
$req_options[GuzzleHttp\RequestOptions::HEADERS]['Accept'] = $http_accept;
if ($encoding)
$req_options[GuzzleHttp\RequestOptions::HEADERS]['Accept-Encoding'] = $encoding;
if ($http_referrer)
$req_options[GuzzleHttp\RequestOptions::HEADERS]['Referer'] = $http_referrer;
if ($login && $pass && in_array($auth_type, ['basic', 'digest', 'ntlm'])) {
// Let Guzzle handle the details for auth types it supports
$req_options[GuzzleHttp\RequestOptions::AUTH] = [$login, $pass, $auth_type];
} elseif ($auth_type === 'any') {
// https://docs.guzzlephp.org/en/stable/faq.html#how-can-i-add-custom-curl-options
$req_options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_ANY;
if ($login && $pass)
$req_options['curl'][\CURLOPT_USERPWD] = "$login:$pass";
}
if ($post_query)
$req_options[GuzzleHttp\RequestOptions::FORM_PARAMS] = $post_query;
if ($max_size) {
$req_options[GuzzleHttp\RequestOptions::PROGRESS] = function($download_size, $downloaded, $upload_size, $uploaded) use(&$max_size, $url) {
//Debug::log("[curl progressfunction] $downloaded $max_size", Debug::$LOG_EXTENDED);
if ($downloaded > $max_size) {
Debug::log("[UrlHelper] fetch error: max size of $max_size bytes exceeded when downloading $url . Aborting.", Debug::LOG_VERBOSE);
throw new \LengthException("Download exceeded size limit");
}
};
# Alternative/supplement to `progress` checking
$req_options[GuzzleHttp\RequestOptions::ON_HEADERS] = function(ResponseInterface $response) use(&$max_size, $url) {
$content_length = $response->getHeaderLine('Content-Length');
if ($content_length > $max_size) {
Debug::log("[UrlHelper] fetch error: server indicated (via 'Content-Length: {$content_length}') max size of $max_size bytes " .
"would be exceeded when downloading $url . Aborting.", Debug::LOG_VERBOSE);
throw new \LengthException("Server sent 'Content-Length' exceeding download limit");
}
};
}
$client = self::get_client();
try {
$response = $client->request($post_query ? 'POST' : 'GET', $url, $req_options);
} catch (\LengthException $ex) {
// Either 'Content-Length' indicated the download limit would be exceeded, or the transfer actually exceeded the download limit.
self::$fetch_last_error = $ex->getMessage();
$span->setAttribute('error', self::$fetch_last_error);
$span->end();
return false;
} catch (GuzzleHttp\Exception\GuzzleException $ex) {
self::$fetch_last_error = $ex->getMessage();
if ($ex instanceof GuzzleHttp\Exception\RequestException) {
if ($ex instanceof GuzzleHttp\Exception\BadResponseException) {
// 4xx or 5xx
self::$fetch_last_error_code = $ex->getResponse()->getStatusCode();
// If credentials were provided and we got a 403 back, retry once with auth type 'any'
// to attempt compatibility with unusual configurations.
if ($login && $pass && self::$fetch_last_error_code === 403 && $auth_type !== 'any') {
$options['auth_type'] = 'any';
$span->end();
return self::fetch($options);
}
self::$fetch_last_content_type = $ex->getResponse()->getHeaderLine('content-type');
if ($type && strpos(self::$fetch_last_content_type, "$type") === false)
self::$fetch_last_error_content = (string) $ex->getResponse()->getBody();
} elseif (array_key_exists('errno', $ex->getHandlerContext())) {
$errno = (int) $ex->getHandlerContext()['errno'];
// By default, all supported encoding types are sent via `Accept-Encoding` and decoding of
// responses with `Content-Encoding` is automatically attempted. If this fails, we do a
// single retry with `Accept-Encoding: none` to try and force an unencoded response.
if (($errno === \CURLE_WRITE_ERROR || $errno === \CURLE_BAD_CONTENT_ENCODING) &&
$ex->getRequest()->getHeaderLine('accept-encoding') !== 'none') {
$options['encoding'] = 'none';
$span->end();
return self::fetch($options);
}
}
}
$span->setAttribute('error', self::$fetch_last_error);
$span->end();
return false;
}
// Keep setting expected 'fetch_last_error_code' and 'fetch_last_error' values
self::$fetch_last_error_code = $response->getStatusCode();
self::$fetch_last_error = "HTTP/{$response->getProtocolVersion()} {$response->getStatusCode()} {$response->getReasonPhrase()}";
self::$fetch_last_modified = $response->getHeaderLine('last-modified');
self::$fetch_last_content_type = $response->getHeaderLine('content-type');
// If a history header value doesn't exist there was no redirection and the original URL is fine.
$history_header = $response->getHeader(GuzzleHttp\RedirectMiddleware::HISTORY_HEADER);
self::$fetch_effective_url = $history_header ? end($history_header) : $url;
// This shouldn't be necessary given the checks that occur during potential redirects, but we'll do it anyway.
if (!self::validate(self::$fetch_effective_url, true)) {
self::$fetch_last_error = "URL received after redirection failed extended validation.";
$span->setAttribute('error', self::$fetch_last_error);
$span->end();
return false;
}
self::$fetch_effective_ip_addr = gethostbyname(parse_url(self::$fetch_effective_url, PHP_URL_HOST));
if (!self::$fetch_effective_ip_addr || strpos(self::$fetch_effective_ip_addr, '127.') === 0) {
self::$fetch_last_error = 'URL hostname received after redirection failed to resolve or resolved to a loopback address (' .
self::$fetch_effective_ip_addr . ')';
$span->setAttribute('error', self::$fetch_last_error);
$span->end();
return false;
}
$body = (string) $response->getBody();
if (!$body) {
self::$fetch_last_error = 'Successful response, but no content was received.';
$span->setAttribute('error', self::$fetch_last_error);
$span->end();
return false;
}
$span->end();
return $body;
}
/**
* @return false|string false if the provided URL didn't match expected patterns, otherwise the video ID string
*/
public static function url_to_youtube_vid(string $url) {
$url = str_replace("youtube.com", "youtube-nocookie.com", $url);
$regexps = [
"/\/\/www\.youtube-nocookie\.com\/v\/([\w-]+)/",
"/\/\/www\.youtube-nocookie\.com\/embed\/([\w-]+)/",
"/\/\/www\.youtube-nocookie\.com\/watch?v=([\w-]+)/",
"/\/\/youtu.be\/([\w-]+)/",
];
foreach ($regexps as $re) {
$matches = [];
if (preg_match($re, $url, $matches)) {
return $matches[1];
}
}
return false;
}
}

View File

@ -250,7 +250,6 @@ class UserHelper {
static function reset_password(int $uid, bool $format_output = false, string $new_password = ""): void {
$user = ORM::for_table('ttrss_users')->find_one($uid);
$message = "";
if ($user) {

View File

@ -1,69 +0,0 @@
<?php
use OpenTracing\GlobalTracer;
use OpenTracing\Scope;
class Tracer {
/** @var Tracer $instance */
private static $instance;
public function __construct() {
$jaeger_host = Config::get(Config::JAEGER_REPORTING_HOST);
if ($jaeger_host) {
$config = new \Jaeger\Config(
[
'sampler' => [
'type' => \Jaeger\SAMPLER_TYPE_CONST,
'param' => true,
],
'logging' => true,
"local_agent" => [
"reporting_host" => $jaeger_host,
"reporting_port" => 6832
],
'dispatch_mode' => \Jaeger\Config::JAEGER_OVER_BINARY_UDP,
],
Config::get(Config::JAEGER_SERVICE_NAME)
);
$config->initializeTracer();
register_shutdown_function(function() {
$tracer = GlobalTracer::get();
$tracer->flush();
});
}
}
/**
* @param string $name
* @param array<string>|array<string, array<string, mixed>> $tags
* @param array<string> $args
* @return Scope
*/
private function _start(string $name, array $tags = [], array $args = []): Scope {
$tracer = GlobalTracer::get();
$tags['args'] = json_encode($args);
return $tracer->startActiveSpan($name, ['tags' => $tags]);
}
/**
* @param string $name
* @param array<string>|array<string, array<string, mixed>> $tags
* @param array<string> $args
* @return Scope
*/
public static function start(string $name, array $tags = [], array $args = []) : Scope {
return self::get_instance()->_start($name, $tags, $args);
}
public static function get_instance() : Tracer {
if (self::$instance == null)
self::$instance = new self();
return self::$instance;
}
}

View File

@ -1,654 +0,0 @@
<?php
class UrlHelper {
const EXTRA_HREF_SCHEMES = [
"magnet",
"mailto",
"tel"
];
const EXTRA_SCHEMES_BY_CONTENT_TYPE = [
"application/x-bittorrent" => [ "magnet" ],
];
static string $fetch_last_error;
static int $fetch_last_error_code;
static string $fetch_last_error_content;
static string $fetch_last_content_type;
static string $fetch_last_modified;
static string $fetch_effective_url;
static string $fetch_effective_ip_addr;
static bool $fetch_curl_used;
/**
* @param array<string, string|int> $parts
*/
static function build_url(array $parts): string {
$tmp = $parts['scheme'] . "://" . $parts['host'];
if (isset($parts['path'])) $tmp .= $parts['path'];
if (isset($parts['query'])) $tmp .= '?' . $parts['query'];
if (isset($parts['fragment'])) $tmp .= '#' . $parts['fragment'];
return $tmp;
}
/**
* Converts a (possibly) relative URL to a absolute one, using provided base URL.
* Provides some exceptions for additional schemes like data: if called with owning element/attribute.
*
* @param string $base_url Base URL (i.e. from where the document is)
* @param string $rel_url Possibly relative URL in the document
* @param string $owner_element Owner element tag name (i.e. "a") (optional)
* @param string $owner_attribute Owner attribute (i.e. "href") (optional)
* @param string $content_type URL content type as specified by enclosures, etc.
*
* @return false|string Absolute URL or false on failure (either during URL parsing or validation)
*/
public static function rewrite_relative($base_url,
$rel_url,
string $owner_element = "",
string $owner_attribute = "",
string $content_type = "") {
$rel_parts = parse_url($rel_url);
if (!$rel_url) return $base_url;
/**
* If parse_url failed to parse $rel_url return false to match the current "invalid thing" behavior
* of UrlHelper::validate().
*
* TODO: There are many places where a string return value is assumed. We should either update those
* to account for the possibility of failure, or look into updating this function's return values.
*/
if ($rel_parts === false) {
return false;
}
if (!empty($rel_parts['host']) && !empty($rel_parts['scheme'])) {
return self::validate($rel_url);
// protocol-relative URL (rare but they exist)
} else if (strpos($rel_url, "//") === 0) {
return self::validate("https:" . $rel_url);
// allow some extra schemes for A href
} else if (in_array($rel_parts["scheme"] ?? "", self::EXTRA_HREF_SCHEMES, true) &&
$owner_element == "a" &&
$owner_attribute == "href") {
return $rel_url;
// allow some extra schemes for links with feed-specified content type i.e. enclosures
} else if ($content_type &&
isset(self::EXTRA_SCHEMES_BY_CONTENT_TYPE[$content_type]) &&
in_array($rel_parts["scheme"], self::EXTRA_SCHEMES_BY_CONTENT_TYPE[$content_type])) {
return $rel_url;
// allow limited subset of inline base64-encoded images for IMG elements
} else if (($rel_parts["scheme"] ?? "") == "data" &&
preg_match('%^image/(webp|gif|jpg|png|svg);base64,%', $rel_parts["path"]) &&
$owner_element == "img" &&
$owner_attribute == "src") {
return $rel_url;
} else {
$base_parts = parse_url($base_url);
$rel_parts['host'] = $base_parts['host'] ?? "";
$rel_parts['scheme'] = $base_parts['scheme'] ?? "";
if ($rel_parts['path'] ?? "") {
// we append dirname() of base path to relative URL path as per RFC 3986 section 5.2.2
$base_path = with_trailing_slash(dirname($base_parts['path'] ?? ""));
// 1. absolute relative path (/test.html) = no-op, proceed as is
// 2. dotslash relative URI (./test.html) - strip "./", append base path
if (strpos($rel_parts['path'], './') === 0) {
$rel_parts['path'] = $base_path . substr($rel_parts['path'], 2);
// 3. anything else relative (test.html) - append dirname() of base path
} else if (strpos($rel_parts['path'], '/') !== 0) {
$rel_parts['path'] = $base_path . $rel_parts['path'];
}
//$rel_parts['path'] = str_replace("/./", "/", $rel_parts['path']);
//$rel_parts['path'] = str_replace("//", "/", $rel_parts['path']);
}
return self::validate(self::build_url($rel_parts));
}
}
/** extended filtering involves validation for safe ports and loopback
* @return false|string false if something went wrong, otherwise the URL string
*/
static function validate(string $url, bool $extended_filtering = false) {
$url = clean($url);
# fix protocol-relative URLs
if (strpos($url, "//") === 0)
$url = "https:" . $url;
$tokens = parse_url($url);
// this isn't really necessary because filter_var(... FILTER_VALIDATE_URL) requires host and scheme
// as per https://php.watch/versions/7.3/filter-var-flag-deprecation but it might save time
if (empty($tokens['host']))
return false;
if (!in_array(strtolower($tokens['scheme']), ['http', 'https']))
return false;
//convert IDNA hostname to punycode if possible
if (function_exists("idn_to_ascii")) {
if (mb_detect_encoding($tokens['host']) != 'ASCII') {
if (defined('IDNA_NONTRANSITIONAL_TO_ASCII') && defined('INTL_IDNA_VARIANT_UTS46')) {
$tokens['host'] = idn_to_ascii($tokens['host'], IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
} else {
$tokens['host'] = idn_to_ascii($tokens['host']);
}
// if `idn_to_ascii` failed
if ($tokens['host'] === false) {
return false;
}
}
}
// separate set of tokens with urlencoded 'path' because filter_var() rightfully fails on non-latin characters
// (used for validation only, we actually request the original URL, in case of urlencode breaking it)
$tokens_filter_var = $tokens;
if ($tokens['path'] ?? false) {
$tokens_filter_var['path'] = implode("/",
array_map("rawurlencode",
array_map("rawurldecode",
explode("/", $tokens['path']))));
}
$url = self::build_url($tokens);
$url_filter_var = self::build_url($tokens_filter_var);
if (filter_var($url_filter_var, FILTER_VALIDATE_URL) === false)
return false;
if ($extended_filtering) {
if (!in_array($tokens['port'] ?? '', [80, 443, '']))
return false;
if (strtolower($tokens['host']) == 'localhost' || $tokens['host'] == '::1' || strpos($tokens['host'], '127.') === 0)
return false;
}
return $url;
}
/**
* @return false|string
*/
static function resolve_redirects(string $url, int $timeout, int $nest = 0) {
$scope = Tracer::start(__METHOD__, ['url' => $url]);
// too many redirects
if ($nest > 10) {
$scope->getSpan()->setTag('error', 'too many redirects');
$scope->close();
return false;
}
$context_options = array(
'http' => array(
'header' => array(
'Connection: close'
),
'method' => 'HEAD',
'timeout' => $timeout,
'protocol_version'=> 1.1)
);
if (Config::get(Config::HTTP_PROXY)) {
$context_options['http']['request_fulluri'] = true;
$context_options['http']['proxy'] = Config::get(Config::HTTP_PROXY);
}
$context = stream_context_create($context_options);
// PHP 8 changed the second param from int to bool, but we still support PHP >= 7.4.0
// @phpstan-ignore-next-line
$headers = get_headers($url, 0, $context);
if (is_array($headers)) {
$headers = array_reverse($headers); // last one is the correct one
foreach($headers as $header) {
if (stripos($header, 'Location:') === 0) {
$url = self::rewrite_relative($url, trim(substr($header, strlen('Location:'))));
return self::resolve_redirects($url, $timeout, $nest + 1);
}
}
$scope->close();
return $url;
}
$scope->getSpan()->setTag('error', 'request failed');
$scope->close();
// request failed?
return false;
}
/**
* @param array<string, bool|int|string>|string $options
* @return false|string false if something went wrong, otherwise string contents
*/
// TODO: max_size currently only works for CURL transfers
// TODO: multiple-argument way is deprecated, first parameter is a hash now
public static function fetch($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
self::$fetch_last_error = "";
self::$fetch_last_error_code = -1;
self::$fetch_last_error_content = "";
self::$fetch_last_content_type = "";
self::$fetch_curl_used = false;
self::$fetch_last_modified = "";
self::$fetch_effective_url = "";
self::$fetch_effective_ip_addr = "";
if (!is_array($options)) {
// falling back on compatibility shim
$option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
$tmp = [];
for ($i = 0; $i < func_num_args(); $i++) {
$tmp[$option_names[$i]] = func_get_arg($i);
}
$options = $tmp;
/*$options = array(
"url" => func_get_arg(0),
"type" => @func_get_arg(1),
"login" => @func_get_arg(2),
"pass" => @func_get_arg(3),
"post_query" => @func_get_arg(4),
"timeout" => @func_get_arg(5),
"timestamp" => @func_get_arg(6),
"useragent" => @func_get_arg(7)
); */
}
$url = $options["url"];
$scope = Tracer::start(__METHOD__, ['url' => $url]);
$type = isset($options["type"]) ? $options["type"] : false;
$login = isset($options["login"]) ? $options["login"] : false;
$pass = isset($options["pass"]) ? $options["pass"] : false;
$post_query = isset($options["post_query"]) ? $options["post_query"] : false;
$timeout = isset($options["timeout"]) ? $options["timeout"] : false;
$last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
$useragent = isset($options["useragent"]) ? $options["useragent"] : false;
$followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
$max_size = isset($options["max_size"]) ? $options["max_size"] : Config::get(Config::MAX_DOWNLOAD_FILE_SIZE); // in bytes
$http_accept = isset($options["http_accept"]) ? $options["http_accept"] : false;
$http_referrer = isset($options["http_referrer"]) ? $options["http_referrer"] : false;
$url = ltrim($url, ' ');
$url = str_replace(' ', '%20', $url);
Debug::log("[UrlHelper] fetching: $url", Debug::LOG_EXTENDED);
$url = self::validate($url, true);
if (!$url) {
self::$fetch_last_error = "Requested URL failed extended validation.";
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
$url_host = parse_url($url, PHP_URL_HOST);
$ip_addr = gethostbyname($url_host);
if (!$ip_addr || strpos($ip_addr, "127.") === 0) {
self::$fetch_last_error = "URL hostname failed to resolve or resolved to a loopback address ($ip_addr)";
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
if (function_exists('curl_init') && !ini_get("open_basedir")) {
self::$fetch_curl_used = true;
$ch = curl_init($url);
if (!$ch) {
self::$fetch_last_error = "curl_init() failed";
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
$curl_http_headers = [];
if ($last_modified && !$post_query)
array_push($curl_http_headers, "If-Modified-Since: $last_modified");
if ($http_accept)
array_push($curl_http_headers, "Accept: " . $http_accept);
if (count($curl_http_headers) > 0)
curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_http_headers);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT));
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : Config::get(Config::FILE_FETCH_TIMEOUT));
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $followlocation);
curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent : Config::get_user_agent());
curl_setopt($ch, CURLOPT_ENCODING, "");
curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
if ($http_referrer)
curl_setopt($ch, CURLOPT_REFERER, $http_referrer);
if ($max_size) {
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_BUFFERSIZE, 16384); // needed to get 5 arguments in progress function?
// holy shit closures in php
// download & upload are *expected* sizes respectively, could be zero
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($curl_handle, $download_size, $downloaded, $upload_size, $uploaded) use(&$max_size, $url) {
//Debug::log("[curl progressfunction] $downloaded $max_size", Debug::$LOG_EXTENDED);
if ($downloaded > $max_size) {
Debug::log("[UrlHelper] fetch error: curl reached max size of $max_size bytes downloading $url, aborting.", Debug::LOG_VERBOSE);
return 1;
}
return 0;
});
}
if (Config::get(Config::HTTP_PROXY)) {
curl_setopt($ch, CURLOPT_PROXY, Config::get(Config::HTTP_PROXY));
}
if ($post_query) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_query);
}
if ($login && $pass)
curl_setopt($ch, CURLOPT_USERPWD, "$login:$pass");
$ret = @curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// CURLAUTH_BASIC didn't work, let's retry with CURLAUTH_ANY in case it's actually something
// unusual like NTLM...
if ($http_code == 403 && $login && $pass) {
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
$ret = @curl_exec($ch);
}
if (curl_errno($ch) === 23 || curl_errno($ch) === 61) {
curl_setopt($ch, CURLOPT_ENCODING, 'none');
$ret = @curl_exec($ch);
}
$headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = explode("\r\n", substr($ret, 0, $headers_length));
$contents = substr($ret, $headers_length);
foreach ($headers as $header) {
if (strstr($header, ": ") !== false) {
list ($key, $value) = explode(": ", $header);
if (strtolower($key) == "last-modified") {
self::$fetch_last_modified = $value;
}
}
if (substr(strtolower($header), 0, 7) == 'http/1.') {
self::$fetch_last_error_code = (int) substr($header, 9, 3);
self::$fetch_last_error = $header;
}
}
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
self::$fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
self::$fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
if (!self::validate(self::$fetch_effective_url, true)) {
self::$fetch_last_error = "URL received after redirection failed extended validation.";
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
self::$fetch_effective_ip_addr = gethostbyname(parse_url(self::$fetch_effective_url, PHP_URL_HOST));
if (!self::$fetch_effective_ip_addr || strpos(self::$fetch_effective_ip_addr, "127.") === 0) {
self::$fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address (".self::$fetch_effective_ip_addr.")";
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
self::$fetch_last_error_code = $http_code;
if ($http_code != 200 || $type && strpos(self::$fetch_last_content_type, "$type") === false) {
if (curl_errno($ch) != 0) {
self::$fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
} else {
self::$fetch_last_error = "HTTP Code: $http_code ";
}
self::$fetch_last_error_content = $contents;
curl_close($ch);
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
if (!$contents) {
if (curl_errno($ch) === 0) {
self::$fetch_last_error = 'Successful response, but no content was received.';
} else {
self::$fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
}
curl_close($ch);
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
curl_close($ch);
$is_gzipped = RSSUtils::is_gzipped($contents);
if ($is_gzipped && is_string($contents)) {
$tmp = @gzdecode($contents);
if ($tmp) $contents = $tmp;
}
$scope->close();
return $contents;
} else {
self::$fetch_curl_used = false;
if ($login && $pass){
$url_parts = array();
preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
$pass = urlencode($pass);
if ($url_parts[1] && $url_parts[2]) {
$url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
}
}
// TODO: should this support POST requests or not? idk
$context_options = array(
'http' => array(
'header' => array(
'Connection: close'
),
'method' => 'GET',
'ignore_errors' => true,
'timeout' => $timeout ? $timeout : Config::get(Config::FILE_FETCH_TIMEOUT),
'protocol_version'=> 1.1)
);
if (!$post_query && $last_modified)
array_push($context_options['http']['header'], "If-Modified-Since: $last_modified");
if ($http_accept)
array_push($context_options['http']['header'], "Accept: $http_accept");
if ($http_referrer)
array_push($context_options['http']['header'], "Referer: $http_referrer");
if (Config::get(Config::HTTP_PROXY)) {
$context_options['http']['request_fulluri'] = true;
$context_options['http']['proxy'] = Config::get(Config::HTTP_PROXY);
}
$context = stream_context_create($context_options);
$old_error = error_get_last();
self::$fetch_effective_url = self::resolve_redirects($url, $timeout ? $timeout : Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT));
if (!self::validate(self::$fetch_effective_url, true)) {
self::$fetch_last_error = "URL received after redirection failed extended validation.";
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
self::$fetch_effective_ip_addr = gethostbyname(parse_url(self::$fetch_effective_url, PHP_URL_HOST));
if (!self::$fetch_effective_ip_addr || strpos(self::$fetch_effective_ip_addr, "127.") === 0) {
self::$fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address (".self::$fetch_effective_ip_addr.")";
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
$data = @file_get_contents($url, false, $context);
if ($data === false) {
self::$fetch_last_error = "'file_get_contents' failed.";
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
foreach ($http_response_header as $header) {
if (strstr($header, ": ") !== false) {
list ($key, $value) = explode(": ", $header);
$key = strtolower($key);
if ($key == 'content-type') {
self::$fetch_last_content_type = $value;
// don't abort here b/c there might be more than one
// e.g. if we were being redirected -- last one is the right one
} else if ($key == 'last-modified') {
self::$fetch_last_modified = $value;
} else if ($key == 'location') {
self::$fetch_effective_url = $value;
}
}
if (substr(strtolower($header), 0, 7) == 'http/1.') {
self::$fetch_last_error_code = (int) substr($header, 9, 3);
self::$fetch_last_error = $header;
}
}
if (self::$fetch_last_error_code != 200) {
$error = error_get_last();
if (($error['message'] ?? '') != ($old_error['message'] ?? '')) {
self::$fetch_last_error .= "; " . $error["message"];
}
self::$fetch_last_error_content = $data;
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
if ($data) {
$is_gzipped = RSSUtils::is_gzipped($data);
if ($is_gzipped) {
$tmp = @gzdecode($data);
if ($tmp) $data = $tmp;
}
$scope->close();
return $data;
} else {
self::$fetch_last_error = 'Successful response, but no content was received.';
$scope->getSpan()->setTag('error', self::$fetch_last_error);
$scope->close();
return false;
}
}
}
/**
* @return false|string false if the provided URL didn't match expected patterns, otherwise the video ID string
*/
public static function url_to_youtube_vid(string $url) {
$url = str_replace("youtube.com", "youtube-nocookie.com", $url);
$regexps = [
"/\/\/www\.youtube-nocookie\.com\/v\/([\w-]+)/",
"/\/\/www\.youtube-nocookie\.com\/embed\/([\w-]+)/",
"/\/\/www\.youtube-nocookie\.com\/watch?v=([\w-]+)/",
"/\/\/youtu.be\/([\w-]+)/",
];
foreach ($regexps as $re) {
$matches = [];
if (preg_match($re, $url, $matches)) {
return $matches[1];
}
}
return false;
}
}

View File

@ -1,6 +1,9 @@
{
"config": {
"platform-check": false
"platform-check": false,
"allow-plugins": {
"php-http/discovery": true
}
},
"repositories": [
{
@ -9,15 +12,24 @@
"url": "https://dev.tt-rss.org/fox/idiorm.git"
}
],
"autoload": {
"psr-4": {
"": "classes/"
}
},
"require": {
"spomky-labs/otphp": "^10.0",
"chillerlan/php-qrcode": "^4.3.3",
"mervick/material-design-icons": "^2.2",
"j4mie/idiorm": "dev-master",
"jonahgeorge/jaeger-client-php": "^1.4"
"open-telemetry/exporter-otlp": "^1.0",
"php-http/guzzle7-adapter": "^1.0",
"soundasleep/html2text": "^2.1",
"guzzlehttp/guzzle": "^7.0"
},
"require-dev": {
"phpstan/phpstan": "1.10.3",
"phpunit/phpunit": "9.5.16"
"phpunit/phpunit": "9.5.16",
"phpunit/php-code-coverage": "^9.2"
}
}

1712
composer.lock generated

File diff suppressed because it is too large Load Diff

59
docker-compose.yml Normal file
View File

@ -0,0 +1,59 @@
#
# simplified compose FOR LOCAL DEVELOPMENT.
#
# please don't use this in production: it has no database persistence and maps to tt-rss source directly.
#
version: '3'
services:
db:
image: postgres:15-alpine
restart: unless-stopped
env_file:
- .env
environment:
- POSTGRES_USER=${TTRSS_DB_USER}
- POSTGRES_PASSWORD=${TTRSS_DB_PASS}
- POSTGRES_DB=${TTRSS_DB_NAME}
app:
image: cthulhoo/ttrss-fpm-pgsql-static:latest
environment:
SKIP_RSYNC_ON_STARTUP: true
build:
dockerfile: .docker/app/Dockerfile
context: .
restart: unless-stopped
env_file:
- .env
volumes:
- .:/var/www/html/tt-rss
depends_on:
- db
updater:
image: cthulhoo/ttrss-fpm-pgsql-static:latest
restart: unless-stopped
env_file:
- .env
volumes:
- .:/var/www/html/tt-rss
depends_on:
- app
command: /opt/tt-rss/updater.sh
web-nginx:
image: cthulhoo/ttrss-web-nginx:latest
build:
dockerfile: .docker/web-nginx/Dockerfile
context: .
restart: unless-stopped
env_file:
- .env
ports:
- ${HTTP_PORT}:80
volumes:
- .:/var/www/html/tt-rss:ro
depends_on:
- app

0
feed-icons/.empty Executable file → Normal file
View File

View File

@ -1,17 +1,2 @@
<?php
spl_autoload_register(function($class) {
$root_dir = dirname(__DIR__); // we were in tt-rss/include
// - internal tt-rss classes are loaded from classes/ and use special naming logic instead of namespaces
// - plugin classes are loaded by PluginHandler from plugins.local/ and plugins/
$class_file = "$root_dir/classes/" . str_replace("_", "/", strtolower($class)) . ".php";
if (file_exists($class_file))
include $class_file;
});
// also pull composer autoloader
require_once "vendor/autoload.php";

View File

@ -29,7 +29,7 @@
}
function pluginhandler_tags(\Plugin $plugin, string $method): string {
return hidden_tag("op", "pluginhandler") .
return hidden_tag("op", "PluginHandler") .
hidden_tag("plugin", strtolower(get_class($plugin))) .
hidden_tag("method", $method);
}

View File

@ -38,6 +38,8 @@
/**
* @return bool|int|null|string
*
* @deprecated by Prefs::get()
*/
function get_pref(string $pref_name, int $owner_uid = null) {
return Prefs::get($pref_name, $owner_uid ? $owner_uid : $_SESSION["uid"], $_SESSION["profile"] ?? null);
@ -45,6 +47,8 @@
/**
* @param bool|int|string $value
*
* @deprecated by Prefs::set()
*/
function set_pref(string $pref_name, $value, int $owner_uid = null, bool $strip_tags = true): bool {
return Prefs::set($pref_name, $value, $owner_uid ? $owner_uid : $_SESSION["uid"], $_SESSION["profile"] ?? null, $strip_tags);

View File

@ -104,7 +104,7 @@
</script>
<?php $return = urlencode(!empty($_REQUEST['return']) ? $_REQUEST['return'] : with_trailing_slash(Config::make_self_url())) ?>
<?php $return = urlencode(!empty($_REQUEST['return']) ? $_REQUEST['return'] : with_trailing_slash(Config::get_self_url())) ?>
<div class="container">

View File

@ -93,7 +93,7 @@
$sth = \Db::pdo()->prepare("SELECT id FROM ttrss_sessions WHERE id=?");
$sth->execute([$id]);
if ($row = $sth->fetch()) {
if ($sth->fetch()) {
$sth = \Db::pdo()->prepare("UPDATE ttrss_sessions SET data=?, expire=? WHERE id=?");
$sth->execute([$data, $expire, $id]);
} else {

View File

@ -45,7 +45,7 @@
<style type="text/css">
<?php
foreach (PluginHost::getInstance()->get_plugins() as $n => $p) {
foreach (PluginHost::getInstance()->get_plugins() as $p) {
if (method_exists($p, "get_css")) {
echo $p->get_css();
}
@ -263,6 +263,7 @@
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcPrefs')"><?= __('Preferences...') ?></div>
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcSearch')"><?= __('Search...') ?></div>
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcFilterFeeds')"><?= __('Search feeds...') ?></div>
<div dojoType="dijit.MenuItem" disabled="1"><?= __('Feed actions:') ?></div>
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcAddFeed')"><?= __('Subscribe to feed...') ?></div>
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcEditFeed')"><?= __('Edit this feed...') ?></div>

View File

@ -153,7 +153,7 @@ const App = {
return dijit.getEnclosingWidget(elem.closest('.dijitDialog'));
},
getPhArgs(plugin, method, args = {}) {
return {...{op: "pluginhandler", plugin: plugin, method: method}, ...args};
return {...{op: "PluginHandler", plugin: plugin, method: method}, ...args};
},
label_to_feed_id: function(label) {
return this.LABEL_BASE_INDEX - 1 - Math.abs(label);
@ -291,7 +291,7 @@ const App = {
setCombinedMode: function(combined) {
const value = combined ? "true" : "false";
xhr.post("backend.php", {op: "rpc", method: "setpref", key: "COMBINED_DISPLAY_MODE", value: value}, () => {
xhr.post("backend.php", {op: "RPC", method: "setpref", key: "COMBINED_DISPLAY_MODE", value: value}, () => {
this.setInitParam("combined_display_mode",
!this.getInitParam("combined_display_mode"));
@ -306,7 +306,7 @@ const App = {
if (App.isCombinedMode()) {
const value = expand ? "true" : "false";
xhr.post("backend.php", {op: "rpc", method: "setpref", key: "CDM_EXPANDED", value: value}, () => {
xhr.post("backend.php", {op: "RPC", method: "setpref", key: "CDM_EXPANDED", value: value}, () => {
this.setInitParam("cdm_expanded", !this.getInitParam("cdm_expanded"));
Headlines.renderAgain();
});
@ -440,7 +440,7 @@ const App = {
}
},
hotkeyHelp: function() {
xhr.post("backend.php", {op: "rpc", method: "hotkeyHelp"}, (reply) => {
xhr.post("backend.php", {op: "RPC", method: "hotkeyHelp"}, (reply) => {
const dialog = new fox.SingleUseDialog({
title: __("Keyboard shortcuts"),
content: reply,
@ -621,7 +621,7 @@ const App = {
try {
xhr.post("backend.php",
{op: "rpc", method: "log",
{op: "RPC", method: "log",
file: params.filename ? params.filename : error.fileName,
line: params.lineno ? params.lineno : error.lineNumber,
msg: message,
@ -703,7 +703,7 @@ const App = {
this.initHotkeyActions();
const params = {
op: "rpc",
op: "RPC",
method: "sanityCheck",
clientTzOffset: new Date().getTimezoneOffset() * 60,
hasSandbox: "sandbox" in document.createElement("iframe"),
@ -737,7 +737,7 @@ const App = {
return errorMsg == "";
},
updateRuntimeInfo: function() {
xhr.json("backend.php", {op: "rpc", method: "getruntimeinfo"}, () => {
xhr.json("backend.php", {op: "RPC", method: "getruntimeinfo"}, () => {
// handled by xhr.json()
});
},
@ -858,7 +858,7 @@ const App = {
checkForUpdates: function() {
console.log('checking for updates...');
xhr.json("backend.php", {op: 'rpc', method: 'checkforupdates'})
xhr.json("backend.php", {op: 'RPC', method: 'checkforupdates'})
.then((reply) => {
console.log('update reply', reply);
@ -965,7 +965,7 @@ const App = {
if (article_id) Article.view(article_id);
xhr.post("backend.php", {op: "rpc", method: "setWidescreen", wide: wide ? 1 : 0});
xhr.post("backend.php", {op: "RPC", method: "setWidescreen", wide: wide ? 1 : 0});
},
initHotkeyActions: function() {
if (this.is_prefs) {
@ -1149,7 +1149,7 @@ const App = {
if (!Feeds.activeIsCat() && parseInt(Feeds.getActive()) > 0) {
/* global __csrf_token */
App.postOpenWindow("backend.php", {op: "feeds", method: "updatedebugger",
App.postOpenWindow("backend.php", {op: "Feeds", method: "updatedebugger",
feed_id: Feeds.getActive(), csrf_token: __csrf_token});
} else {
@ -1158,7 +1158,7 @@ const App = {
};
this.hotkey_actions["feed_debug_viewfeed"] = () => {
App.postOpenWindow("backend.php", {op: "feeds", method: "view",
App.postOpenWindow("backend.php", {op: "Feeds", method: "view",
feed: Feeds.getActive(), timestamps: 1, debug: 1, cat: Feeds.activeIsCat(), csrf_token: __csrf_token});
};
@ -1177,13 +1177,13 @@ const App = {
Headlines.reverse();
};
this.hotkey_actions["feed_toggle_grid"] = () => {
xhr.json("backend.php", {op: "rpc", method: "togglepref", key: "CDM_ENABLE_GRID"}, (reply) => {
xhr.json("backend.php", {op: "RPC", method: "togglepref", key: "CDM_ENABLE_GRID"}, (reply) => {
App.setInitParam("cdm_enable_grid", reply.value);
Headlines.renderAgain();
})
};
this.hotkey_actions["feed_toggle_vgroup"] = () => {
xhr.post("backend.php", {op: "rpc", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => {
xhr.post("backend.php", {op: "RPC", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => {
Feeds.reloadCurrent();
})
};
@ -1270,11 +1270,14 @@ const App = {
case "qmcSearch":
Feeds.search();
break;
case "qmcFilterFeeds":
Feeds.filter();
break;
case "qmcAddFeed":
CommonDialogs.subscribeToFeed();
break;
case "qmcDigest":
window.location.href = "backend.php?op=digest";
window.location.href = "backend.php?op=Digest";
break;
case "qmcEditFeed":
if (Feeds.activeIsCat())

View File

@ -123,7 +123,7 @@ const Article = {
Article.setActive(0);
},
displayUrl: function (id) {
const query = {op: "article", method: "getmetadatabyid", id: id};
const query = {op: "Article", method: "getmetadatabyid", id: id};
xhr.json("backend.php", query, (reply) => {
if (reply && reply.link) {
@ -136,7 +136,7 @@ const Article = {
openInNewWindow: function (id) {
/* global __csrf_token */
App.postOpenWindow("backend.php",
{ "op": "article", "method": "redirect", "id": id, "csrf_token": __csrf_token });
{ "op": "Article", "method": "redirect", "id": id, "csrf_token": __csrf_token });
Headlines.toggleUnread(id, 0);
},
@ -352,7 +352,7 @@ const Article = {
title: __("Article tags"),
content: `
${App.FormFields.hidden_tag("id", id.toString())}
${App.FormFields.hidden_tag("op", "article")}
${App.FormFields.hidden_tag("op", "Article")}
${App.FormFields.hidden_tag("method", "setArticleTags")}
<header class='horizontal'>
@ -395,7 +395,7 @@ const Article = {
const tmph = dojo.connect(dialog, 'onShow', function () {
dojo.disconnect(tmph);
xhr.json("backend.php", {op: "article", method: "printArticleTags", id: id}, (reply) => {
xhr.json("backend.php", {op: "Article", method: "printArticleTags", id: id}, (reply) => {
dijit.getEnclosingWidget(App.byId("tags_str"))
.attr('value', reply.tags.join(", "))
@ -404,7 +404,7 @@ const Article = {
App.byId('tags_str').onkeyup = (e) => {
const last_tag = e.target.value.split(',').pop().trim();
xhr.json("backend.php", {op: 'article', method: 'completeTags', search: last_tag}, (data) => {
xhr.json("backend.php", {op: 'Article', method: 'completeTags', search: last_tag}, (data) => {
App.byId("tags_choices").innerHTML = `${data.map((tag) =>
`<a href="#" onclick="Article.autocompleteInject(this, 'tags_str')">${tag}</a>` )
.join(', ')}`

View File

@ -28,14 +28,14 @@ const CommonDialogs = {
},
subscribeToFeed: function() {
xhr.json("backend.php",
{op: "feeds", method: "subscribeToFeed"},
{op: "Feeds", method: "subscribeToFeed"},
(reply) => {
const dialog = new fox.SingleUseDialog({
title: __("Subscribe to feed"),
content: `
<form onsubmit='return false'>
${App.FormFields.hidden_tag("op", "feeds")}
${App.FormFields.hidden_tag("op", "Feeds")}
${App.FormFields.hidden_tag("method", "add")}
<div id='fadd_error_message' style='display : none' class='alert alert-danger'></div>
@ -215,7 +215,7 @@ const CommonDialogs = {
},
showFeedsWithErrors: function() {
xhr.json("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (reply) => {
xhr.json("backend.php", {op: "Pref_Feeds", method: "feedsWithErrors"}, (reply) => {
const dialog = new fox.SingleUseDialog({
id: "errorFeedsDlg",
@ -231,7 +231,7 @@ const CommonDialogs = {
Notify.progress("Removing selected feeds...", true);
const query = {
op: "pref-feeds", method: "remove",
op: "Pref_Feeds", method: "remove",
ids: sel_rows.toString()
};
@ -268,7 +268,7 @@ const CommonDialogs = {
<table width='100%' id='error-feeds-list'>
${reply.map((row) => `
<tr data-row-id='${row.id}'>
<tr data-row-id='${row.id}' style='vertical-align: top'>
<td class='checkbox'>
<input onclick='Tables.onRowChecked(this)' dojoType="dijit.form.CheckBox"
type="checkbox">
@ -305,7 +305,7 @@ const CommonDialogs = {
if (caption != undefined && caption.trim().length > 0) {
const query = {op: "pref-labels", method: "add", caption: caption.trim()};
const query = {op: "Pref_Labels", method: "add", caption: caption.trim()};
Notify.progress("Loading, please wait...", true);
@ -325,7 +325,7 @@ const CommonDialogs = {
if (typeof title == "undefined" || confirm(msg)) {
Notify.progress("Removing feed...");
const query = {op: "pref-feeds", quiet: 1, method: "remove", ids: feed_id};
const query = {op: "Pref_Feeds", quiet: 1, method: "remove", ids: feed_id};
xhr.post("backend.php", query, () => {
if (App.isPrefs()) {
@ -348,7 +348,7 @@ const CommonDialogs = {
if (feed_id <= 0)
return alert(__("You can't edit this kind of feed."));
const query = {op: "pref-feeds", method: "editfeed", id: feed_id};
const query = {op: "Pref_Feeds", method: "editfeed", id: feed_id};
console.log("editFeed", query);
@ -378,7 +378,7 @@ const CommonDialogs = {
const fd = new FormData();
fd.append('icon_file', icon_file)
fd.append('feed_id', feed_id);
fd.append('op', 'pref-feeds');
fd.append('op', 'Pref_Feeds');
fd.append('method', 'uploadIcon');
fd.append('csrf_token', App.getInitParam("csrf_token"));
@ -427,7 +427,7 @@ const CommonDialogs = {
if (confirm(__("Remove stored feed icon?"))) {
Notify.progress("Removing feed icon...", true);
xhr.post("backend.php", {op: "pref-feeds", method: "removeicon", feed_id: id}, () => {
xhr.post("backend.php", {op: "Pref_Feeds", method: "removeicon", feed_id: id}, () => {
Notify.info("Feed icon removed.");
if (App.isPrefs())
@ -470,7 +470,7 @@ const CommonDialogs = {
const tmph = dojo.connect(dialog, 'onShow', function () {
dojo.disconnect(tmph);
xhr.json("backend.php", {op: "pref-feeds", method: "editfeed", id: feed_id}, (reply) => {
xhr.json("backend.php", {op: "Pref_Feeds", method: "editfeed", id: feed_id}, (reply) => {
const feed = reply.feed;
const is_readonly = reply.user.access_level == App.UserAccessLevels.ACCESS_LEVEL_READONLY;
@ -493,7 +493,7 @@ const CommonDialogs = {
<div dojoType="dijit.layout.ContentPane" title="${__('General')}">
${App.FormFields.hidden_tag("id", feed_id)}
${App.FormFields.hidden_tag("op", "pref-feeds")}
${App.FormFields.hidden_tag("op", "Pref_Feeds")}
${App.FormFields.hidden_tag("method", "editSave")}
<section>
@ -621,7 +621,7 @@ const CommonDialogs = {
Notify.progress("Loading, please wait...", true);
xhr.json("backend.php", {op: "pref-feeds", method: "getsharedurl", id: feed, is_cat: is_cat, search: search}, (reply) => {
xhr.json("backend.php", {op: "Pref_Feeds", method: "getsharedurl", id: feed, is_cat: is_cat, search: search}, (reply) => {
try {
const dialog = new fox.SingleUseDialog({
title: __("Show as feed"),
@ -630,7 +630,7 @@ const CommonDialogs = {
Notify.progress("Trying to change address...", true);
const query = {op: "pref-feeds", method: "regenFeedKey", id: feed, is_cat: is_cat};
const query = {op: "Pref_Feeds", method: "regenFeedKey", id: feed, is_cat: is_cat};
xhr.json("backend.php", query, (reply) => {
const new_link = reply.link;

View File

@ -115,7 +115,7 @@ const Filters = {
insertRule: function(parentNode, replaceNode) {
const rule = dojo.formToJson("filter_new_rule_form");
xhr.post("backend.php", {op: "pref-filters", method: "printrulename", rule: rule}, (reply) => {
xhr.post("backend.php", {op: "Pref_Filters", method: "printrulename", rule: rule}, (reply) => {
try {
const li = document.createElement('li');
li.addClassName("rule");
@ -147,7 +147,7 @@ const Filters = {
const action = dojo.formToJson(form);
xhr.post("backend.php", { op: "pref-filters", method: "printactionname", action: action }, (reply) => {
xhr.post("backend.php", { op: "Pref_Filters", method: "printactionname", action: action }, (reply) => {
try {
const li = document.createElement('li');
li.addClassName("action");
@ -200,7 +200,7 @@ const Filters = {
console.log(rule, dialog.filter_info);
xhr.json("backend.php", {op: "pref-filters", method: "editrule", ids: rule.feed_id.join(",")}, function (editrule) {
xhr.json("backend.php", {op: "Pref_Filters", method: "editrule", ids: rule.feed_id.join(",")}, function (editrule) {
edit_rule_dialog.attr('content',
`
<form name="filter_new_rule_form" id="filter_new_rule_form" onsubmit="return false">
@ -326,7 +326,7 @@ const Filters = {
dijit.byId("filterDlg_actionSelect").attr('value', action.action_id);
/*xhr.post("backend.php", {op: 'pref-filters', method: 'newaction', action: actionStr}, (reply) => {
/*xhr.post("backend.php", {op: 'Pref_Filters', method: 'newaction', action: actionStr}, (reply) => {
edit_action_dialog.attr('content', reply);
setTimeout(() => {
@ -365,7 +365,7 @@ const Filters = {
Notify.progress("Removing filter...");
const query = {op: "pref-filters", method: "remove", ids: this.attr('value').id};
const query = {op: "Pref_Filters", method: "remove", ids: this.attr('value').id};
xhr.post("backend.php", query, () => {
const tree = dijit.byId("filterTree");
@ -411,7 +411,7 @@ const Filters = {
const tmph = dojo.connect(dialog, 'onShow', function () {
dojo.disconnect(tmph);
xhr.json("backend.php", {op: "pref-filters", method: "edit", id: filter_id}, function (filter) {
xhr.json("backend.php", {op: "Pref_Filters", method: "edit", id: filter_id}, function (filter) {
dialog.filter_info = filter;
@ -425,7 +425,7 @@ const Filters = {
`
<form onsubmit='return false'>
${App.FormFields.hidden_tag("op", "pref-filters")}
${App.FormFields.hidden_tag("op", "Pref_Filters")}
${App.FormFields.hidden_tag("id", filter_id)}
${App.FormFields.hidden_tag("method", filter_id ? "editSave" : "add")}
${App.FormFields.hidden_tag("csrf_token", App.getInitParam('csrf_token'))}
@ -541,7 +541,7 @@ const Filters = {
dialog.editRule(null, dojo.toJson(rule));
} else {
const query = {op: "article", method: "getmetadatabyid", id: Article.getActive()};
const query = {op: "Article", method: "getmetadatabyid", id: Article.getActive()};
xhr.json("backend.php", query, (reply) => {
let title;

View File

@ -104,7 +104,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
menu.addChild(new dijit.MenuItem({
label: __("Open site"),
onClick: function() {
App.postOpenWindow("backend.php", {op: "feeds", method: "opensite",
App.postOpenWindow("backend.php", {op: "Feeds", method: "opensite",
feed_id: this.getParent().row_id, csrf_token: __csrf_token});
}}));
@ -114,7 +114,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
label: __("Debug feed"),
onClick: function() {
/* global __csrf_token */
App.postOpenWindow("backend.php", {op: "feeds", method: "updatedebugger",
App.postOpenWindow("backend.php", {op: "Feeds", method: "updatedebugger",
feed_id: this.getParent().row_id, csrf_token: __csrf_token});
}}));
}

View File

@ -23,6 +23,7 @@ const Feeds = {
infscroll_in_progress: 0,
infscroll_disabled: 0,
_infscroll_timeout: false,
_filter_query: false, // TODO: figure out the UI for this
_search_query: false,
last_search_query: [],
_viewfeed_wait_timeout: false,
@ -154,13 +155,17 @@ const Feeds = {
toggle: function() {
Element.toggle("feeds-holder");
},
cancelFilter: function() {
this._filter_query = "";
this.reload();
},
cancelSearch: function() {
this._search_query = "";
this.reloadCurrent();
},
// null = get all data, [] would give empty response for specific type
requestCounters: function(feed_ids = null, label_ids = null) {
xhr.json("backend.php", {op: "rpc",
xhr.json("backend.php", {op: "RPC",
method: "getAllCounters",
"feed_ids[]": feed_ids,
"feed_id_count": feed_ids ? feed_ids.length : -1,
@ -178,8 +183,14 @@ const Feeds = {
dijit.byId("feedTree").destroyRecursive();
}
let query = {op: 'Pref_Feeds', method: 'getfeedtree', mode: 2};
if (this._filter_query) {
query = Object.assign(query, this._filter_query);
}
const store = new dojo.data.ItemFileWriteStore({
url: "backend.php?op=pref_feeds&method=getfeedtree&mode=2"
url: "backend.php?" + dojo.objectToQuery(query)
});
// noinspection JSUnresolvedFunction
@ -347,7 +358,7 @@ const Feeds = {
toggleUnread: function() {
const hide = !App.getInitParam("hide_read_feeds");
xhr.post("backend.php", {op: "rpc", method: "setpref", key: "HIDE_READ_FEEDS", value: hide}, () => {
xhr.post("backend.php", {op: "RPC", method: "setpref", key: "HIDE_READ_FEEDS", value: hide}, () => {
this.hideOrShowFeeds(hide);
App.setInitParam("hide_read_feeds", hide);
});
@ -386,7 +397,7 @@ const Feeds = {
}, 10 * 1000);
}
let query = {...{op: "feeds", method: "view", feed: feed}, ...dojo.formToObject("toolbar-main")};
let query = {...{op: "Feeds", method: "view", feed: feed}, ...dojo.formToObject("toolbar-main")};
if (method) query.m = method;
@ -435,7 +446,7 @@ const Feeds = {
Notify.progress("Marking all feeds as read...");
xhr.json("backend.php", {op: "feeds", method: "catchupAll"}, () => {
xhr.json("backend.php", {op: "Feeds", method: "catchupAll"}, () => {
this.reloadCurrent();
});
@ -473,7 +484,7 @@ const Feeds = {
}
const catchup_query = {
op: 'rpc', method: 'catchupFeed', feed_id: feed,
op: 'RPC', method: 'catchupFeed', feed_id: feed,
is_cat: is_cat, mode: mode, search_query: this.last_search_query[0],
search_lang: this.last_search_query[1]
};
@ -612,7 +623,7 @@ const Feeds = {
},
search: function() {
xhr.json("backend.php",
{op: "feeds", method: "search"},
{op: "Feeds", method: "search"},
(reply) => {
try {
const dialog = new fox.SingleUseDialog({
@ -682,11 +693,53 @@ const Feeds = {
}
});
},
filter: function() {
const dialog = new fox.SingleUseDialog({
content: `
<form onsubmit='return false'>
<section>
<fieldset>
<input dojoType='dijit.form.ValidationTextBox' id='filter_query'
style='font-size : 16px; width : 540px;'
placeHolder="${__("Show feeds matching...")}"
name='search' type='search' value=''>
</fieldset>
</section>
<footer>
${App.FormFields.submit_tag(App.FormFields.icon("search") + " " + __('Search'), {onclick: "App.dialogOf(this).execute()"})}
${App.FormFields.cancel_dialog_tag(__('Cancel'))}
</footer>
</form>
`,
title: __("Search feeds"),
execute: function () {
if (this.validate()) {
Feeds._filter_query = this.attr('value');
this.hide();
Feeds.reload();
}
},
});
const tmph = dojo.connect(dialog, 'onShow', function () {
dojo.disconnect(tmph);
if (Feeds._filter_query && Feeds._filter_query.search) {
dijit.byId('filter_query')
.attr('value', Feeds._filter_query.search);
}
});
dialog.show();
},
updateRandom: function() {
console.log("in update_random_feed");
xhr.json("backend.php", {op: "rpc", method: "updaterandomfeed"}, () => {
xhr.json("backend.php", {op: "RPC", method: "updaterandomfeed"}, () => {
//
});
},

View File

@ -160,26 +160,26 @@ const Headlines = {
if (ops.tmark.length != 0)
promises.push(xhr.post("backend.php",
{op: "rpc", method: "markSelected", "ids[]": ops.tmark, cmode: 2}));
{op: "RPC", method: "markSelected", "ids[]": ops.tmark, cmode: 2}));
if (ops.tpub.length != 0)
promises.push(xhr.post("backend.php",
{op: "rpc", method: "publishSelected", "ids[]": ops.tpub, cmode: 2}));
{op: "RPC", method: "publishSelected", "ids[]": ops.tpub, cmode: 2}));
if (ops.read.length != 0)
promises.push(xhr.post("backend.php",
{op: "rpc", method: "catchupSelected", "ids[]": ops.read, cmode: 0}));
{op: "RPC", method: "catchupSelected", "ids[]": ops.read, cmode: 0}));
if (ops.unread.length != 0)
promises.push(xhr.post("backend.php",
{op: "rpc", method: "catchupSelected", "ids[]": ops.unread, cmode: 1}));
{op: "RPC", method: "catchupSelected", "ids[]": ops.unread, cmode: 1}));
const scores = Object.keys(ops.rescore);
if (scores.length != 0) {
scores.forEach((score) => {
promises.push(xhr.post("backend.php",
{op: "article", method: "setScore", "ids[]": ops.rescore[score], score: score}));
{op: "Article", method: "setScore", "ids[]": ops.rescore[score], score: score}));
});
}
@ -1132,7 +1132,7 @@ const Headlines = {
}
const query = {
op: "article", method: "removeFromLabel",
op: "Article", method: "removeFromLabel",
ids: ids.toString(), lid: id
};
@ -1149,7 +1149,7 @@ const Headlines = {
}
const query = {
op: "article", method: "assignToLabel",
op: "Article", method: "assignToLabel",
ids: ids.toString(), lid: id
};
@ -1181,7 +1181,7 @@ const Headlines = {
return;
}
const query = {op: "rpc", method: "delete", ids: rows.toString()};
const query = {op: "RPC", method: "delete", ids: rows.toString()};
xhr.json("backend.php", query, () => {
Feeds.reloadCurrent();
@ -1586,7 +1586,7 @@ const Headlines = {
menu.addChild(new dijit.MenuItem({
label: __("Open site"),
onClick: function() {
App.postOpenWindow("backend.php", {op: "feeds", method: "opensite",
App.postOpenWindow("backend.php", {op: "Feeds", method: "opensite",
feed_id: this.getParent().currentTarget.getAttribute("data-feed-id"), csrf_token: __csrf_token});
}}));
@ -1596,7 +1596,7 @@ const Headlines = {
label: __("Debug feed"),
onClick: function() {
/* global __csrf_token */
App.postOpenWindow("backend.php", {op: "feeds", method: "updatedebugger",
App.postOpenWindow("backend.php", {op: "Feeds", method: "updatedebugger",
feed_id: this.getParent().currentTarget.getAttribute("data-feed-id"), csrf_token: __csrf_token});
}}));

View File

@ -8,7 +8,7 @@ define(["dojo/_base/declare", "dojo/data/ItemFileWriteStore"], function (declare
dojo.xhrPost({
url: "backend.php",
content: {op: "pref-feeds", method: "savefeedorder",
content: {op: "Pref_Feeds", method: "savefeedorder",
payload: newFileContentString},
error: saveFailedCallback,
load: saveCompleteCallback});

View File

@ -150,7 +150,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
const searchElem = App.byId("feed_search");
const search = (searchElem) ? searchElem.value : "";
xhr.post("backend.php", { op: "pref-feeds", search: search }, (reply) => {
xhr.post("backend.php", { op: "Pref_Feeds", search: search }, (reply) => {
dijit.byId('feedsTab').attr('content', reply);
Notify.close();
});
@ -185,14 +185,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
resetFeedOrder: function() {
Notify.progress("Loading, please wait...");
xhr.post("backend.php", {op: "pref-feeds", method: "feedsortreset"}, () => {
xhr.post("backend.php", {op: "Pref_Feeds", method: "feedsortreset"}, () => {
this.reload();
});
},
resetCatOrder: function() {
Notify.progress("Loading, please wait...");
xhr.post("backend.php", {op: "pref-feeds", method: "catsortreset"}, () => {
xhr.post("backend.php", {op: "Pref_Feeds", method: "catsortreset"}, () => {
this.reload();
});
},
@ -200,7 +200,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
if (confirm(__("Remove category %s? Any nested feeds would be placed into Uncategorized.").replace("%s", item.name))) {
Notify.progress("Removing category...");
xhr.post("backend.php", {op: "pref-feeds", method: "removeCat", ids: id}, () => {
xhr.post("backend.php", {op: "Pref_Feeds", method: "removeCat", ids: id}, () => {
Notify.close();
this.reload();
});
@ -215,7 +215,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
Notify.progress("Unsubscribing from selected feeds...", true);
const query = {
op: "pref-feeds", method: "remove",
op: "Pref_Feeds", method: "remove",
ids: sel_rows.toString()
};
@ -231,14 +231,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
return false;
},
checkErrorFeeds: function() {
xhr.json("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (reply) => {
xhr.json("backend.php", {op: "Pref_Feeds", method: "feedsWithErrors"}, (reply) => {
if (reply.length > 0) {
Element.show(dijit.byId("pref_feeds_errors_btn").domNode);
}
});
},
checkInactiveFeeds: function() {
xhr.json("backend.php", {op: "pref-feeds", method: "inactivefeeds"}, (reply) => {
xhr.json("backend.php", {op: "Pref_Feeds", method: "inactivefeeds"}, (reply) => {
if (reply.length > 0) {
Element.show(dijit.byId("pref_feeds_inactive_btn").domNode);
}
@ -264,7 +264,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
Notify.progress("Removing selected categories...");
const query = {
op: "pref-feeds", method: "removeCat",
op: "Pref_Feeds", method: "removeCat",
ids: sel_rows.toString()
};
@ -316,7 +316,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
Notify.progress("Loading, please wait...");
xhr.post("backend.php", {op: "pref-feeds", method: "editfeeds", ids: rows.toString()}, (reply) => {
xhr.post("backend.php", {op: "Pref_Feeds", method: "editfeeds", ids: rows.toString()}, (reply) => {
Notify.close();
try {
@ -393,7 +393,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
Notify.progress("Loading, please wait...");
xhr.post("backend.php", { op: 'pref-feeds', method: 'renamecat', id: id, title: new_name }, () => {
xhr.post("backend.php", { op: 'Pref_Feeds', method: 'renamecat', id: id, title: new_name }, () => {
this.reload();
});
}
@ -404,14 +404,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
if (title) {
Notify.progress("Creating category...");
xhr.post("backend.php", {op: "pref-feeds", method: "addCat", cat: title}, () => {
xhr.post("backend.php", {op: "Pref_Feeds", method: "addCat", cat: title}, () => {
Notify.close();
this.reload();
});
}
},
batchSubscribe: function() {
xhr.json("backend.php", {op: 'pref-feeds', method: 'batchSubscribe'}, (reply) => {
xhr.json("backend.php", {op: 'Pref_Feeds', method: 'batchSubscribe'}, (reply) => {
const dialog = new fox.SingleUseDialog({
id: "batchSubDlg",
title: __("Batch subscribe"),
@ -431,7 +431,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
},
content: `
<form onsubmit='return false'>
${App.FormFields.hidden_tag("op", "pref-feeds")}
${App.FormFields.hidden_tag("op", "Pref_Feeds")}
${App.FormFields.hidden_tag("method", "batchaddfeeds")}
<header class='horizontal'>
@ -484,7 +484,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
});
},
showInactiveFeeds: function() {
xhr.json("backend.php", {op: 'pref-feeds', method: 'inactivefeeds'}, function (reply) {
xhr.json("backend.php", {op: 'Pref_Feeds', method: 'inactivefeeds'}, function (reply) {
const dialog = new fox.SingleUseDialog({
id: "inactiveFeedsDlg",
@ -500,7 +500,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
Notify.progress("Removing selected feeds...", true);
const query = {
op: "pref-feeds", method: "remove",
op: "Pref_Feeds", method: "remove",
ids: sel_rows.toString()
};

View File

@ -9,7 +9,7 @@ define(["dojo/_base/declare", "dojo/data/ItemFileWriteStore"], function (declare
dojo.xhrPost({
url: "backend.php",
content: {
op: "pref-filters", method: "savefilterorder",
op: "Pref_Filters", method: "savefilterorder",
payload: newFileContentString
},
error: saveFailedCallback,

View File

@ -107,7 +107,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
let search = "";
if (user_search) { search = user_search.value; }
xhr.post("backend.php", { op: "pref-filters", search: search }, (reply) => {
xhr.post("backend.php", { op: "Pref_Filters", search: search }, (reply) => {
dijit.byId('filtersTab').attr('content', reply);
Notify.close();
});
@ -125,7 +125,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
resetFilterOrder: function() {
Notify.progress("Loading, please wait...");
xhr.post("backend.php", {op: "pref-filters", method: "filtersortreset"}, () => {
xhr.post("backend.php", {op: "Pref_Filters", method: "filtersortreset"}, () => {
this.reload();
});
},
@ -140,7 +140,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
if (confirm(__("Combine selected filters?"))) {
Notify.progress("Joining filters...");
xhr.post("backend.php", {op: "pref-filters", method: "join", ids: rows.toString()}, () => {
xhr.post("backend.php", {op: "Pref_Filters", method: "join", ids: rows.toString()}, () => {
this.reload();
});
}
@ -153,7 +153,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
Notify.progress("Removing selected filters...");
const query = {
op: "pref-filters", method: "remove",
op: "Pref_Filters", method: "remove",
ids: sel_rows.toString()
};

View File

@ -19,7 +19,7 @@ const Helpers = {
alert("No passwords selected.");
} else if (confirm(__("Remove selected app passwords?"))) {
xhr.post("backend.php", {op: "pref-prefs", method: "deleteAppPasswords", "ids[]": rows}, (reply) => {
xhr.post("backend.php", {op: "Pref_Prefs", method: "deleteAppPasswords", "ids[]": rows}, (reply) => {
this.updateContent(reply);
Notify.close();
});
@ -31,7 +31,7 @@ const Helpers = {
const title = prompt("Password description:")
if (title) {
xhr.post("backend.php", {op: "pref-prefs", method: "generateAppPassword", title: title}, (reply) => {
xhr.post("backend.php", {op: "Pref_Prefs", method: "generateAppPassword", title: title}, (reply) => {
this.updateContent(reply);
Notify.close();
});
@ -45,7 +45,7 @@ const Helpers = {
if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) {
Notify.progress("Clearing URLs...");
xhr.post("backend.php", {op: "pref-feeds", method: "clearKeys"}, () => {
xhr.post("backend.php", {op: "Pref_Feeds", method: "clearKeys"}, () => {
Notify.info("Generated URLs cleared.");
});
}
@ -71,7 +71,7 @@ const Helpers = {
const tmph = dojo.connect(dialog, 'onShow', function () {
dojo.disconnect(tmph);
xhr.json("backend.php", {op: "pref-prefs", method: "previewDigest"}, (reply) => {
xhr.json("backend.php", {op: "Pref_Prefs", method: "previewDigest"}, (reply) => {
dialog.domNode.querySelector('.digest-preview').innerHTML = reply[0];
});
});
@ -91,7 +91,7 @@ const Helpers = {
},
update: function() {
xhr.post("backend.php", {
op: "pref-system",
op: "Pref_System",
severity: dijit.byId("severity").attr('value'),
page: Helpers.EventLog.log_page
}, (reply) => {
@ -114,7 +114,7 @@ const Helpers = {
Notify.progress("Loading, please wait...");
xhr.post("backend.php", {op: "pref-system", method: "clearLog"}, () => {
xhr.post("backend.php", {op: "Pref_System", method: "clearLog"}, () => {
Helpers.EventLog.refresh();
});
}
@ -135,7 +135,7 @@ const Helpers = {
const new_title = prompt(__("Name for cloned profile:"));
if (new_title) {
xhr.post("backend.php", {op: "pref-prefs", method: "cloneprofile", "new_title": new_title, "old_profile": sel_rows[0]}, () => {
xhr.post("backend.php", {op: "Pref_Prefs", method: "cloneprofile", "new_title": new_title, "old_profile": sel_rows[0]}, () => {
Notify.close();
dialog.refresh();
});
@ -153,7 +153,7 @@ const Helpers = {
if (confirm(__("Remove selected profiles? Active and default profiles will not be removed."))) {
Notify.progress("Removing selected profiles...", true);
xhr.post("backend.php", {op: "pref-prefs", method: "remprofiles", "ids[]": sel_rows}, () => {
xhr.post("backend.php", {op: "Pref_Prefs", method: "remprofiles", "ids[]": sel_rows}, () => {
Notify.close();
dialog.refresh();
});
@ -167,7 +167,7 @@ const Helpers = {
if (this.validate()) {
Notify.progress("Creating profile...", true);
const query = {op: "pref-prefs", method: "addprofile", title: dialog.attr('value').newprofile};
const query = {op: "Pref_Prefs", method: "addprofile", title: dialog.attr('value').newprofile};
xhr.post("backend.php", query, () => {
Notify.close();
@ -177,7 +177,7 @@ const Helpers = {
}
},
refresh: function() {
xhr.json("backend.php", {op: 'pref-prefs', method: 'getprofiles'}, (reply) => {
xhr.json("backend.php", {op: 'Pref_Prefs', method: 'getprofiles'}, (reply) => {
dialog.attr('content', `
<div dojoType='fox.Toolbar'>
<div dojoType='fox.form.DropDownButton'>
@ -210,7 +210,7 @@ const Helpers = {
profile-id='${profile.id}'>${profile.title}
<script type='dojo/method' event='onChange' args='value'>
xhr.post("backend.php",
{op: 'pref-prefs', method: 'saveprofile', value: value, id: this.attr('profile-id')}, () => {
{op: 'Pref_Prefs', method: 'saveprofile', value: value, id: this.attr('profile-id')}, () => {
//
});
</script>
@ -242,7 +242,7 @@ const Helpers = {
if (confirm(__("Activate selected profile?"))) {
Notify.progress("Loading, please wait...");
xhr.post("backend.php", {op: "pref-prefs", method: "activateprofile", id: sel_rows.toString()}, () => {
xhr.post("backend.php", {op: "Pref_Prefs", method: "activateprofile", id: sel_rows.toString()}, () => {
window.location.reload();
});
}
@ -280,7 +280,7 @@ const Helpers = {
${__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here.")}
</div>
${App.FormFields.hidden_tag('op', 'rpc')}
${App.FormFields.hidden_tag('op', 'RPC')}
${App.FormFields.hidden_tag('method', 'setpref')}
${App.FormFields.hidden_tag('key', 'USER_STYLESHEET')}
@ -312,7 +312,7 @@ const Helpers = {
const tmph = dojo.connect(dialog, 'onShow', function () {
dojo.disconnect(tmph);
xhr.json("backend.php", {op: "pref-prefs", method: "customizeCSS"}, (reply) => {
xhr.json("backend.php", {op: "Pref_Prefs", method: "customizeCSS"}, (reply) => {
const editor = dijit.getEnclosingWidget(dialog.domNode.querySelector(".user-css-editor"));
@ -327,14 +327,14 @@ const Helpers = {
},
confirmReset: function() {
if (confirm(__("Reset to defaults?"))) {
xhr.post("backend.php", {op: "pref-prefs", method: "resetconfig"}, (reply) => {
xhr.post("backend.php", {op: "Pref_Prefs", method: "resetconfig"}, (reply) => {
Helpers.Prefs.refresh();
Notify.info(reply);
});
}
},
refresh: function() {
xhr.post("backend.php", { op: "pref-prefs" }, (reply) => {
xhr.post("backend.php", { op: "Pref_Prefs" }, (reply) => {
dijit.byId('prefsTab').attr('content', reply);
Notify.close();
});
@ -360,7 +360,7 @@ const Helpers = {
this.render_contents();
},
reload: function() {
xhr.json("backend.php", {op: "pref-prefs", method: "getPluginsList"}, (reply) => {
xhr.json("backend.php", {op: "Pref_Prefs", method: "getPluginsList"}, (reply) => {
this._list_of_plugins = reply;
this.render_contents();
}, (e) => {
@ -444,7 +444,7 @@ const Helpers = {
if (confirm(__("Clear stored data for %s?").replace("%s", name))) {
Notify.progress("Loading, please wait...");
xhr.post("backend.php", {op: "pref-prefs", method: "clearPluginData", name: name}, () => {
xhr.post("backend.php", {op: "Pref_Prefs", method: "clearPluginData", name: name}, () => {
Helpers.Prefs.refresh();
});
}
@ -455,7 +455,7 @@ const Helpers = {
if (confirm(msg)) {
Notify.progress("Loading, please wait...");
xhr.json("backend.php", {op: "pref-prefs", method: "uninstallPlugin", plugin: plugin}, (reply) => {
xhr.json("backend.php", {op: "Pref_Prefs", method: "uninstallPlugin", plugin: plugin}, (reply) => {
if (reply && reply.status == 1)
Helpers.Plugins.reload();
else {
@ -504,7 +504,7 @@ const Helpers = {
const container = install_dialog.domNode.querySelector(".contents");
xhr.json("backend.php", {op: "pref-prefs", method: "installPlugin", plugin: plugin}, (reply) => {
xhr.json("backend.php", {op: "Pref_Prefs", method: "installPlugin", plugin: plugin}, (reply) => {
if (!reply) {
container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`;
} else {
@ -603,7 +603,7 @@ const Helpers = {
const container = dialog.domNode.querySelector(".contents");
container.innerHTML = `<li class='text-center'>${__("Looking for plugins...")}</li>`;
xhr.json("backend.php", {op: "pref-prefs", method: "getAvailablePlugins"}, (reply) => {
xhr.json("backend.php", {op: "Pref_Prefs", method: "getAvailablePlugins"}, (reply) => {
dialog.entries = reply;
dialog.render_contents();
});
@ -656,7 +656,7 @@ const Helpers = {
container.innerHTML = `<li class='text-center'>${__("Updating, please wait...")}</li>`;
let enable_update_btn = false;
xhr.json("backend.php", {op: "pref-prefs", method: "updateLocalPlugins", plugins: dialog.plugins_to_update.join(",")}, (reply) => {
xhr.json("backend.php", {op: "Pref_Prefs", method: "updateLocalPlugins", plugins: dialog.plugins_to_update.join(",")}, (reply) => {
if (!reply) {
container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`;
@ -717,7 +717,7 @@ const Helpers = {
//container.innerHTML = `<li class='text-center'>${__("Checking: %s...").replace("%s", name)}</li>`;
xhr.json("backend.php", {op: "pref-prefs", method: "checkForPluginUpdates", name: name}, (reply) => {
xhr.json("backend.php", {op: "Pref_Prefs", method: "checkForPluginUpdates", name: name}, (reply) => {
if (!reply) {
container.innerHTML += `<li class='text-error'>${__("%s: Operation failed: check event log.").replace("%s", name)}</li>`;
@ -834,7 +834,7 @@ const Helpers = {
},
export: function() {
console.log("export");
window.open("backend.php?op=opml&method=export&" + dojo.formToQuery("opmlExportForm"));
window.open("backend.php?op=OPML&method=export&" + dojo.formToQuery("opmlExportForm"));
},
}
};

View File

@ -55,13 +55,13 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
return rv;
},
reload: function() {
xhr.post("backend.php", { op: "pref-labels" }, (reply) => {
xhr.post("backend.php", { op: "Pref_Labels" }, (reply) => {
dijit.byId('labelsTab').attr('content', reply);
Notify.close();
});
},
editLabel: function(id) {
xhr.json("backend.php", {op: "pref-labels", method: "edit", id: id}, (reply) => {
xhr.json("backend.php", {op: "Pref_Labels", method: "edit", id: id}, (reply) => {
const fg_color = reply['fg_color'];
const bg_color = reply['bg_color'] ? reply['bg_color'] : '#fff7d5';
@ -91,7 +91,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
}
const query = {
op: "pref-labels", method: "colorset", kind: kind,
op: "Pref_Labels", method: "colorset", kind: kind,
ids: id, fg: fg, bg: bg, color: color
};
@ -131,7 +131,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
</section>
${App.FormFields.hidden_tag('id', id)}
${App.FormFields.hidden_tag('op', 'pref-labels')}
${App.FormFields.hidden_tag('op', 'Pref_Labels')}
${App.FormFields.hidden_tag('method', 'save')}
${App.FormFields.hidden_tag('fg_color', fg_color, {}, 'labelEdit_fgColor')}
@ -189,7 +189,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
if (confirm(__("Reset selected labels to default colors?"))) {
const query = {
op: "pref-labels", method: "colorreset",
op: "Pref_Labels", method: "colorreset",
ids: labels.toString()
};
@ -210,7 +210,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
Notify.progress("Removing selected labels...");
const query = {
op: "pref-labels", method: "remove",
op: "Pref_Labels", method: "remove",
ids: sel_rows.toString()
};

View File

@ -8,7 +8,7 @@ const Users = {
const user_search = App.byId("user_search");
const search = user_search ? user_search.value : "";
xhr.post("backend.php", { op: "pref-users", sort: sort, search: search }, (reply) => {
xhr.post("backend.php", { op: "Pref_Users", sort: sort, search: search }, (reply) => {
dijit.byId('usersTab').attr('content', reply);
Notify.close();
resolve();
@ -21,7 +21,7 @@ const Users = {
if (login) {
Notify.progress("Adding user...");
xhr.post("backend.php", {op: "pref-users", method: "add", login: login}, (reply) => {
xhr.post("backend.php", {op: "Pref_Users", method: "add", login: login}, (reply) => {
Users.reload().then(() => {
Notify.info(reply);
})
@ -30,7 +30,7 @@ const Users = {
}
},
edit: function(id) {
xhr.json('backend.php', {op: 'pref-users', method: 'edit', id: id}, (reply) => {
xhr.json('backend.php', {op: 'Pref_Users', method: 'edit', id: id}, (reply) => {
const user = reply.user;
const admin_disabled = (user.id == 1);
@ -53,7 +53,7 @@ const Users = {
<form onsubmit='return false'>
${App.FormFields.hidden_tag('id', user.id.toString())}
${App.FormFields.hidden_tag('op', 'pref-users')}
${App.FormFields.hidden_tag('op', 'Pref_Users')}
${App.FormFields.hidden_tag('method', 'editSave')}
<div dojoType="dijit.layout.TabContainer" style="height : 400px">
@ -104,7 +104,7 @@ const Users = {
<div dojoType="dijit.layout.ContentPane" title="${__('User details')}">
<script type='dojo/method' event='onShow' args='evt'>
if (this.domNode.querySelector('.loading')) {
xhr.post("backend.php", {op: 'pref-users', method: 'userdetails', id: ${user.id}}, (reply) => {
xhr.post("backend.php", {op: 'Pref_Users', method: 'userdetails', id: ${user.id}}, (reply) => {
this.attr('content', reply);
});
}
@ -147,7 +147,7 @@ const Users = {
const id = rows[0];
xhr.post("backend.php", {op: "pref-users", method: "resetPass", id: id}, (reply) => {
xhr.post("backend.php", {op: "Pref_Users", method: "resetPass", id: id}, (reply) => {
Notify.close();
Notify.info(reply, true);
});
@ -162,7 +162,7 @@ const Users = {
Notify.progress("Removing selected users...");
const query = {
op: "pref-users", method: "remove",
op: "Pref_Users", method: "remove",
ids: sel_rows.toString()
};

0
lock/.empty Executable file → Normal file
View File

View File

@ -1,5 +1,6 @@
parameters:
level: 6
tmpDir: .phpstan-tmp
parallel:
maximumNumberOfProcesses: 4
reportUnmatchedIgnoredErrors: false
@ -13,19 +14,20 @@ parameters:
- plugins/*/vendor/*
- plugins.local/*/vendor/*
excludePaths:
- node_modules/*
- vendor/**/tests/*
- vendor/**/test/*
- vendor/sebastian/*
- lib/dojo-src/*
- lib/**/tests/*
- lib/**/test/*
- plugins/**/tests/*
- plugins/**/Test/*
- plugins.local/**/tests/*
- plugins/**/test/*
- lib/**/tests/*
- lib/dojo-src/*
- node_modules/*
- plugins.local/**/test/*
- plugins.local/**/tests/*
- plugins.local/*/vendor/intervention/*
- plugins.local/*/vendor/psr/log/*
- plugins.local/cache_s3/vendor/*
- plugins/**/test/*
- plugins/**/Test/*
- plugins/**/tests/*
- plugins/*/vendor/intervention/*
- plugins/*/vendor/psr/log/*
- vendor/**/*
paths:
- .

View File

@ -10,9 +10,10 @@ class Af_Comics_Cad extends Af_ComicFilter {
if (strpos($article["title"], "News:") === false) {
$doc = new DOMDocument();
$res = UrlHelper::fetch($article["link"], false, false, false,
false, false, 0,
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");
$res = UrlHelper::fetch([
'url' => $article['link'],
'useragent' => 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0',
]);
if (!$res && UrlHelper::$fetch_last_error_content)
$res = UrlHelper::$fetch_last_error_content;

View File

@ -11,9 +11,10 @@ class Af_Comics_ComicClass extends Af_ComicFilter {
// lol at people who block clients by user agent
// oh noes my ad revenue Q_Q
$res = UrlHelper::fetch($article["link"], false, false, false,
false, false, 0,
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)");
$res = UrlHelper::fetch([
'url' => $article['link'],
'useragent' => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)',
]);
$doc = new DOMDocument();

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