Compare commits

..

No commits in common. "a86df7eac8307b8aa1a253459d8504c7abfa2de4" and "dc25a9cf6816b756cb38490eab93f02589c44a10" have entirely different histories.

1362 changed files with 25862 additions and 123809 deletions

View File

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

View File

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

View File

@ -21,7 +21,7 @@ while ! pg_isready -h $TTRSS_DB_HOST -U $TTRSS_DB_USER; do
done done
sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \ sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \
/etc/php83/php.ini /etc/php82/php.ini
DST_DIR=/var/www/html/tt-rss 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 sleep 3
done done
sudo -E -u app /usr/bin/php83 /var/www/html/tt-rss/update_daemon2.php sudo -E -u app /usr/bin/php82 /var/www/html/tt-rss/update_daemon2.php

View File

@ -1,27 +1,13 @@
ARG PROXY_REGISTRY FROM registry.fakecake.org/docker.io/nginx:alpine
FROM ${PROXY_REGISTRY}nginx:alpine
HEALTHCHECK CMD curl --fail http://localhost${APP_BASE}/index.php || exit 1 HEALTHCHECK CMD curl --fail http://localhost/tt-rss/index.php || exit 1
COPY .docker/web-nginx/nginx.conf /etc/nginx/templates/nginx.conf.template COPY nginx.conf /etc/nginx/templates/nginx.conf.template
# By default, nginx will send the php requests to "app" server, but this server # 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 # name can be overridden at runtime by passing an APP_UPSTREAM env var
ENV APP_UPSTREAM=${APP_UPSTREAM:-app} 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 # 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 # 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) # and put the result in /etc/nginx/nginx.conf (instead of /etc/nginx/conf.d/nginx.conf)

View File

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

View File

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

View File

@ -1,47 +0,0 @@
# 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,6 +1,4 @@
Thumbs.db Thumbs.db
/.env
/docker-compose.override.yml
/.app_is_ready /.app_is_ready
/messages.mo /messages.mo
/node_modules /node_modules
@ -14,5 +12,3 @@ Thumbs.db
/.vscode/settings.json /.vscode/settings.json
/vendor/**/.git /vendor/**/.git
/.phpunit.result.cache /.phpunit.result.cache
/.phpstan-tmp
/.tools/

View File

@ -1,28 +1,20 @@
stages: stages:
- lint - lint
- build - build
- test
- publish
variables: variables:
ESLINT_PATHS: js plugins ESLINT_PATHS: js plugins
REGISTRY_PROJECT: cthulhoo
include: include:
- project: 'ci/ci-templates' - project: 'ci/ci-templates'
ref: master ref: master
file: .ci-build-docker-kaniko.yml file: .ci-build-docker.yml
- project: 'ci/ci-templates' - project: 'ci/ci-templates'
ref: master ref: master
file: .ci-lint-common.yml file: .ci-lint-common.yml
- project: 'ci/ci-templates'
ref: master
file: .ci-integration-test.yml
phpunit: phpunit:
extends: .phpunit extends: .phpunit
variables:
PHPUNIT_ARGS: --exclude integration --coverage-filter classes --coverage-filter include
eslint: eslint:
extends: .eslint extends: .eslint
@ -30,96 +22,39 @@ eslint:
phpstan: phpstan:
extends: .phpstan extends: .phpstan
ttrss-web-nginx:
extends: .build-master
variables:
BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/web-nginx
ttrss-web-nginx:branch: ttrss-web-nginx:branch:
extends: .build-branch extends: .build-branch
variables: variables:
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/web-nginx/Dockerfile BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/web-nginx
ttrss-fpm-pgsql-static:
extends: .build-master
variables:
BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/app
ttrss-fpm-pgsql-static:branch: ttrss-fpm-pgsql-static:branch:
extends: .build-branch extends: .build-branch
variables: variables:
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/app/Dockerfile BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/app
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: phpdoc:
image: ${PHP_IMAGE} image:
stage: publish name: ${CI_DOCKER_IMAGE}
stage: build
rules: 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 - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $PHPDOC_DEPLOY_SSH_KEY != null
when: manual
script: script:
- php83 /phpDocumentor.phar -d classes -d include -t phpdoc --visibility=public - php81 /phpDocumentor.phar -d classes -d include -t phpdoc --visibility=public
- mkdir -p ~/.ssh && - mkdir -p ~/.ssh &&
cp ${PHPDOC_DEPLOY_SSH_KEY} ~/.ssh/id_ed25519 && cp ${PHPDOC_DEPLOY_SSH_KEY} ~/.ssh/id_ed25519 &&
chmod 0600 ~/.ssh/id_ed25519 chmod 0600 ~/.ssh/id_ed25519
- rsync -av -e 'ssh -o StrictHostKeyChecking=no' phpdoc/ ${PHPDOC_DEPLOY_HOST}:phpdoc/ - 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"

View File

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

View File

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

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

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

View File

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

View File

@ -1,216 +0,0 @@
<?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;
}
}

View File

@ -1,505 +0,0 @@
<?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;
}
}

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

@ -691,6 +691,20 @@ 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 = []; $qfh_ret = [];
if (!$is_cat && is_numeric($feed_id) && $feed_id < PLUGIN_FEED_BASE_INDEX && $feed_id > LABEL_BASE_INDEX) { 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 Normal file → Executable file
View File

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

View File

@ -46,7 +46,10 @@ class Config {
* garbage unicode characters with this option, try setting it to a blank string. */ * garbage unicode characters with this option, try setting it to a blank string. */
const MYSQL_CHARSET = "MYSQL_CHARSET"; const MYSQL_CHARSET = "MYSQL_CHARSET";
/** this is a fallback falue for the CLI SAPI, it should be set to a fully-qualified tt-rss URL */ /** 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 */
const SELF_URL_PATH = "SELF_URL_PATH"; const SELF_URL_PATH = "SELF_URL_PATH";
/** operate in single user mode, disables all functionality related to /** operate in single user mode, disables all functionality related to
@ -190,10 +193,10 @@ class Config {
const HTTP_429_THROTTLE_INTERVAL = "HTTP_429_THROTTLE_INTERVAL"; const HTTP_429_THROTTLE_INTERVAL = "HTTP_429_THROTTLE_INTERVAL";
/** host running Jaeger collector to receive traces (disabled if empty) */ /** host running Jaeger collector to receive traces (disabled if empty) */
const OPENTELEMETRY_ENDPOINT = "OPENTELEMETRY_ENDPOINT"; const JAEGER_REPORTING_HOST = "JAEGER_REPORTING_HOST";
/** Jaeger service name */ /** Jaeger service name */
const OPENTELEMETRY_SERVICE = "OPENTELEMETRY_SERVICE"; const JAEGER_SERVICE_NAME = "JAEGER_SERVICE_NAME";
/** default values for all global configuration options */ /** default values for all global configuration options */
private const _DEFAULTS = [ private const _DEFAULTS = [
@ -204,7 +207,7 @@ class Config {
Config::DB_PASS => [ "", Config::T_STRING ], Config::DB_PASS => [ "", Config::T_STRING ],
Config::DB_PORT => [ "5432", Config::T_STRING ], Config::DB_PORT => [ "5432", Config::T_STRING ],
Config::MYSQL_CHARSET => [ "UTF8", Config::T_STRING ], Config::MYSQL_CHARSET => [ "UTF8", Config::T_STRING ],
Config::SELF_URL_PATH => [ "https://example.com/tt-rss", Config::T_STRING ], Config::SELF_URL_PATH => [ "", Config::T_STRING ],
Config::SINGLE_USER_MODE => [ "", Config::T_BOOL ], Config::SINGLE_USER_MODE => [ "", Config::T_BOOL ],
Config::SIMPLE_UPDATE_MODE => [ "", Config::T_BOOL ], Config::SIMPLE_UPDATE_MODE => [ "", Config::T_BOOL ],
Config::PHP_EXECUTABLE => [ "/usr/bin/php", Config::T_STRING ], Config::PHP_EXECUTABLE => [ "/usr/bin/php", Config::T_STRING ],
@ -252,8 +255,8 @@ class Config {
Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)', Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)',
Config::T_STRING ], Config::T_STRING ],
Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ], Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ],
Config::OPENTELEMETRY_ENDPOINT => [ "", Config::T_STRING ], Config::JAEGER_REPORTING_HOST => [ "", Config::T_STRING ],
Config::OPENTELEMETRY_SERVICE => [ "tt-rss", Config::T_STRING ], Config::JAEGER_SERVICE_NAME => [ "tt-rss", Config::T_STRING ],
]; ];
/** @var Config|null */ /** @var Config|null */
@ -319,7 +322,7 @@ class Config {
* @return array<string, mixed>|string * @return array<string, mixed>|string
*/ */
private function _get_version(bool $as_string = true) { private function _get_version(bool $as_string = true) {
$root_dir = self::get_self_dir(); $root_dir = dirname(__DIR__);
if (empty($this->version)) { if (empty($this->version)) {
$this->version["status"] = -1; $this->version["status"] = -1;
@ -413,7 +416,7 @@ class Config {
private function _get_migrations() : Db_Migrations { private function _get_migrations() : Db_Migrations {
if (empty($this->migrations)) { if (empty($this->migrations)) {
$this->migrations = new Db_Migrations(); $this->migrations = new Db_Migrations();
$this->migrations->initialize(self::get_self_dir() . "/sql", "ttrss_version", true, self::SCHEMA_VERSION); $this->migrations->initialize(dirname(__DIR__) . "/sql", "ttrss_version", true, self::SCHEMA_VERSION);
} }
return $this->migrations; return $this->migrations;
@ -471,22 +474,24 @@ class Config {
return $instance->_get($param); 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 { static function is_server_https() : bool {
return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
(!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'); (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https');
} }
/** returns fully-qualified external URL to tt-rss (no trailing slash) /** generates reference self_url_path (no trailing slash) */
* SELF_URL_PATH configuration variable is used as a fallback for the CLI SAPI static function make_self_url() : string {
* */
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'; $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 = $proto . '://' . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$self_url_path = preg_replace("/(\/api\/{1,})?(\w+\.php)?(\?.*$)?$/", "", $self_url_path);
$self_url_path = preg_replace("/\w+\.php(\?.*$)?$/", "", $self_url_path);
#$self_url_path = preg_replace("/(\?.*$)?$/", "", $self_url_path);
if (substr($self_url_path, -1) === "/") { if (substr($self_url_path, -1) === "/") {
return substr($self_url_path, 0, -1); return substr($self_url_path, 0, -1);
@ -494,7 +499,7 @@ class Config {
return $self_url_path; return $self_url_path;
} }
} }
}
/* sanity check stuff */ /* sanity check stuff */
/** checks for mysql tables not using InnoDB (tt-rss is incompatible with MyISAM) /** checks for mysql tables not using InnoDB (tt-rss is incompatible with MyISAM)
@ -615,9 +620,27 @@ class Config {
// skip check for CLI scripts so that we could install database schema if it is missing. // skip check for CLI scripts so that we could install database schema if it is missing.
if (php_sapi_name() != "cli") { if (php_sapi_name() != "cli") {
if (self::get_schema_version() < 0) { 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>)"); 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") { if (self::get(Config::DB_TYPE) == "mysql") {
@ -643,9 +666,7 @@ class Config {
} }
} }
if (count($errors) > 0 && php_sapi_name() != "cli") { if (count($errors) > 0 && php_sapi_name() != "cli") { ?>
http_response_code(503); ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -703,9 +724,4 @@ class Config {
static function get_user_agent(): string { static function get_user_agent(): string {
return sprintf(self::get(self::HTTP_USER_AGENT), self::get_version()); 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>> * @return array<int, array<string, int|string>>
*/ */
private static function get_feeds(array $feed_ids = null): array { private static function get_feeds(array $feed_ids = null): array {
$span = Tracer::start(__METHOD__); $scope = Tracer::start(__METHOD__);
$ret = []; $ret = [];
@ -212,7 +212,7 @@ class Counters {
} }
$span->end(); $scope->close();
return $ret; return $ret;
} }
@ -221,7 +221,7 @@ class Counters {
* @return array<int, array<string, int|string>> * @return array<int, array<string, int|string>>
*/ */
private static function get_global(): array { private static function get_global(): array {
$span = Tracer::start(__METHOD__); $scope = Tracer::start(__METHOD__);
$ret = [ $ret = [
[ [
@ -239,7 +239,7 @@ class Counters {
"counter" => $subcribed_feeds "counter" => $subcribed_feeds
]); ]);
$span->end(); $scope->close();
return $ret; return $ret;
} }
@ -248,7 +248,7 @@ class Counters {
* @return array<int, array<string, int|string>> * @return array<int, array<string, int|string>>
*/ */
private static function get_virt(): array { private static function get_virt(): array {
$span = Tracer::start(__METHOD__); $scope = Tracer::start(__METHOD__);
$ret = []; $ret = [];
@ -295,7 +295,7 @@ class Counters {
} }
} }
$span->end(); $scope->close();
return $ret; return $ret;
} }
@ -304,7 +304,7 @@ class Counters {
* @return array<int, array<string, int|string>> * @return array<int, array<string, int|string>>
*/ */
static function get_labels(array $label_ids = null): array { static function get_labels(array $label_ids = null): array {
$span = Tracer::start(__METHOD__); $scope = Tracer::start(__METHOD__);
$ret = []; $ret = [];
@ -356,7 +356,7 @@ class Counters {
array_push($ret, $cv); array_push($ret, $cv);
} }
$span->end(); $scope->close();
return $ret; return $ret;
} }
} }

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

@ -12,7 +12,7 @@ class Mailer {
$to_address = $params["to_address"]; $to_address = $params["to_address"];
$subject = $params["subject"]; $subject = $params["subject"];
$message = $params["message"]; $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_name = $params["from_name"] ?? Config::get(Config::SMTP_FROM_NAME);
$from_address = $params["from_address"] ?? Config::get(Config::SMTP_FROM_ADDRESS); $from_address = $params["from_address"] ?? Config::get(Config::SMTP_FROM_ADDRESS);
$additional_headers = $params["headers"] ?? []; $additional_headers = $params["headers"] ?? [];

View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

69
classes/tracer.php Normal file
View File

@ -0,0 +1,69 @@
<?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;
}
}

654
classes/urlhelper.php Normal file
View File

@ -0,0 +1,654 @@
<?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

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

View File

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

View File

@ -1,59 +0,0 @@
#
# 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 Normal file → Executable file
View File

View File

@ -1,2 +1,17 @@
<?php <?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"; require_once "vendor/autoload.php";

View File

@ -29,7 +29,7 @@
} }
function pluginhandler_tags(\Plugin $plugin, string $method): string { 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("plugin", strtolower(get_class($plugin))) .
hidden_tag("method", $method); hidden_tag("method", $method);
} }

View File

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

View File

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

View File

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

View File

@ -45,7 +45,7 @@
<style type="text/css"> <style type="text/css">
<?php <?php
foreach (PluginHost::getInstance()->get_plugins() as $p) { foreach (PluginHost::getInstance()->get_plugins() as $n => $p) {
if (method_exists($p, "get_css")) { if (method_exists($p, "get_css")) {
echo $p->get_css(); echo $p->get_css();
} }
@ -263,7 +263,6 @@
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcPrefs')"><?= __('Preferences...') ?></div> <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('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" disabled="1"><?= __('Feed actions:') ?></div>
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcAddFeed')"><?= __('Subscribe to feed...') ?></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> <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')); return dijit.getEnclosingWidget(elem.closest('.dijitDialog'));
}, },
getPhArgs(plugin, method, args = {}) { 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) { label_to_feed_id: function(label) {
return this.LABEL_BASE_INDEX - 1 - Math.abs(label); return this.LABEL_BASE_INDEX - 1 - Math.abs(label);
@ -291,7 +291,7 @@ const App = {
setCombinedMode: function(combined) { setCombinedMode: function(combined) {
const value = combined ? "true" : "false"; 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.setInitParam("combined_display_mode",
!this.getInitParam("combined_display_mode")); !this.getInitParam("combined_display_mode"));
@ -306,7 +306,7 @@ const App = {
if (App.isCombinedMode()) { if (App.isCombinedMode()) {
const value = expand ? "true" : "false"; 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")); this.setInitParam("cdm_expanded", !this.getInitParam("cdm_expanded"));
Headlines.renderAgain(); Headlines.renderAgain();
}); });
@ -440,7 +440,7 @@ const App = {
} }
}, },
hotkeyHelp: function() { 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({ const dialog = new fox.SingleUseDialog({
title: __("Keyboard shortcuts"), title: __("Keyboard shortcuts"),
content: reply, content: reply,
@ -621,7 +621,7 @@ const App = {
try { try {
xhr.post("backend.php", xhr.post("backend.php",
{op: "RPC", method: "log", {op: "rpc", method: "log",
file: params.filename ? params.filename : error.fileName, file: params.filename ? params.filename : error.fileName,
line: params.lineno ? params.lineno : error.lineNumber, line: params.lineno ? params.lineno : error.lineNumber,
msg: message, msg: message,
@ -703,7 +703,7 @@ const App = {
this.initHotkeyActions(); this.initHotkeyActions();
const params = { const params = {
op: "RPC", op: "rpc",
method: "sanityCheck", method: "sanityCheck",
clientTzOffset: new Date().getTimezoneOffset() * 60, clientTzOffset: new Date().getTimezoneOffset() * 60,
hasSandbox: "sandbox" in document.createElement("iframe"), hasSandbox: "sandbox" in document.createElement("iframe"),
@ -737,7 +737,7 @@ const App = {
return errorMsg == ""; return errorMsg == "";
}, },
updateRuntimeInfo: function() { updateRuntimeInfo: function() {
xhr.json("backend.php", {op: "RPC", method: "getruntimeinfo"}, () => { xhr.json("backend.php", {op: "rpc", method: "getruntimeinfo"}, () => {
// handled by xhr.json() // handled by xhr.json()
}); });
}, },
@ -858,7 +858,7 @@ const App = {
checkForUpdates: function() { checkForUpdates: function() {
console.log('checking for updates...'); console.log('checking for updates...');
xhr.json("backend.php", {op: 'RPC', method: 'checkforupdates'}) xhr.json("backend.php", {op: 'rpc', method: 'checkforupdates'})
.then((reply) => { .then((reply) => {
console.log('update reply', reply); console.log('update reply', reply);
@ -965,7 +965,7 @@ const App = {
if (article_id) Article.view(article_id); 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() { initHotkeyActions: function() {
if (this.is_prefs) { if (this.is_prefs) {
@ -1149,7 +1149,7 @@ const App = {
if (!Feeds.activeIsCat() && parseInt(Feeds.getActive()) > 0) { if (!Feeds.activeIsCat() && parseInt(Feeds.getActive()) > 0) {
/* global __csrf_token */ /* 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}); feed_id: Feeds.getActive(), csrf_token: __csrf_token});
} else { } else {
@ -1158,7 +1158,7 @@ const App = {
}; };
this.hotkey_actions["feed_debug_viewfeed"] = () => { 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}); feed: Feeds.getActive(), timestamps: 1, debug: 1, cat: Feeds.activeIsCat(), csrf_token: __csrf_token});
}; };
@ -1177,13 +1177,13 @@ const App = {
Headlines.reverse(); Headlines.reverse();
}; };
this.hotkey_actions["feed_toggle_grid"] = () => { 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); App.setInitParam("cdm_enable_grid", reply.value);
Headlines.renderAgain(); Headlines.renderAgain();
}) })
}; };
this.hotkey_actions["feed_toggle_vgroup"] = () => { 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(); Feeds.reloadCurrent();
}) })
}; };
@ -1270,14 +1270,11 @@ const App = {
case "qmcSearch": case "qmcSearch":
Feeds.search(); Feeds.search();
break; break;
case "qmcFilterFeeds":
Feeds.filter();
break;
case "qmcAddFeed": case "qmcAddFeed":
CommonDialogs.subscribeToFeed(); CommonDialogs.subscribeToFeed();
break; break;
case "qmcDigest": case "qmcDigest":
window.location.href = "backend.php?op=Digest"; window.location.href = "backend.php?op=digest";
break; break;
case "qmcEditFeed": case "qmcEditFeed":
if (Feeds.activeIsCat()) if (Feeds.activeIsCat())

View File

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

View File

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

View File

@ -115,7 +115,7 @@ const Filters = {
insertRule: function(parentNode, replaceNode) { insertRule: function(parentNode, replaceNode) {
const rule = dojo.formToJson("filter_new_rule_form"); 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 { try {
const li = document.createElement('li'); const li = document.createElement('li');
li.addClassName("rule"); li.addClassName("rule");
@ -147,7 +147,7 @@ const Filters = {
const action = dojo.formToJson(form); 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 { try {
const li = document.createElement('li'); const li = document.createElement('li');
li.addClassName("action"); li.addClassName("action");
@ -200,7 +200,7 @@ const Filters = {
console.log(rule, dialog.filter_info); 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', edit_rule_dialog.attr('content',
` `
<form name="filter_new_rule_form" id="filter_new_rule_form" onsubmit="return false"> <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); 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); edit_action_dialog.attr('content', reply);
setTimeout(() => { setTimeout(() => {
@ -365,7 +365,7 @@ const Filters = {
Notify.progress("Removing filter..."); 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, () => { xhr.post("backend.php", query, () => {
const tree = dijit.byId("filterTree"); const tree = dijit.byId("filterTree");
@ -411,7 +411,7 @@ const Filters = {
const tmph = dojo.connect(dialog, 'onShow', function () { const tmph = dojo.connect(dialog, 'onShow', function () {
dojo.disconnect(tmph); 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; dialog.filter_info = filter;
@ -425,7 +425,7 @@ const Filters = {
` `
<form onsubmit='return false'> <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("id", filter_id)}
${App.FormFields.hidden_tag("method", filter_id ? "editSave" : "add")} ${App.FormFields.hidden_tag("method", filter_id ? "editSave" : "add")}
${App.FormFields.hidden_tag("csrf_token", App.getInitParam('csrf_token'))} ${App.FormFields.hidden_tag("csrf_token", App.getInitParam('csrf_token'))}
@ -541,7 +541,7 @@ const Filters = {
dialog.editRule(null, dojo.toJson(rule)); dialog.editRule(null, dojo.toJson(rule));
} else { } 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) => { xhr.json("backend.php", query, (reply) => {
let title; 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({ menu.addChild(new dijit.MenuItem({
label: __("Open site"), label: __("Open site"),
onClick: function() { 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}); 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"), label: __("Debug feed"),
onClick: function() { onClick: function() {
/* global __csrf_token */ /* 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}); feed_id: this.getParent().row_id, csrf_token: __csrf_token});
}})); }}));
} }

View File

@ -23,7 +23,6 @@ const Feeds = {
infscroll_in_progress: 0, infscroll_in_progress: 0,
infscroll_disabled: 0, infscroll_disabled: 0,
_infscroll_timeout: false, _infscroll_timeout: false,
_filter_query: false, // TODO: figure out the UI for this
_search_query: false, _search_query: false,
last_search_query: [], last_search_query: [],
_viewfeed_wait_timeout: false, _viewfeed_wait_timeout: false,
@ -155,17 +154,13 @@ const Feeds = {
toggle: function() { toggle: function() {
Element.toggle("feeds-holder"); Element.toggle("feeds-holder");
}, },
cancelFilter: function() {
this._filter_query = "";
this.reload();
},
cancelSearch: function() { cancelSearch: function() {
this._search_query = ""; this._search_query = "";
this.reloadCurrent(); this.reloadCurrent();
}, },
// null = get all data, [] would give empty response for specific type // null = get all data, [] would give empty response for specific type
requestCounters: function(feed_ids = null, label_ids = null) { requestCounters: function(feed_ids = null, label_ids = null) {
xhr.json("backend.php", {op: "RPC", xhr.json("backend.php", {op: "rpc",
method: "getAllCounters", method: "getAllCounters",
"feed_ids[]": feed_ids, "feed_ids[]": feed_ids,
"feed_id_count": feed_ids ? feed_ids.length : -1, "feed_id_count": feed_ids ? feed_ids.length : -1,
@ -183,14 +178,8 @@ const Feeds = {
dijit.byId("feedTree").destroyRecursive(); 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({ const store = new dojo.data.ItemFileWriteStore({
url: "backend.php?" + dojo.objectToQuery(query) url: "backend.php?op=pref_feeds&method=getfeedtree&mode=2"
}); });
// noinspection JSUnresolvedFunction // noinspection JSUnresolvedFunction
@ -358,7 +347,7 @@ const Feeds = {
toggleUnread: function() { toggleUnread: function() {
const hide = !App.getInitParam("hide_read_feeds"); 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); this.hideOrShowFeeds(hide);
App.setInitParam("hide_read_feeds", hide); App.setInitParam("hide_read_feeds", hide);
}); });
@ -397,7 +386,7 @@ const Feeds = {
}, 10 * 1000); }, 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; if (method) query.m = method;
@ -446,7 +435,7 @@ const Feeds = {
Notify.progress("Marking all feeds as read..."); 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(); this.reloadCurrent();
}); });
@ -484,7 +473,7 @@ const Feeds = {
} }
const catchup_query = { 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], is_cat: is_cat, mode: mode, search_query: this.last_search_query[0],
search_lang: this.last_search_query[1] search_lang: this.last_search_query[1]
}; };
@ -623,7 +612,7 @@ const Feeds = {
}, },
search: function() { search: function() {
xhr.json("backend.php", xhr.json("backend.php",
{op: "Feeds", method: "search"}, {op: "feeds", method: "search"},
(reply) => { (reply) => {
try { try {
const dialog = new fox.SingleUseDialog({ const dialog = new fox.SingleUseDialog({
@ -693,53 +682,11 @@ 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() { updateRandom: function() {
console.log("in update_random_feed"); 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) if (ops.tmark.length != 0)
promises.push(xhr.post("backend.php", 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) if (ops.tpub.length != 0)
promises.push(xhr.post("backend.php", 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) if (ops.read.length != 0)
promises.push(xhr.post("backend.php", 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) if (ops.unread.length != 0)
promises.push(xhr.post("backend.php", 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); const scores = Object.keys(ops.rescore);
if (scores.length != 0) { if (scores.length != 0) {
scores.forEach((score) => { scores.forEach((score) => {
promises.push(xhr.post("backend.php", 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 = { const query = {
op: "Article", method: "removeFromLabel", op: "article", method: "removeFromLabel",
ids: ids.toString(), lid: id ids: ids.toString(), lid: id
}; };
@ -1149,7 +1149,7 @@ const Headlines = {
} }
const query = { const query = {
op: "Article", method: "assignToLabel", op: "article", method: "assignToLabel",
ids: ids.toString(), lid: id ids: ids.toString(), lid: id
}; };
@ -1181,7 +1181,7 @@ const Headlines = {
return; return;
} }
const query = {op: "RPC", method: "delete", ids: rows.toString()}; const query = {op: "rpc", method: "delete", ids: rows.toString()};
xhr.json("backend.php", query, () => { xhr.json("backend.php", query, () => {
Feeds.reloadCurrent(); Feeds.reloadCurrent();
@ -1586,7 +1586,7 @@ const Headlines = {
menu.addChild(new dijit.MenuItem({ menu.addChild(new dijit.MenuItem({
label: __("Open site"), label: __("Open site"),
onClick: function() { 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}); feed_id: this.getParent().currentTarget.getAttribute("data-feed-id"), csrf_token: __csrf_token});
}})); }}));
@ -1596,7 +1596,7 @@ const Headlines = {
label: __("Debug feed"), label: __("Debug feed"),
onClick: function() { onClick: function() {
/* global __csrf_token */ /* 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}); 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({ dojo.xhrPost({
url: "backend.php", url: "backend.php",
content: {op: "Pref_Feeds", method: "savefeedorder", content: {op: "pref-feeds", method: "savefeedorder",
payload: newFileContentString}, payload: newFileContentString},
error: saveFailedCallback, error: saveFailedCallback,
load: saveCompleteCallback}); 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 searchElem = App.byId("feed_search");
const search = (searchElem) ? searchElem.value : ""; 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); dijit.byId('feedsTab').attr('content', reply);
Notify.close(); Notify.close();
}); });
@ -185,14 +185,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
resetFeedOrder: function() { resetFeedOrder: function() {
Notify.progress("Loading, please wait..."); 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(); this.reload();
}); });
}, },
resetCatOrder: function() { resetCatOrder: function() {
Notify.progress("Loading, please wait..."); 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(); 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))) { if (confirm(__("Remove category %s? Any nested feeds would be placed into Uncategorized.").replace("%s", item.name))) {
Notify.progress("Removing category..."); 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(); Notify.close();
this.reload(); this.reload();
}); });
@ -215,7 +215,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
Notify.progress("Unsubscribing from selected feeds...", true); Notify.progress("Unsubscribing from selected feeds...", true);
const query = { const query = {
op: "Pref_Feeds", method: "remove", op: "pref-feeds", method: "remove",
ids: sel_rows.toString() ids: sel_rows.toString()
}; };
@ -231,14 +231,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
return false; return false;
}, },
checkErrorFeeds: function() { 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) { if (reply.length > 0) {
Element.show(dijit.byId("pref_feeds_errors_btn").domNode); Element.show(dijit.byId("pref_feeds_errors_btn").domNode);
} }
}); });
}, },
checkInactiveFeeds: function() { 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) { if (reply.length > 0) {
Element.show(dijit.byId("pref_feeds_inactive_btn").domNode); 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..."); Notify.progress("Removing selected categories...");
const query = { const query = {
op: "Pref_Feeds", method: "removeCat", op: "pref-feeds", method: "removeCat",
ids: sel_rows.toString() ids: sel_rows.toString()
}; };
@ -316,7 +316,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
Notify.progress("Loading, please wait..."); 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(); Notify.close();
try { try {
@ -393,7 +393,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
Notify.progress("Loading, please wait..."); 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(); this.reload();
}); });
} }
@ -404,14 +404,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
if (title) { if (title) {
Notify.progress("Creating category..."); 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(); Notify.close();
this.reload(); this.reload();
}); });
} }
}, },
batchSubscribe: function() { 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({ const dialog = new fox.SingleUseDialog({
id: "batchSubDlg", id: "batchSubDlg",
title: __("Batch subscribe"), title: __("Batch subscribe"),
@ -431,7 +431,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
}, },
content: ` content: `
<form onsubmit='return false'> <form onsubmit='return false'>
${App.FormFields.hidden_tag("op", "Pref_Feeds")} ${App.FormFields.hidden_tag("op", "pref-feeds")}
${App.FormFields.hidden_tag("method", "batchaddfeeds")} ${App.FormFields.hidden_tag("method", "batchaddfeeds")}
<header class='horizontal'> <header class='horizontal'>
@ -484,7 +484,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
}); });
}, },
showInactiveFeeds: function() { 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({ const dialog = new fox.SingleUseDialog({
id: "inactiveFeedsDlg", id: "inactiveFeedsDlg",
@ -500,7 +500,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
Notify.progress("Removing selected feeds...", true); Notify.progress("Removing selected feeds...", true);
const query = { const query = {
op: "Pref_Feeds", method: "remove", op: "pref-feeds", method: "remove",
ids: sel_rows.toString() ids: sel_rows.toString()
}; };

View File

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

View File

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

View File

@ -19,7 +19,7 @@ const Helpers = {
alert("No passwords selected."); alert("No passwords selected.");
} else if (confirm(__("Remove selected app passwords?"))) { } 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); this.updateContent(reply);
Notify.close(); Notify.close();
}); });
@ -31,7 +31,7 @@ const Helpers = {
const title = prompt("Password description:") const title = prompt("Password description:")
if (title) { 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); this.updateContent(reply);
Notify.close(); Notify.close();
}); });
@ -45,7 +45,7 @@ const Helpers = {
if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) { if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) {
Notify.progress("Clearing URLs..."); 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."); Notify.info("Generated URLs cleared.");
}); });
} }
@ -71,7 +71,7 @@ const Helpers = {
const tmph = dojo.connect(dialog, 'onShow', function () { const tmph = dojo.connect(dialog, 'onShow', function () {
dojo.disconnect(tmph); 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]; dialog.domNode.querySelector('.digest-preview').innerHTML = reply[0];
}); });
}); });
@ -91,7 +91,7 @@ const Helpers = {
}, },
update: function() { update: function() {
xhr.post("backend.php", { xhr.post("backend.php", {
op: "Pref_System", op: "pref-system",
severity: dijit.byId("severity").attr('value'), severity: dijit.byId("severity").attr('value'),
page: Helpers.EventLog.log_page page: Helpers.EventLog.log_page
}, (reply) => { }, (reply) => {
@ -114,7 +114,7 @@ const Helpers = {
Notify.progress("Loading, please wait..."); 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(); Helpers.EventLog.refresh();
}); });
} }
@ -135,7 +135,7 @@ const Helpers = {
const new_title = prompt(__("Name for cloned profile:")); const new_title = prompt(__("Name for cloned profile:"));
if (new_title) { 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(); Notify.close();
dialog.refresh(); dialog.refresh();
}); });
@ -153,7 +153,7 @@ const Helpers = {
if (confirm(__("Remove selected profiles? Active and default profiles will not be removed."))) { if (confirm(__("Remove selected profiles? Active and default profiles will not be removed."))) {
Notify.progress("Removing selected profiles...", true); 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(); Notify.close();
dialog.refresh(); dialog.refresh();
}); });
@ -167,7 +167,7 @@ const Helpers = {
if (this.validate()) { if (this.validate()) {
Notify.progress("Creating profile...", true); 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, () => { xhr.post("backend.php", query, () => {
Notify.close(); Notify.close();
@ -177,7 +177,7 @@ const Helpers = {
} }
}, },
refresh: function() { 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', ` dialog.attr('content', `
<div dojoType='fox.Toolbar'> <div dojoType='fox.Toolbar'>
<div dojoType='fox.form.DropDownButton'> <div dojoType='fox.form.DropDownButton'>
@ -210,7 +210,7 @@ const Helpers = {
profile-id='${profile.id}'>${profile.title} profile-id='${profile.id}'>${profile.title}
<script type='dojo/method' event='onChange' args='value'> <script type='dojo/method' event='onChange' args='value'>
xhr.post("backend.php", 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> </script>
@ -242,7 +242,7 @@ const Helpers = {
if (confirm(__("Activate selected profile?"))) { if (confirm(__("Activate selected profile?"))) {
Notify.progress("Loading, please wait..."); 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(); 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.")} ${__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here.")}
</div> </div>
${App.FormFields.hidden_tag('op', 'RPC')} ${App.FormFields.hidden_tag('op', 'rpc')}
${App.FormFields.hidden_tag('method', 'setpref')} ${App.FormFields.hidden_tag('method', 'setpref')}
${App.FormFields.hidden_tag('key', 'USER_STYLESHEET')} ${App.FormFields.hidden_tag('key', 'USER_STYLESHEET')}
@ -312,7 +312,7 @@ const Helpers = {
const tmph = dojo.connect(dialog, 'onShow', function () { const tmph = dojo.connect(dialog, 'onShow', function () {
dojo.disconnect(tmph); 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")); const editor = dijit.getEnclosingWidget(dialog.domNode.querySelector(".user-css-editor"));
@ -327,14 +327,14 @@ const Helpers = {
}, },
confirmReset: function() { confirmReset: function() {
if (confirm(__("Reset to defaults?"))) { 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(); Helpers.Prefs.refresh();
Notify.info(reply); Notify.info(reply);
}); });
} }
}, },
refresh: function() { refresh: function() {
xhr.post("backend.php", { op: "Pref_Prefs" }, (reply) => { xhr.post("backend.php", { op: "pref-prefs" }, (reply) => {
dijit.byId('prefsTab').attr('content', reply); dijit.byId('prefsTab').attr('content', reply);
Notify.close(); Notify.close();
}); });
@ -360,7 +360,7 @@ const Helpers = {
this.render_contents(); this.render_contents();
}, },
reload: function() { 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._list_of_plugins = reply;
this.render_contents(); this.render_contents();
}, (e) => { }, (e) => {
@ -444,7 +444,7 @@ const Helpers = {
if (confirm(__("Clear stored data for %s?").replace("%s", name))) { if (confirm(__("Clear stored data for %s?").replace("%s", name))) {
Notify.progress("Loading, please wait..."); 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(); Helpers.Prefs.refresh();
}); });
} }
@ -455,7 +455,7 @@ const Helpers = {
if (confirm(msg)) { if (confirm(msg)) {
Notify.progress("Loading, please wait..."); 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) if (reply && reply.status == 1)
Helpers.Plugins.reload(); Helpers.Plugins.reload();
else { else {
@ -504,7 +504,7 @@ const Helpers = {
const container = install_dialog.domNode.querySelector(".contents"); 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) { if (!reply) {
container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`; container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`;
} else { } else {
@ -603,7 +603,7 @@ const Helpers = {
const container = dialog.domNode.querySelector(".contents"); const container = dialog.domNode.querySelector(".contents");
container.innerHTML = `<li class='text-center'>${__("Looking for plugins...")}</li>`; 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.entries = reply;
dialog.render_contents(); dialog.render_contents();
}); });
@ -656,7 +656,7 @@ const Helpers = {
container.innerHTML = `<li class='text-center'>${__("Updating, please wait...")}</li>`; container.innerHTML = `<li class='text-center'>${__("Updating, please wait...")}</li>`;
let enable_update_btn = false; 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) { if (!reply) {
container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`; 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>`; //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) { if (!reply) {
container.innerHTML += `<li class='text-error'>${__("%s: Operation failed: check event log.").replace("%s", name)}</li>`; container.innerHTML += `<li class='text-error'>${__("%s: Operation failed: check event log.").replace("%s", name)}</li>`;
@ -834,7 +834,7 @@ const Helpers = {
}, },
export: function() { export: function() {
console.log("export"); 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; return rv;
}, },
reload: function() { reload: function() {
xhr.post("backend.php", { op: "Pref_Labels" }, (reply) => { xhr.post("backend.php", { op: "pref-labels" }, (reply) => {
dijit.byId('labelsTab').attr('content', reply); dijit.byId('labelsTab').attr('content', reply);
Notify.close(); Notify.close();
}); });
}, },
editLabel: function(id) { 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 fg_color = reply['fg_color'];
const bg_color = reply['bg_color'] ? reply['bg_color'] : '#fff7d5'; 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 = { const query = {
op: "Pref_Labels", method: "colorset", kind: kind, op: "pref-labels", method: "colorset", kind: kind,
ids: id, fg: fg, bg: bg, color: color ids: id, fg: fg, bg: bg, color: color
}; };
@ -131,7 +131,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
</section> </section>
${App.FormFields.hidden_tag('id', id)} ${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('method', 'save')}
${App.FormFields.hidden_tag('fg_color', fg_color, {}, 'labelEdit_fgColor')} ${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?"))) { if (confirm(__("Reset selected labels to default colors?"))) {
const query = { const query = {
op: "Pref_Labels", method: "colorreset", op: "pref-labels", method: "colorreset",
ids: labels.toString() ids: labels.toString()
}; };
@ -210,7 +210,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
Notify.progress("Removing selected labels..."); Notify.progress("Removing selected labels...");
const query = { const query = {
op: "Pref_Labels", method: "remove", op: "pref-labels", method: "remove",
ids: sel_rows.toString() ids: sel_rows.toString()
}; };

View File

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

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

View File

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

View File

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

View File

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

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