Compare commits
148 Commits
dc25a9cf68
...
a86df7eac8
Author | SHA1 | Date |
---|---|---|
Andrew Dolgov | a86df7eac8 | |
wn_ | 03c9d4f390 | |
Andrew Dolgov | 283ad4ebea | |
Andrew Dolgov | d334023267 | |
wn_ | 8ef2803b27 | |
Andrew Dolgov | de214a01d2 | |
Andrew Dolgov | bcdfedeb8a | |
Andrew Dolgov | ea6cdcccb0 | |
wn_ | 8727fb3ba8 | |
Andrew Dolgov | f0f22c23c5 | |
wn_ | 90e7bf7cc3 | |
Andrew Dolgov | a882eb13f7 | |
wn_ | 91a91dac15 | |
Andrew Dolgov | 51cd02fc3e | |
wn_ | 0ea9db3170 | |
wn_ | 9a1f7c2ebf | |
wn_ | 3c171cc92c | |
wn_ | e33b0297d5 | |
wn_ | 9132360d46 | |
wn_ | d82da74363 | |
wn_ | ff59fbd460 | |
wn_ | e85d47dfd4 | |
Andrew Dolgov | d4ae6c67db | |
Chih-Hsuan Yen | f1a9ac9b15 | |
Andrew Dolgov | 67012f9dac | |
Andrew Dolgov | 14ad8b21d5 | |
Andrew Dolgov | 4b3cf17d8d | |
Andrew Dolgov | 1b31e6fd5b | |
wn_ | 7883f024e7 | |
Andrew Dolgov | 8f66f579e4 | |
Andrew Dolgov | 09898ccbc8 | |
Andrew Dolgov | 2b8e344532 | |
Andrew Dolgov | e453befab6 | |
Andrew Dolgov | dbb6e7291e | |
Andrew Dolgov | eac9e7c103 | |
Andrew Dolgov | 93bd96e356 | |
Andrew Dolgov | 7005d6d5f3 | |
Andrew Dolgov | 0621d22bbe | |
Andrew Dolgov | cc133d2c0a | |
Andrew Dolgov | e52eaf0e7b | |
Andrew Dolgov | ce9847d317 | |
Chih-Hsuan Yen | d4da4dcc32 | |
Andrew Dolgov | 2c7e000120 | |
Andrew Dolgov | 1fe1132a1a | |
Andrew Dolgov | 61910acbcd | |
Andrew Dolgov | ff4248b09e | |
Andrew Dolgov | 0b7d021f8e | |
Andrew Dolgov | d4c972f551 | |
Andrew Dolgov | f48f1b0131 | |
Andrew Dolgov | 4ced03b4b6 | |
Andrew Dolgov | 9ce347d8d5 | |
Andrew Dolgov | e777f2e292 | |
Andrew Dolgov | ee18936bfe | |
Andrew Dolgov | 5cfde4cada | |
Andrew Dolgov | 1be156408a | |
Andrew Dolgov | cfcab96e18 | |
Andrew Dolgov | 7cd2c5cac8 | |
Andrew Dolgov | adf3985afa | |
Andrew Dolgov | afaef66783 | |
Andrew Dolgov | 8b72d9ab11 | |
Andrew Dolgov | 855695a862 | |
Andrew Dolgov | 0ac8710ea1 | |
Andrew Dolgov | 01c9869e2b | |
Andrew Dolgov | d2424b9e4b | |
Andrew Dolgov | a1a2fe40f6 | |
Andrew Dolgov | 925256c81f | |
Andrew Dolgov | 5a7c5b8249 | |
Andrew Dolgov | 5920ac814c | |
Andrew Dolgov | 2af5f73480 | |
wn_ | c7e1caf223 | |
Andrew Dolgov | 8c9c69921f | |
Andrew Dolgov | 3181272619 | |
Andrew Dolgov | 865ecc8796 | |
Andrew Dolgov | 0a5507d3bd | |
Andrew Dolgov | 69c1c62992 | |
Andrew Dolgov | de2830b241 | |
Andrew Dolgov | ed43a73369 | |
Andrew Dolgov | e31636bf97 | |
Andrew Dolgov | 3d5308a6e5 | |
Andrew Dolgov | 30b36e0034 | |
Andrew Dolgov | 1e3b7f7a43 | |
Andrew Dolgov | 994f376f42 | |
Andrew Dolgov | deb441e9e3 | |
Andrew Dolgov | 9826d2f075 | |
Andrew Dolgov | e956632c5c | |
Andrew Dolgov | 7af2938aea | |
Andrew Dolgov | c28955c8ba | |
Andrew Dolgov | a7f3543516 | |
Andrew Dolgov | 761c3826d1 | |
Andrew Dolgov | de39d97e1f | |
Andrew Dolgov | 1bfae41e6d | |
Andrew Dolgov | efd5d79dde | |
Andrew Dolgov | db05575b2d | |
Andrew Dolgov | ce3eb32076 | |
Andrew Dolgov | 752c692170 | |
Andrew Dolgov | 8d3f570ee9 | |
Andrew Dolgov | 7bba4ae558 | |
Andrew Dolgov | 382d01e8db | |
Andrew Dolgov | 487635ca28 | |
Andrew Dolgov | bde94dbf4b | |
Andrew Dolgov | 322296d6a0 | |
Andrew Dolgov | ccb4a4d337 | |
Andrew Dolgov | b0f96dbb5a | |
Andrew Dolgov | aec8cdd0c8 | |
Andrew Dolgov | cb90393a7e | |
Andrew Dolgov | 028afdd7d5 | |
Andrew Dolgov | 6b1b496248 | |
Andrew Dolgov | d744209df7 | |
Andrew Dolgov | eac076fcd6 | |
Andrew Dolgov | e7ddbbb2ce | |
Andrew Dolgov | ff818a75f0 | |
Andrew Dolgov | 03e956132d | |
Andrew Dolgov | 2b61052e87 | |
Andrew Dolgov | cf18bc576e | |
Andrew Dolgov | 3bf275e445 | |
Andrew Dolgov | 492c4eecfb | |
Andrew Dolgov | 93bb473bce | |
Andrew Dolgov | 6e025103d3 | |
Andrew Dolgov | 350177df39 | |
Andrew Dolgov | d3fadc0bd0 | |
Andrew Dolgov | bf6e3c381b | |
Andrew Dolgov | 7092a1e85d | |
Andrew Dolgov | 62ca093b75 | |
Andrew Dolgov | cdd7ad020e | |
Andrew Dolgov | 45a9ff0c88 | |
Andrew Dolgov | 6c75ea17da | |
Andrew Dolgov | b07ad642de | |
Andrew Dolgov | 56315b39b4 | |
Andrew Dolgov | 89f5af62d8 | |
Andrew Dolgov | 9556519e67 | |
Andrew Dolgov | c779e2ba0d | |
Andrew Dolgov | 40df94c169 | |
Andrew Dolgov | e29fe626e1 | |
Andrew Dolgov | b15f185e3d | |
Andrew Dolgov | f489f620d0 | |
Andrew Dolgov | dd6ac57a07 | |
Andrew Dolgov | 03526d8151 | |
Andrew Dolgov | 8535305cfc | |
Andrew Dolgov | afd04d141c | |
Andrew Dolgov | 485bfe327a | |
Andrew Dolgov | e2ab00c889 | |
Andrew Dolgov | 83f5ab5c79 | |
Andrew Dolgov | faefedb950 | |
Andrew Dolgov | adba0aa8d2 | |
Andrew Dolgov | ba6a912abd | |
Andrew Dolgov | bd95325f8d | |
Andrew Dolgov | 1d788eddf8 | |
Andrew Dolgov | 3d255d861c |
|
@ -1,22 +1,25 @@
|
|||
FROM registry.fakecake.org/docker.io/alpine:3.18
|
||||
ARG PROXY_REGISTRY
|
||||
FROM ${PROXY_REGISTRY}alpine:3.19
|
||||
EXPOSE 9000/tcp
|
||||
|
||||
ENV SCRIPT_ROOT=/opt/tt-rss
|
||||
ENV SRC_DIR=/src/tt-rss/
|
||||
|
||||
RUN apk add --no-cache dcron php82 php82-fpm php82-phar php82-sockets php82-pecl-apcu \
|
||||
php82-pdo php82-gd php82-pgsql php82-pdo_pgsql php82-xmlwriter php82-opcache \
|
||||
php82-mbstring php82-intl php82-xml php82-curl php82-simplexml \
|
||||
php82-session php82-tokenizer php82-dom php82-fileinfo php82-ctype \
|
||||
php82-json php82-iconv php82-pcntl php82-posix php82-zip php82-exif \
|
||||
php82-openssl git postgresql-client sudo php82-pecl-xdebug rsync tzdata && \
|
||||
sed -i 's/\(memory_limit =\) 128M/\1 256M/' /etc/php82/php.ini && \
|
||||
RUN apk add --no-cache dcron php83 php83-fpm php83-phar php83-sockets php83-pecl-apcu \
|
||||
php83-pdo php83-gd php83-pgsql php83-pdo_pgsql php83-xmlwriter php83-opcache \
|
||||
php83-mbstring php83-intl php83-xml php83-curl php83-simplexml \
|
||||
php83-session php83-tokenizer php83-dom php83-fileinfo php83-ctype \
|
||||
php83-json php83-iconv php83-pcntl php83-posix php83-zip php83-exif \
|
||||
php83-openssl git postgresql-client sudo php83-pecl-xdebug rsync tzdata && \
|
||||
sed -i 's/\(memory_limit =\) 128M/\1 256M/' /etc/php83/php.ini && \
|
||||
sed -i -e 's/^listen = 127.0.0.1:9000/listen = 9000/' \
|
||||
-e 's/;\(clear_env\) = .*/\1 = no/i' \
|
||||
-e 's/;\(pm.status_path = \/status\)/\1/i' \
|
||||
-e 's/;\(pm.status_listen\) = .*/\1 = 9001/i' \
|
||||
-e 's/^\(user\|group\) = .*/\1 = app/i' \
|
||||
-e 's/;\(php_admin_value\[error_log\]\) = .*/\1 = \/tmp\/error.log/' \
|
||||
-e 's/;\(php_admin_flag\[log_errors\]\) = .*/\1 = on/' \
|
||||
/etc/php82/php-fpm.d/www.conf && \
|
||||
/etc/php83/php-fpm.d/www.conf && \
|
||||
mkdir -p /var/www ${SCRIPT_ROOT}/config.d
|
||||
|
||||
ARG CI_COMMIT_BRANCH
|
||||
|
@ -31,15 +34,17 @@ ENV CI_COMMIT_TIMESTAMP=${CI_COMMIT_TIMESTAMP}
|
|||
ARG CI_COMMIT_SHA
|
||||
ENV CI_COMMIT_SHA=${CI_COMMIT_SHA}
|
||||
|
||||
ADD --chmod=0755 startup.sh ${SCRIPT_ROOT}
|
||||
ADD --chmod=0755 updater.sh ${SCRIPT_ROOT}
|
||||
ADD --chmod=0755 dcron.sh ${SCRIPT_ROOT}
|
||||
ADD --chmod=0755 backup.sh /etc/periodic/weekly/backup
|
||||
ADD .docker/app/startup.sh ${SCRIPT_ROOT}
|
||||
ADD .docker/app/updater.sh ${SCRIPT_ROOT}
|
||||
ADD .docker/app/dcron.sh ${SCRIPT_ROOT}
|
||||
ADD .docker/app/backup.sh /etc/periodic/weekly/backup
|
||||
|
||||
ADD index.php ${SCRIPT_ROOT}
|
||||
ADD config.docker.php ${SCRIPT_ROOT}
|
||||
RUN chmod 0755 ${SCRIPT_ROOT}/*.sh
|
||||
|
||||
COPY --from=app-src . ${SRC_DIR}
|
||||
ADD .docker/app/index.php ${SCRIPT_ROOT}
|
||||
ADD .docker/app/config.docker.php ${SCRIPT_ROOT}
|
||||
|
||||
COPY . ${SRC_DIR}
|
||||
|
||||
ARG ORIGIN_REPO_XACCEL=https://git.tt-rss.org/fox/ttrss-nginx-xaccel.git
|
||||
|
||||
|
@ -62,6 +67,7 @@ ENV ADMIN_USER_ACCESS_LEVEL=""
|
|||
ENV AUTO_CREATE_USER=""
|
||||
ENV AUTO_CREATE_USER_PASS=""
|
||||
ENV AUTO_CREATE_USER_ACCESS_LEVEL="0"
|
||||
ENV AUTO_CREATE_USER_ENABLE_API=""
|
||||
|
||||
# TODO: remove prefix from container variables not used by tt-rss itself:
|
||||
#
|
||||
|
@ -81,7 +87,7 @@ ENV TTRSS_DB_HOST="db"
|
|||
ENV TTRSS_DB_PORT="5432"
|
||||
|
||||
ENV TTRSS_MYSQL_CHARSET="UTF8"
|
||||
ENV TTRSS_PHP_EXECUTABLE="/usr/bin/php82"
|
||||
ENV TTRSS_PHP_EXECUTABLE="/usr/bin/php83"
|
||||
ENV TTRSS_PLUGINS="auth_internal, note, nginx_xaccel"
|
||||
|
||||
CMD ${SCRIPT_ROOT}/startup.sh
|
||||
|
|
|
@ -24,31 +24,35 @@ export PGPASSWORD=$TTRSS_DB_PASS
|
|||
|
||||
[ ! -e /var/www/html/index.php ] && cp ${SCRIPT_ROOT}/index.php /var/www/html
|
||||
|
||||
if [ ! -d $DST_DIR ]; then
|
||||
mkdir -p $DST_DIR
|
||||
chown $OWNER_UID:$OWNER_GID $DST_DIR
|
||||
if [ -z $SKIP_RSYNC_ON_STARTUP ]; then
|
||||
if [ ! -d $DST_DIR ]; then
|
||||
mkdir -p $DST_DIR
|
||||
chown $OWNER_UID:$OWNER_GID $DST_DIR
|
||||
|
||||
sudo -u app rsync -a \
|
||||
$SRC_DIR/ $DST_DIR/
|
||||
sudo -u app rsync -a \
|
||||
$SRC_DIR/ $DST_DIR/
|
||||
else
|
||||
chown -R $OWNER_UID:$OWNER_GID $DST_DIR
|
||||
|
||||
sudo -u app rsync -a --delete \
|
||||
--exclude /cache \
|
||||
--exclude /lock \
|
||||
--exclude /feed-icons \
|
||||
--exclude /plugins/af_comics/filters.local \
|
||||
--exclude /plugins.local \
|
||||
--exclude /templates.local \
|
||||
--exclude /themes.local \
|
||||
$SRC_DIR/ $DST_DIR/
|
||||
|
||||
sudo -u app rsync -a --delete \
|
||||
$SRC_DIR/plugins.local/nginx_xaccel \
|
||||
$DST_DIR/plugins.local/nginx_xaccel
|
||||
fi
|
||||
else
|
||||
chown -R $OWNER_UID:$OWNER_GID $DST_DIR
|
||||
|
||||
sudo -u app rsync -a --delete \
|
||||
--exclude /cache \
|
||||
--exclude /lock \
|
||||
--exclude /feed-icons \
|
||||
--exclude /plugins/af_comics/filters.local \
|
||||
--exclude /plugins.local \
|
||||
--exclude /templates.local \
|
||||
--exclude /themes.local \
|
||||
$SRC_DIR/ $DST_DIR/
|
||||
|
||||
sudo -u app rsync -a --delete \
|
||||
$SRC_DIR/plugins.local/nginx_xaccel \
|
||||
$DST_DIR/plugins.local/nginx_xaccel
|
||||
echo "warning: working copy in $DST_DIR won't be updated, make sure you know what you're doing."
|
||||
fi
|
||||
|
||||
for d in cache lock feed-icons plugins.local themes.local; do
|
||||
for d in cache lock feed-icons plugins.local themes.local templates.local cache/export cache/feeds cache/images cache/upload; do
|
||||
sudo -u app mkdir -p $DST_DIR/$d
|
||||
done
|
||||
|
||||
|
@ -61,7 +65,7 @@ sudo -u app cp ${SCRIPT_ROOT}/config.docker.php $DST_DIR/config.php
|
|||
chmod 644 $DST_DIR/config.php
|
||||
|
||||
chown -R $OWNER_UID:$OWNER_GID $DST_DIR \
|
||||
/var/log/php82
|
||||
/var/log/php83
|
||||
|
||||
if [ -z "$TTRSS_NO_STARTUP_PLUGIN_UPDATES" ]; then
|
||||
echo updating all local plugins...
|
||||
|
@ -100,9 +104,9 @@ if [ ! -z "${TTRSS_XDEBUG_ENABLED}" ]; then
|
|||
fi
|
||||
echo enabling xdebug with the following parameters:
|
||||
env | grep TTRSS_XDEBUG
|
||||
cat > /etc/php82/conf.d/50_xdebug.ini <<EOF
|
||||
cat > /etc/php83/conf.d/50_xdebug.ini <<EOF
|
||||
zend_extension=xdebug.so
|
||||
xdebug.mode=develop,trace,debug
|
||||
xdebug.mode=debug
|
||||
xdebug.start_with_request = yes
|
||||
xdebug.client_port = ${TTRSS_XDEBUG_PORT}
|
||||
xdebug.client_host = ${TTRSS_XDEBUG_HOST}
|
||||
|
@ -110,17 +114,17 @@ EOF
|
|||
fi
|
||||
|
||||
sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \
|
||||
/etc/php82/php.ini
|
||||
/etc/php83/php.ini
|
||||
|
||||
sed -i.bak "s/^\(pm.max_children\) = \(.*\)/\1 = ${PHP_WORKER_MAX_CHILDREN}/" \
|
||||
/etc/php82/php-fpm.d/www.conf
|
||||
/etc/php83/php-fpm.d/www.conf
|
||||
|
||||
sudo -Eu app php82 $DST_DIR/update.php --update-schema=force-yes
|
||||
sudo -Eu app php83 $DST_DIR/update.php --update-schema=force-yes
|
||||
|
||||
if [ ! -z "$ADMIN_USER_PASS" ]; then
|
||||
sudo -Eu app php82 $DST_DIR/update.php --user-set-password "admin:$ADMIN_USER_PASS"
|
||||
sudo -Eu app php83 $DST_DIR/update.php --user-set-password "admin:$ADMIN_USER_PASS"
|
||||
else
|
||||
if sudo -Eu app php82 $DST_DIR/update.php --user-check-password "admin:password"; then
|
||||
if sudo -Eu app php83 $DST_DIR/update.php --user-check-password "admin:password"; then
|
||||
RANDOM_PASS=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 16 ; echo '')
|
||||
|
||||
echo "*****************************************************************************"
|
||||
|
@ -128,17 +132,23 @@ else
|
|||
echo "* If you want to set it manually, use ADMIN_USER_PASS environment variable. *"
|
||||
echo "*****************************************************************************"
|
||||
|
||||
sudo -Eu app php82 $DST_DIR/update.php --user-set-password "admin:$RANDOM_PASS"
|
||||
sudo -Eu app php83 $DST_DIR/update.php --user-set-password "admin:$RANDOM_PASS"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -z "$ADMIN_USER_ACCESS_LEVEL" ]; then
|
||||
sudo -Eu app php82 $DST_DIR/update.php --user-set-access-level "admin:$ADMIN_USER_ACCESS_LEVEL"
|
||||
sudo -Eu app php83 $DST_DIR/update.php --user-set-access-level "admin:$ADMIN_USER_ACCESS_LEVEL"
|
||||
fi
|
||||
|
||||
if [ ! -z "$AUTO_CREATE_USER" ]; then
|
||||
sudo -Eu app /bin/sh -c "php82 $DST_DIR/update.php --user-exists $AUTO_CREATE_USER ||
|
||||
php82 $DST_DIR/update.php --force-yes --user-add \"$AUTO_CREATE_USER:$AUTO_CREATE_USER_PASS:$AUTO_CREATE_USER_ACCESS_LEVEL\""
|
||||
sudo -Eu app /bin/sh -c "php83 $DST_DIR/update.php --user-exists $AUTO_CREATE_USER ||
|
||||
php83 $DST_DIR/update.php --force-yes --user-add \"$AUTO_CREATE_USER:$AUTO_CREATE_USER_PASS:$AUTO_CREATE_USER_ACCESS_LEVEL\""
|
||||
|
||||
if [ ! -z "$AUTO_CREATE_USER_ENABLE_API" ]; then
|
||||
# TODO: remove || true later
|
||||
sudo -Eu app /bin/sh -c "php83 $DST_DIR/update.php --user-enable-api \"$AUTO_CREATE_USER:$AUTO_CREATE_USER_ENABLE_API\"" || true
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
rm -f /tmp/error.log && mkfifo /tmp/error.log && chown app:app /tmp/error.log
|
||||
|
@ -150,4 +160,4 @@ unset AUTO_CREATE_USER_PASS
|
|||
|
||||
touch $DST_DIR/.app_is_ready
|
||||
|
||||
exec /usr/sbin/php-fpm82 --nodaemonize --force-stderr
|
||||
exec /usr/sbin/php-fpm83 --nodaemonize --force-stderr
|
||||
|
|
|
@ -21,7 +21,7 @@ while ! pg_isready -h $TTRSS_DB_HOST -U $TTRSS_DB_USER; do
|
|||
done
|
||||
|
||||
sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \
|
||||
/etc/php82/php.ini
|
||||
/etc/php83/php.ini
|
||||
|
||||
DST_DIR=/var/www/html/tt-rss
|
||||
|
||||
|
@ -30,4 +30,4 @@ while [ ! -s $DST_DIR/config.php -a -e $DST_DIR/.app_is_ready ]; do
|
|||
sleep 3
|
||||
done
|
||||
|
||||
sudo -E -u app /usr/bin/php82 /var/www/html/tt-rss/update_daemon2.php
|
||||
sudo -E -u app /usr/bin/php83 /var/www/html/tt-rss/update_daemon2.php
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
FROM registry.fakecake.org/docker.io/nginx:alpine
|
||||
ARG PROXY_REGISTRY
|
||||
FROM ${PROXY_REGISTRY}nginx:alpine
|
||||
|
||||
HEALTHCHECK CMD curl --fail http://localhost/tt-rss/index.php || exit 1
|
||||
HEALTHCHECK CMD curl --fail http://localhost${APP_BASE}/index.php || exit 1
|
||||
|
||||
COPY nginx.conf /etc/nginx/templates/nginx.conf.template
|
||||
COPY .docker/web-nginx/nginx.conf /etc/nginx/templates/nginx.conf.template
|
||||
|
||||
# By default, nginx will send the php requests to "app" server, but this server
|
||||
# name can be overridden at runtime by passing an APP_UPSTREAM env var
|
||||
ENV APP_UPSTREAM=${APP_UPSTREAM:-app}
|
||||
|
||||
# Webroot (defaults to /var/www/html)
|
||||
ENV APP_WEB_ROOT=${APP_WEB_ROOT:-/var/www/html}
|
||||
|
||||
# Base location for tt-rss (defaults to /tt-rss)
|
||||
ENV APP_BASE=${APP_BASE:-/tt-rss}
|
||||
|
||||
# Resolver for nginx (kube-dns.kube-system.svc.cluster.local for k8s)
|
||||
ENV RESOLVER=${RESOLVER:-127.0.0.11}
|
||||
|
||||
# In order to make tt-rss appear on website root without /tt-rss/ set above as follows in .env:
|
||||
# APP_WEB_ROOT=/var/www/html/tt-rss
|
||||
# APP_BASE=
|
||||
|
||||
# It's necessary to set the following NGINX_ENVSUBST_OUTPUT_DIR env var to tell
|
||||
# nginx to replace the env vars of /etc/nginx/templates/nginx.conf.template
|
||||
# and put the result in /etc/nginx/nginx.conf (instead of /etc/nginx/conf.d/nginx.conf)
|
||||
|
|
|
@ -16,25 +16,24 @@ http {
|
|||
|
||||
index index.php;
|
||||
|
||||
upstream app {
|
||||
server ${APP_UPSTREAM}:9000;
|
||||
}
|
||||
resolver ${RESOLVER} valid=5s;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
root ${APP_WEB_ROOT};
|
||||
|
||||
root /var/www/html;
|
||||
|
||||
location /tt-rss/cache {
|
||||
location ${APP_BASE}/cache {
|
||||
aio threads;
|
||||
internal;
|
||||
}
|
||||
|
||||
location /tt-rss/backups {
|
||||
location ${APP_BASE}/backups {
|
||||
internal;
|
||||
}
|
||||
|
||||
rewrite ${APP_BASE}/healthz ${APP_BASE}/public.php?op=healthcheck;
|
||||
|
||||
location ~ \.php$ {
|
||||
# regex to split $uri to $fastcgi_script_name and $fastcgi_path
|
||||
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
|
||||
|
@ -50,7 +49,9 @@ http {
|
|||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
|
||||
fastcgi_pass app;
|
||||
set $backend "${APP_UPSTREAM}:9000";
|
||||
|
||||
fastcgi_pass $backend;
|
||||
}
|
||||
|
||||
location / {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.git/
|
||||
cache/
|
||||
plugins.local/
|
||||
templates.local/
|
||||
themes.local/
|
|
@ -0,0 +1,47 @@
|
|||
# Copy this file to .env before building the container. Put any local modifications here.
|
||||
|
||||
# Run FPM under this UID/GID.
|
||||
# OWNER_UID=1000
|
||||
# OWNER_GID=1000
|
||||
|
||||
# FPM settings.
|
||||
#PHP_WORKER_MAX_CHILDREN=5
|
||||
#PHP_WORKER_MEMORY_LIMIT=256M
|
||||
|
||||
# ADMIN_USER_* settings are applied on every startup.
|
||||
|
||||
# Set admin user password to this value. If not set, random password will be generated on startup, look for it in the 'app' container logs.
|
||||
#ADMIN_USER_PASS=
|
||||
|
||||
# Sets admin user access level to this value. Valid values:
|
||||
# -2 - forbidden to login
|
||||
# -1 - readonly
|
||||
# 0 - default user
|
||||
# 10 - admin
|
||||
#ADMIN_USER_ACCESS_LEVEL=
|
||||
|
||||
# Auto create another user (in addition to built-in admin) unless it already exists.
|
||||
#AUTO_CREATE_USER=
|
||||
#AUTO_CREATE_USER_PASS=
|
||||
#AUTO_CREATE_USER_ACCESS_LEVEL=0
|
||||
|
||||
# Default database credentials.
|
||||
TTRSS_DB_USER=postgres
|
||||
TTRSS_DB_NAME=postgres
|
||||
TTRSS_DB_PASS=password
|
||||
|
||||
# This is a fallback value for PHP CLI SAPI, it should be set to a fully-qualified tt-rss URL
|
||||
# TTRSS_SELF_URL_PATH=http://example.com/tt-rss
|
||||
|
||||
# You can customize other config.php defines by setting overrides here. See tt-rss/.docker/app/Dockerfile for complete list. Examples:
|
||||
|
||||
# TTRSS_PLUGINS=auth_remote
|
||||
# TTRSS_SINGLE_USER_MODE=true
|
||||
# TTRSS_SESSION_COOKIE_LIFETIME=2592000
|
||||
# TTRSS_FORCE_ARTICLE_PURGE=30
|
||||
# ...
|
||||
|
||||
# Bind exposed port to 127.0.0.1 to run behind reverse proxy on the same host. If you plan expose the container, remove "127.0.0.1:".
|
||||
HTTP_PORT=127.0.0.1:8280
|
||||
#HTTP_PORT=8280
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
Thumbs.db
|
||||
/.env
|
||||
/docker-compose.override.yml
|
||||
/.app_is_ready
|
||||
/messages.mo
|
||||
/node_modules
|
||||
|
@ -12,3 +14,5 @@ Thumbs.db
|
|||
/.vscode/settings.json
|
||||
/vendor/**/.git
|
||||
/.phpunit.result.cache
|
||||
/.phpstan-tmp
|
||||
/.tools/
|
||||
|
|
111
.gitlab-ci.yml
111
.gitlab-ci.yml
|
@ -1,20 +1,28 @@
|
|||
stages:
|
||||
- lint
|
||||
- build
|
||||
- test
|
||||
- publish
|
||||
|
||||
variables:
|
||||
ESLINT_PATHS: js plugins
|
||||
REGISTRY_PROJECT: cthulhoo
|
||||
|
||||
include:
|
||||
- project: 'ci/ci-templates'
|
||||
ref: master
|
||||
file: .ci-build-docker.yml
|
||||
file: .ci-build-docker-kaniko.yml
|
||||
- project: 'ci/ci-templates'
|
||||
ref: master
|
||||
file: .ci-lint-common.yml
|
||||
- project: 'ci/ci-templates'
|
||||
ref: master
|
||||
file: .ci-integration-test.yml
|
||||
|
||||
phpunit:
|
||||
extends: .phpunit
|
||||
variables:
|
||||
PHPUNIT_ARGS: --exclude integration --coverage-filter classes --coverage-filter include
|
||||
|
||||
eslint:
|
||||
extends: .eslint
|
||||
|
@ -22,39 +30,96 @@ eslint:
|
|||
phpstan:
|
||||
extends: .phpstan
|
||||
|
||||
ttrss-web-nginx:
|
||||
extends: .build-master
|
||||
variables:
|
||||
BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/web-nginx
|
||||
|
||||
ttrss-web-nginx:branch:
|
||||
extends: .build-branch
|
||||
variables:
|
||||
BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/web-nginx
|
||||
|
||||
ttrss-fpm-pgsql-static:
|
||||
extends: .build-master
|
||||
variables:
|
||||
BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/app
|
||||
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/web-nginx/Dockerfile
|
||||
|
||||
ttrss-fpm-pgsql-static:branch:
|
||||
extends: .build-branch
|
||||
variables:
|
||||
BUILD_CONTEXT: ${CI_PROJECT_DIR}/.docker/app
|
||||
extends: .build-branch
|
||||
variables:
|
||||
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/app/Dockerfile
|
||||
|
||||
ttrss-web-nginx:
|
||||
extends: .build-master-commit-only
|
||||
variables:
|
||||
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/web-nginx/Dockerfile
|
||||
|
||||
ttrss-fpm-pgsql-static:
|
||||
extends: .build-master-commit-only
|
||||
variables:
|
||||
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/app/Dockerfile
|
||||
|
||||
phpdoc:
|
||||
image:
|
||||
name: ${CI_DOCKER_IMAGE}
|
||||
stage: build
|
||||
image: ${PHP_IMAGE}
|
||||
stage: publish
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE != "web" && $PHPDOC_DEPLOY_SSH_KEY != null
|
||||
changes:
|
||||
- '**/*.php'
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $PHPDOC_DEPLOY_SSH_KEY != null
|
||||
when: manual
|
||||
script:
|
||||
- php81 /phpDocumentor.phar -d classes -d include -t phpdoc --visibility=public
|
||||
- php83 /phpDocumentor.phar -d classes -d include -t phpdoc --visibility=public
|
||||
- mkdir -p ~/.ssh &&
|
||||
cp ${PHPDOC_DEPLOY_SSH_KEY} ~/.ssh/id_ed25519 &&
|
||||
chmod 0600 ~/.ssh/id_ed25519
|
||||
- rsync -av -e 'ssh -o StrictHostKeyChecking=no' phpdoc/ ${PHPDOC_DEPLOY_HOST}:phpdoc/
|
||||
|
||||
phpunit-integration:
|
||||
image: ${PHP_IMAGE}
|
||||
variables:
|
||||
TEST_HELM_REPO: https://gitlab.tt-rss.org/tt-rss/helm-charts/tt-rss
|
||||
extends: .integration-test
|
||||
script:
|
||||
- export K8S_NAMESPACE=$(kubectl get pods -o=custom-columns=NS:.metadata.namespace | tail -1)
|
||||
- export API_URL="http://tt-rss-${CI_COMMIT_SHORT_SHA}-app.$K8S_NAMESPACE.svc.cluster.local/tt-rss/api/"
|
||||
- export TTRSS_DB_HOST=tt-rss-${CI_COMMIT_SHORT_SHA}-app.$K8S_NAMESPACE.svc.cluster.local
|
||||
- export TTRSS_DB_USER=postgres
|
||||
- export TTRSS_DB_NAME=postgres
|
||||
- export TTRSS_DB_PASS=password
|
||||
- php83 vendor/bin/phpunit --group integration --do-not-cache-result --log-junit phpunit-report.xml --coverage-cobertura phpunit-coverage.xml --coverage-text --colors=never
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit: phpunit-report.xml
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: phpunit-coverage.xml
|
||||
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
|
||||
|
||||
selenium:
|
||||
image: ${SELENIUM_IMAGE}
|
||||
variables:
|
||||
TEST_HELM_REPO: https://gitlab.tt-rss.org/tt-rss/helm-charts/tt-rss
|
||||
SELENIUM_GRID_ENDPOINT: http://selenium-hub.selenium-grid.svc.cluster.local:4444/wd/hub
|
||||
extends: .integration-test
|
||||
script:
|
||||
- export K8S_NAMESPACE=$(kubectl get pods -o=custom-columns=NS:.metadata.namespace | tail -1)
|
||||
- python3 tests/integration/selenium_test.py
|
||||
needs:
|
||||
- job: phpunit-integration
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit: selenium-report.xml
|
||||
|
||||
ttrss-web-nginx:publish:
|
||||
stage: publish
|
||||
extends: .build-master
|
||||
variables:
|
||||
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/web-nginx/Dockerfile
|
||||
|
||||
ttrss-fpm-pgsql-static:publish:
|
||||
stage: publish
|
||||
extends: .build-master
|
||||
variables:
|
||||
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/app/Dockerfile
|
||||
|
||||
update-demo:
|
||||
stage: publish
|
||||
image: ${HELM_IMAGE}
|
||||
variables:
|
||||
HELM_REPO: https://gitlab.tt-rss.org/tt-rss/helm-charts/tt-rss
|
||||
script:
|
||||
- git clone ${HELM_REPO} chart
|
||||
- helm upgrade --atomic --install tt-rss-demo chart --values .helm/values-demo.yaml --set imageTag=${CI_COMMIT_SHORT_SHA}
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_REGISTRY_USER != null && $UPDATE_DEMO == "true"
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
imageTag: latest
|
||||
|
||||
user:
|
||||
name: demo
|
||||
password: demo
|
||||
access_level: 10
|
||||
|
||||
virtualservice:
|
||||
suffix: k3s.kake
|
||||
additional_domains:
|
||||
- demo.tt-rss.org
|
||||
|
||||
web:
|
||||
root: /var/www/html/tt-rss
|
||||
base: ""
|
||||
|
||||
restart:
|
||||
enabled: true
|
|
@ -3,25 +3,33 @@
|
|||
"tasks": [
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "phpstan 8.1 (watcher)",
|
||||
"label": "phpstan (watcher)",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"fileLocation": ["relative", "${workspaceRoot}"],
|
||||
"owner": "phpstan-watcher-8.1",
|
||||
"fileLocation": [
|
||||
"relative",
|
||||
"${workspaceRoot}"
|
||||
],
|
||||
"owner": "phpstan-watcher",
|
||||
"pattern": {
|
||||
"regexp": "^/app/(.*?):([0-9\\?]*):(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"message": 3
|
||||
"regexp": "^/app/(.*?):([0-9\\?]*):(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"message": 3
|
||||
},
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "Using configuration file",
|
||||
"endsPattern": "All done"
|
||||
|
||||
}
|
||||
},
|
||||
"command": "${workspaceRoot}/utils/phpstan-watcher.sh",
|
||||
},
|
||||
"command": "chmod +x ${workspaceRoot}/utils/phpstan-watcher.sh && ${workspaceRoot}/utils/phpstan-watcher.sh"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "phpunit",
|
||||
"command": "chmod +x ${workspaceRoot}/utils/phpunit.sh && ${workspaceRoot}/utils/phpunit.sh",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "gulp",
|
||||
|
@ -30,9 +38,9 @@
|
|||
"label": "gulp: default",
|
||||
"options": {
|
||||
"env": {
|
||||
"PATH": "${env:PATH}:/usr/lib/sdk/node16/bin/"
|
||||
"PATH": "${env:PATH}:/usr/lib/sdk/node16/bin/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
45
backend.php
45
backend.php
|
@ -30,17 +30,14 @@
|
|||
$op = (string)clean($op);
|
||||
$method = (string)clean($method);
|
||||
|
||||
$scope = Tracer::start(__FILE__, ['tags' => json_encode($_REQUEST)]);
|
||||
|
||||
startup_gettext();
|
||||
|
||||
$script_started = microtime(true);
|
||||
|
||||
if (!init_plugins()) {
|
||||
$scope->close();
|
||||
return;
|
||||
}
|
||||
|
||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
||||
|
||||
header("Content-Type: text/json; charset=utf-8");
|
||||
|
||||
if (Config::get(Config::SINGLE_USER_MODE)) {
|
||||
|
@ -52,8 +49,7 @@
|
|||
header("Content-Type: text/json");
|
||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||
|
||||
$scope->getSpan()->setTag('error', Errors::E_UNAUTHORIZED);
|
||||
$scope->close();
|
||||
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
UserHelper::load_user_plugins($_SESSION["uid"]);
|
||||
|
@ -62,8 +58,7 @@
|
|||
if (Config::is_migration_needed()) {
|
||||
print Errors::to_json(Errors::E_SCHEMA_MISMATCH);
|
||||
|
||||
$scope->getSpan()->setTag('error', Errors::E_SCHEMA_MISMATCH);
|
||||
$scope->close();
|
||||
$span->setAttribute('error', Errors::E_SCHEMA_MISMATCH);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -115,7 +110,7 @@
|
|||
$op = "pluginhandler";
|
||||
} */
|
||||
|
||||
$op = str_replace("-", "_", $op);
|
||||
// $op = str_replace(, "_", $op);
|
||||
|
||||
$override = PluginHost::getInstance()->lookup_handler($op, $method);
|
||||
|
||||
|
@ -126,8 +121,7 @@
|
|||
header("Content-Type: text/json");
|
||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||
|
||||
$scope->getSpan()->setTag('error', Errors::E_UNAUTHORIZED);
|
||||
$scope->close();
|
||||
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -140,18 +134,16 @@
|
|||
}
|
||||
|
||||
if (implements_interface($handler, 'IHandler')) {
|
||||
$h_scope = Tracer::start("construct/$op");
|
||||
$span->addEvent("construct/$op");
|
||||
$handler->__construct($_REQUEST);
|
||||
$h_scope->close();
|
||||
|
||||
if (validate_csrf($csrf_token) || $handler->csrf_ignore($method)) {
|
||||
|
||||
$b_scope = Tracer::start("before/$method");
|
||||
$span->addEvent("before/$method");
|
||||
$before = $handler->before($method);
|
||||
$b_scope->close();
|
||||
|
||||
if ($before) {
|
||||
$m_scope = Tracer::start("method/$method");
|
||||
$span->addEvent("method/$method");
|
||||
if ($method && method_exists($handler, $method)) {
|
||||
$reflection = new ReflectionMethod($handler, $method);
|
||||
|
||||
|
@ -161,7 +153,7 @@
|
|||
user_error("Refusing to invoke method $method of handler $op which has required parameters.", E_USER_WARNING);
|
||||
header("Content-Type: text/json");
|
||||
|
||||
$m_scope->getSpan()->setTag('error', Errors::E_UNAUTHORIZED);
|
||||
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
|
||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||
}
|
||||
} else {
|
||||
|
@ -170,24 +162,19 @@
|
|||
} else {
|
||||
header("Content-Type: text/json");
|
||||
|
||||
$m_scope->getSpan()->setTag('error', Errors::E_UNKNOWN_METHOD);
|
||||
$span->setAttribute('error', Errors::E_UNKNOWN_METHOD);
|
||||
print Errors::to_json(Errors::E_UNKNOWN_METHOD, ["info" => get_class($handler) . "->$method"]);
|
||||
}
|
||||
}
|
||||
$m_scope->close();
|
||||
|
||||
$a_scope = Tracer::start("after/$method");
|
||||
$span->addEvent("after/$method");
|
||||
$handler->after();
|
||||
$a_scope->close();
|
||||
|
||||
$scope->close();
|
||||
return;
|
||||
} else {
|
||||
header("Content-Type: text/json");
|
||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||
|
||||
$scope->getSpan()->setTag('error', Errors::E_UNAUTHORIZED);
|
||||
$scope->close();
|
||||
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
@ -195,8 +182,7 @@
|
|||
header("Content-Type: text/json");
|
||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||
|
||||
$scope->getSpan()->setTag('error', Errors::E_UNAUTHORIZED);
|
||||
$scope->close();
|
||||
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -205,5 +191,4 @@
|
|||
header("Content-Type: text/json");
|
||||
print Errors::to_json(Errors::E_UNKNOWN_METHOD, [ "info" => (isset($handler) ? get_class($handler) : "UNKNOWN:".$op) . "->$method"]);
|
||||
|
||||
$scope->getSpan()->setTag('error', Errors::E_UNKNOWN_METHOD);
|
||||
$scope->close();
|
||||
$span->setAttribute('error', Errors::E_UNKNOWN_METHOD);
|
||||
|
|
|
@ -691,20 +691,6 @@ class API extends Handler {
|
|||
}
|
||||
}
|
||||
|
||||
$params = array(
|
||||
"feed" => $feed_id,
|
||||
"limit" => $limit,
|
||||
"view_mode" => $view_mode,
|
||||
"cat_view" => $is_cat,
|
||||
"search" => $search,
|
||||
"override_order" => $order,
|
||||
"offset" => $offset,
|
||||
"since_id" => $since_id,
|
||||
"include_children" => $include_nested,
|
||||
"check_first_id" => $check_first_id,
|
||||
"skip_first_id_check" => $skip_first_id_check
|
||||
);
|
||||
|
||||
$qfh_ret = [];
|
||||
|
||||
if (!$is_cat && is_numeric($feed_id) && $feed_id < PLUGIN_FEED_BASE_INDEX && $feed_id > LABEL_BASE_INDEX) {
|
|
@ -90,7 +90,7 @@ class Article extends Handler_Protected {
|
|||
SET tsvector_combined = to_tsvector( :ts_content)
|
||||
WHERE id = :id");
|
||||
$params = [
|
||||
":ts_content" => mb_substr(strip_tags($content ), 0, 900000),
|
||||
":ts_content" => mb_substr(\Soundasleep\Html2Text::convert($content), 0, 900000),
|
||||
":id" => $ref_id];
|
||||
$sth->execute($params);
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ class Article extends Handler_Protected {
|
|||
SET tsvector_combined = to_tsvector( :ts_content)
|
||||
WHERE id = :id");
|
||||
$params = [
|
||||
":ts_content" => mb_substr(strip_tags($content ), 0, 900000),
|
||||
":ts_content" => mb_substr(\Soundasleep\Html2Text::convert($content), 0, 900000),
|
||||
":id" => $ref_id];
|
||||
$sth->execute($params);
|
||||
}
|
||||
|
@ -298,7 +298,7 @@ class Article extends Handler_Protected {
|
|||
* @return array{'formatted': string, 'entries': array<int, array<string, mixed>>}
|
||||
*/
|
||||
static function _format_enclosures(int $id, bool $always_display_enclosures, string $article_content, bool $hide_images = false): array {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$enclosures = self::_get_enclosures($id);
|
||||
$enclosures_formatted = "";
|
||||
|
@ -326,7 +326,7 @@ class Article extends Handler_Protected {
|
|||
$enclosures_formatted, $enclosures, $id, $always_display_enclosures, $article_content, $hide_images);
|
||||
|
||||
if (!empty($enclosures_formatted)) {
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return [
|
||||
'formatted' => $enclosures_formatted,
|
||||
'entries' => []
|
||||
|
@ -370,7 +370,7 @@ class Article extends Handler_Protected {
|
|||
}
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return $rv;
|
||||
}
|
||||
|
||||
|
@ -378,7 +378,7 @@ class Article extends Handler_Protected {
|
|||
* @return array<int, string>
|
||||
*/
|
||||
static function _get_tags(int $id, int $owner_uid = 0, ?string $tag_cache = null): array {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$a_id = $id;
|
||||
|
||||
|
@ -427,7 +427,7 @@ class Article extends Handler_Protected {
|
|||
$sth->execute([$tags_str, $id, $owner_uid]);
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return $tags;
|
||||
}
|
||||
|
||||
|
@ -522,7 +522,7 @@ class Article extends Handler_Protected {
|
|||
* @return array<int, array<int, int|string>>
|
||||
*/
|
||||
static function _get_labels(int $id, ?int $owner_uid = null): array {
|
||||
$scope = Tracer::start(__METHOD__, []);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$rv = array();
|
||||
|
||||
|
@ -569,7 +569,7 @@ class Article extends Handler_Protected {
|
|||
else
|
||||
Labels::update_cache($owner_uid, $id, array("no-labels" => 1));
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return $rv;
|
||||
}
|
||||
|
@ -581,7 +581,7 @@ class Article extends Handler_Protected {
|
|||
* @return array<int, Article::ARTICLE_KIND_*|string>
|
||||
*/
|
||||
static function _get_image(array $enclosures, string $content, string $site_url, array $headline) {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$article_image = "";
|
||||
$article_stream = "";
|
||||
|
@ -606,7 +606,7 @@ class Article extends Handler_Protected {
|
|||
foreach ($elems as $e) {
|
||||
if ($e->nodeName == "iframe") {
|
||||
$matches = [];
|
||||
if ($rrr = preg_match("/\/embed\/([\w-]+)/", $e->getAttribute("src"), $matches)) {
|
||||
if (preg_match("/\/embed\/([\w-]+)/", $e->getAttribute("src"), $matches)) {
|
||||
$article_image = "https://img.youtube.com/vi/" . $matches[1] . "/hqdefault.jpg";
|
||||
$article_stream = "https://youtu.be/" . $matches[1];
|
||||
$article_kind = Article::ARTICLE_KIND_YOUTUBE;
|
||||
|
@ -660,7 +660,7 @@ class Article extends Handler_Protected {
|
|||
if ($article_stream && $cache->exists(sha1($article_stream)))
|
||||
$article_stream = $cache->get_url(sha1($article_stream));
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return [$article_image, $article_stream, $article_kind];
|
||||
}
|
||||
|
@ -675,7 +675,7 @@ class Article extends Handler_Protected {
|
|||
if (count($article_ids) == 0)
|
||||
return [];
|
||||
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$entries = ORM::for_table('ttrss_entries')
|
||||
->table_alias('e')
|
||||
|
@ -696,7 +696,7 @@ class Article extends Handler_Protected {
|
|||
}
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return array_unique($rv);
|
||||
}
|
||||
|
@ -709,7 +709,7 @@ class Article extends Handler_Protected {
|
|||
if (count($article_ids) == 0)
|
||||
return [];
|
||||
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$entries = ORM::for_table('ttrss_entries')
|
||||
->table_alias('e')
|
||||
|
@ -723,7 +723,7 @@ class Article extends Handler_Protected {
|
|||
array_push($rv, $entry->feed_id);
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return array_unique($rv);
|
||||
}
|
|
@ -46,10 +46,7 @@ class Config {
|
|||
* garbage unicode characters with this option, try setting it to a blank string. */
|
||||
const MYSQL_CHARSET = "MYSQL_CHARSET";
|
||||
|
||||
/** this should be set to a fully qualified URL used to access
|
||||
* your tt-rss instance over the net, such as: https://example.com/tt-rss/
|
||||
* if your tt-rss instance is behind a reverse proxy, use external URL.
|
||||
* tt-rss will likely help you pick correct value for this on startup */
|
||||
/** this is a fallback falue for the CLI SAPI, it should be set to a fully-qualified tt-rss URL */
|
||||
const SELF_URL_PATH = "SELF_URL_PATH";
|
||||
|
||||
/** operate in single user mode, disables all functionality related to
|
||||
|
@ -193,10 +190,10 @@ class Config {
|
|||
const HTTP_429_THROTTLE_INTERVAL = "HTTP_429_THROTTLE_INTERVAL";
|
||||
|
||||
/** host running Jaeger collector to receive traces (disabled if empty) */
|
||||
const JAEGER_REPORTING_HOST = "JAEGER_REPORTING_HOST";
|
||||
const OPENTELEMETRY_ENDPOINT = "OPENTELEMETRY_ENDPOINT";
|
||||
|
||||
/** Jaeger service name */
|
||||
const JAEGER_SERVICE_NAME = "JAEGER_SERVICE_NAME";
|
||||
const OPENTELEMETRY_SERVICE = "OPENTELEMETRY_SERVICE";
|
||||
|
||||
/** default values for all global configuration options */
|
||||
private const _DEFAULTS = [
|
||||
|
@ -207,7 +204,7 @@ class Config {
|
|||
Config::DB_PASS => [ "", Config::T_STRING ],
|
||||
Config::DB_PORT => [ "5432", Config::T_STRING ],
|
||||
Config::MYSQL_CHARSET => [ "UTF8", Config::T_STRING ],
|
||||
Config::SELF_URL_PATH => [ "", Config::T_STRING ],
|
||||
Config::SELF_URL_PATH => [ "https://example.com/tt-rss", Config::T_STRING ],
|
||||
Config::SINGLE_USER_MODE => [ "", Config::T_BOOL ],
|
||||
Config::SIMPLE_UPDATE_MODE => [ "", Config::T_BOOL ],
|
||||
Config::PHP_EXECUTABLE => [ "/usr/bin/php", Config::T_STRING ],
|
||||
|
@ -255,8 +252,8 @@ class Config {
|
|||
Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)',
|
||||
Config::T_STRING ],
|
||||
Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ],
|
||||
Config::JAEGER_REPORTING_HOST => [ "", Config::T_STRING ],
|
||||
Config::JAEGER_SERVICE_NAME => [ "tt-rss", Config::T_STRING ],
|
||||
Config::OPENTELEMETRY_ENDPOINT => [ "", Config::T_STRING ],
|
||||
Config::OPENTELEMETRY_SERVICE => [ "tt-rss", Config::T_STRING ],
|
||||
];
|
||||
|
||||
/** @var Config|null */
|
||||
|
@ -322,7 +319,7 @@ class Config {
|
|||
* @return array<string, mixed>|string
|
||||
*/
|
||||
private function _get_version(bool $as_string = true) {
|
||||
$root_dir = dirname(__DIR__);
|
||||
$root_dir = self::get_self_dir();
|
||||
|
||||
if (empty($this->version)) {
|
||||
$this->version["status"] = -1;
|
||||
|
@ -416,7 +413,7 @@ class Config {
|
|||
private function _get_migrations() : Db_Migrations {
|
||||
if (empty($this->migrations)) {
|
||||
$this->migrations = new Db_Migrations();
|
||||
$this->migrations->initialize(dirname(__DIR__) . "/sql", "ttrss_version", true, self::SCHEMA_VERSION);
|
||||
$this->migrations->initialize(self::get_self_dir() . "/sql", "ttrss_version", true, self::SCHEMA_VERSION);
|
||||
}
|
||||
|
||||
return $this->migrations;
|
||||
|
@ -474,32 +471,30 @@ class Config {
|
|||
return $instance->_get($param);
|
||||
}
|
||||
|
||||
/** this returns Config::SELF_URL_PATH sans trailing slash */
|
||||
static function get_self_url() : string {
|
||||
return preg_replace("#/*$#", "", self::get(Config::SELF_URL_PATH));
|
||||
}
|
||||
|
||||
static function is_server_https() : bool {
|
||||
return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
|
||||
(!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https');
|
||||
}
|
||||
|
||||
/** generates reference self_url_path (no trailing slash) */
|
||||
static function make_self_url() : string {
|
||||
$proto = self::is_server_https() ? 'https' : 'http';
|
||||
|
||||
$self_url_path = $proto . '://' . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
|
||||
|
||||
$self_url_path = preg_replace("/\w+\.php(\?.*$)?$/", "", $self_url_path);
|
||||
#$self_url_path = preg_replace("/(\?.*$)?$/", "", $self_url_path);
|
||||
|
||||
if (substr($self_url_path, -1) === "/") {
|
||||
return substr($self_url_path, 0, -1);
|
||||
/** returns fully-qualified external URL to tt-rss (no trailing slash)
|
||||
* SELF_URL_PATH configuration variable is used as a fallback for the CLI SAPI
|
||||
* */
|
||||
static function get_self_url(bool $always_detect = false) : string {
|
||||
if (!$always_detect && php_sapi_name() == "cli") {
|
||||
return self::get(Config::SELF_URL_PATH);
|
||||
} else {
|
||||
return $self_url_path;
|
||||
$proto = self::is_server_https() ? 'https' : 'http';
|
||||
|
||||
$self_url_path = $proto . '://' . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
|
||||
$self_url_path = preg_replace("/(\/api\/{1,})?(\w+\.php)?(\?.*$)?$/", "", $self_url_path);
|
||||
|
||||
if (substr($self_url_path, -1) === "/") {
|
||||
return substr($self_url_path, 0, -1);
|
||||
} else {
|
||||
return $self_url_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* sanity check stuff */
|
||||
|
||||
/** checks for mysql tables not using InnoDB (tt-rss is incompatible with MyISAM)
|
||||
|
@ -620,27 +615,9 @@ class Config {
|
|||
|
||||
// skip check for CLI scripts so that we could install database schema if it is missing.
|
||||
if (php_sapi_name() != "cli") {
|
||||
|
||||
if (self::get_schema_version() < 0) {
|
||||
array_push($errors, "Base database schema is missing. Either load it manually or perform a migration (<code>update.php --update-schema</code>)");
|
||||
}
|
||||
|
||||
$ref_self_url_path = self::make_self_url();
|
||||
|
||||
if ($ref_self_url_path) {
|
||||
$ref_self_url_path = preg_replace("/\w+\.php$/", "", $ref_self_url_path);
|
||||
}
|
||||
|
||||
if (self::get_self_url() == "http://example.org/tt-rss") {
|
||||
$hint = $ref_self_url_path ? "(possible value: <b>$ref_self_url_path</b>)" : "";
|
||||
array_push($errors,
|
||||
"Please set SELF_URL_PATH to the correct value for your server: $hint");
|
||||
}
|
||||
|
||||
if (self::get_self_url() != $ref_self_url_path) {
|
||||
array_push($errors,
|
||||
"Please set SELF_URL_PATH to the correct value detected for your server: <b>$ref_self_url_path</b> (you're using: <b>" . self::get_self_url() . "</b>)");
|
||||
}
|
||||
}
|
||||
|
||||
if (self::get(Config::DB_TYPE) == "mysql") {
|
||||
|
@ -666,7 +643,9 @@ class Config {
|
|||
}
|
||||
}
|
||||
|
||||
if (count($errors) > 0 && php_sapi_name() != "cli") { ?>
|
||||
if (count($errors) > 0 && php_sapi_name() != "cli") {
|
||||
http_response_code(503); ?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
@ -724,4 +703,9 @@ class Config {
|
|||
static function get_user_agent(): string {
|
||||
return sprintf(self::get(self::HTTP_USER_AGENT), self::get_version());
|
||||
}
|
||||
|
||||
static function get_self_dir() : string {
|
||||
return dirname(__DIR__); # we're in classes/Config.php
|
||||
}
|
||||
|
||||
}
|
|
@ -145,7 +145,7 @@ class Counters {
|
|||
* @return array<int, array<string, int|string>>
|
||||
*/
|
||||
private static function get_feeds(array $feed_ids = null): array {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$ret = [];
|
||||
|
||||
|
@ -212,7 +212,7 @@ class Counters {
|
|||
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
@ -221,7 +221,7 @@ class Counters {
|
|||
* @return array<int, array<string, int|string>>
|
||||
*/
|
||||
private static function get_global(): array {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$ret = [
|
||||
[
|
||||
|
@ -239,7 +239,7 @@ class Counters {
|
|||
"counter" => $subcribed_feeds
|
||||
]);
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
@ -248,7 +248,7 @@ class Counters {
|
|||
* @return array<int, array<string, int|string>>
|
||||
*/
|
||||
private static function get_virt(): array {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$ret = [];
|
||||
|
||||
|
@ -295,7 +295,7 @@ class Counters {
|
|||
}
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
@ -304,7 +304,7 @@ class Counters {
|
|||
* @return array<int, array<string, int|string>>
|
||||
*/
|
||||
static function get_labels(array $label_ids = null): array {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$ret = [];
|
||||
|
||||
|
@ -356,7 +356,7 @@ class Counters {
|
|||
array_push($ret, $cv);
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return $ret;
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@ class Db_Migrations {
|
|||
|
||||
$sth = $this->pdo->query("SELECT * FROM {$this->migrations_table}");
|
||||
|
||||
if ($res = $sth->fetch()) {
|
||||
if ($sth->fetch()) {
|
||||
$sth = $this->pdo->prepare("UPDATE {$this->migrations_table} SET schema_version = ?");
|
||||
} else {
|
||||
$sth = $this->pdo->prepare("INSERT INTO {$this->migrations_table} (schema_version) VALUES (?)");
|
|
@ -5,6 +5,8 @@ class Debug {
|
|||
const LOG_VERBOSE = 1;
|
||||
const LOG_EXTENDED = 2;
|
||||
|
||||
const SEPARATOR = "<-{log-separator}->";
|
||||
|
||||
const ALL_LOG_LEVELS = [
|
||||
Debug::LOG_DISABLED,
|
||||
Debug::LOG_NORMAL,
|
||||
|
@ -35,6 +37,7 @@ class Debug {
|
|||
private static bool $enabled = false;
|
||||
private static bool $quiet = false;
|
||||
private static ?string $logfile = null;
|
||||
private static bool $enable_html = false;
|
||||
|
||||
private static int $loglevel = self::LOG_NORMAL;
|
||||
|
||||
|
@ -82,58 +85,77 @@ class Debug {
|
|||
}
|
||||
}
|
||||
|
||||
public static function enable_html(bool $enable) : void {
|
||||
self::$enable_html = $enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Debug::LOG_* $level log level
|
||||
*/
|
||||
public static function log(string $message, int $level = Debug::LOG_NORMAL): bool {
|
||||
|
||||
if (!self::$enabled || self::$loglevel < $level) return false;
|
||||
if (!self::$enabled || self::$loglevel < $level) return false;
|
||||
|
||||
$ts = date("H:i:s", time());
|
||||
if (function_exists('posix_getpid')) {
|
||||
$ts = "$ts/" . posix_getpid();
|
||||
}
|
||||
$ts = date("H:i:s", time());
|
||||
if (function_exists('posix_getpid')) {
|
||||
$ts = "$ts/" . posix_getpid();
|
||||
}
|
||||
|
||||
if (self::$logfile) {
|
||||
$fp = fopen(self::$logfile, 'a+');
|
||||
$orig_message = $message;
|
||||
|
||||
if ($fp) {
|
||||
$locked = false;
|
||||
if ($message === self::SEPARATOR) {
|
||||
$message = self::$enable_html ? "<hr/>" :
|
||||
"=================================================================================================================================";
|
||||
}
|
||||
|
||||
if (function_exists("flock")) {
|
||||
$tries = 0;
|
||||
if (self::$logfile) {
|
||||
$fp = fopen(self::$logfile, 'a+');
|
||||
|
||||
// try to lock logfile for writing
|
||||
while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB)) {
|
||||
sleep(1);
|
||||
++$tries;
|
||||
}
|
||||
if ($fp) {
|
||||
$locked = false;
|
||||
|
||||
if (!$locked) {
|
||||
fclose($fp);
|
||||
user_error("Unable to lock debugging log file: " . self::$logfile, E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (function_exists("flock")) {
|
||||
$tries = 0;
|
||||
|
||||
fputs($fp, "[$ts] $message\n");
|
||||
// try to lock logfile for writing
|
||||
while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB)) {
|
||||
sleep(1);
|
||||
++$tries;
|
||||
}
|
||||
|
||||
if (function_exists("flock")) {
|
||||
flock($fp, LOCK_UN);
|
||||
}
|
||||
if (!$locked) {
|
||||
fclose($fp);
|
||||
user_error("Unable to lock debugging log file: " . self::$logfile, E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($fp);
|
||||
fputs($fp, "[$ts] $message\n");
|
||||
|
||||
if (self::$quiet)
|
||||
return false;
|
||||
if (function_exists("flock")) {
|
||||
flock($fp, LOCK_UN);
|
||||
}
|
||||
|
||||
} else {
|
||||
user_error("Unable to open debugging log file: " . self::$logfile, E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
fclose($fp);
|
||||
|
||||
print "[$ts] $message\n";
|
||||
if (self::$quiet)
|
||||
return false;
|
||||
|
||||
} else {
|
||||
user_error("Unable to open debugging log file: " . self::$logfile, E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$enable_html) {
|
||||
if ($orig_message === self::SEPARATOR) {
|
||||
print "$message\n";
|
||||
} else {
|
||||
print "<span class='log-timestamp'>$ts</span> <span class='log-message'>$message</span>\n";
|
||||
}
|
||||
} else {
|
||||
print "[$ts] $message\n";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
class Digest
|
||||
{
|
||||
static function send_headlines_digests(): void {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$user_limit = 15; // amount of users to process (e.g. emails to send out)
|
||||
$limit = 1000; // maximum amount of headlines to include
|
||||
|
@ -17,7 +17,7 @@ class Digest
|
|||
|
||||
$pdo = Db::pdo();
|
||||
|
||||
$res = $pdo->query("SELECT id,email FROM ttrss_users
|
||||
$res = $pdo->query("SELECT id, login, email FROM ttrss_users
|
||||
WHERE email != '' AND (last_digest_sent IS NULL OR $interval_qpart)");
|
||||
|
||||
while ($line = $res->fetch()) {
|
||||
|
@ -77,7 +77,7 @@ class Digest
|
|||
}
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
Debug::log("All done.");
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,8 @@ class Digest
|
|||
$tpl->readTemplateFromFile("digest_template_html.txt");
|
||||
$tpl_t->readTemplateFromFile("digest_template.txt");
|
||||
|
||||
$user_tz_string = get_pref(Prefs::USER_TIMEZONE, $user_id);
|
||||
$user_tz_string = Prefs::get(Prefs::USER_TIMEZONE, $user_id);
|
||||
$min_score = Prefs::get(Prefs::DIGEST_MIN_SCORE, $user_id);
|
||||
|
||||
if ($user_tz_string == 'Automatic')
|
||||
$user_tz_string = 'GMT';
|
||||
|
@ -136,10 +137,10 @@ class Digest
|
|||
AND $interval_qpart
|
||||
AND ttrss_user_entries.owner_uid = :user_id
|
||||
AND unread = true
|
||||
AND score >= 0
|
||||
AND score >= :min_score
|
||||
ORDER BY ttrss_feed_categories.title, ttrss_feeds.title, score DESC, date_updated DESC
|
||||
LIMIT " . (int)$limit);
|
||||
$sth->execute([':user_id' => $user_id]);
|
||||
$sth->execute([':user_id' => $user_id, ':min_score' => $min_score]);
|
||||
|
||||
$headlines_count = 0;
|
||||
$headlines = array();
|
||||
|
@ -191,7 +192,7 @@ class Digest
|
|||
|
||||
$tpl_t->addBlock('article');
|
||||
|
||||
if ($headlines[$i]['feed_title'] != $headlines[$i + 1]['feed_title']) {
|
||||
if (!isset($headlines[$i + 1]) || $headlines[$i]['feed_title'] != $headlines[$i + 1]['feed_title']) {
|
||||
$tpl->addBlock('feed');
|
||||
$tpl_t->addBlock('feed');
|
||||
}
|
|
@ -206,7 +206,7 @@ class DiskCache implements Cache_Adapter {
|
|||
}
|
||||
|
||||
public function __construct(string $dir) {
|
||||
foreach (PluginHost::getInstance()->get_plugins() as $n => $p) {
|
||||
foreach (PluginHost::getInstance()->get_plugins() as $p) {
|
||||
if (implements_interface($p, "Cache_Adapter")) {
|
||||
|
||||
/** @var Cache_Adapter $p */
|
||||
|
@ -221,9 +221,11 @@ class DiskCache implements Cache_Adapter {
|
|||
}
|
||||
|
||||
public function remove(string $filename): bool {
|
||||
$scope = Tracer::start(__METHOD__, ['filename' => $filename]);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
$span->setAttribute('file.name', $filename);
|
||||
|
||||
$rc = $this->adapter->remove($filename);
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return $rc;
|
||||
}
|
||||
|
@ -243,14 +245,16 @@ class DiskCache implements Cache_Adapter {
|
|||
return $this->adapter->make_dir();
|
||||
}
|
||||
|
||||
/** @param string|null $filename null means check that cache directory itself is writable */
|
||||
public function is_writable(?string $filename = null): bool {
|
||||
return $this->adapter->is_writable(basename($filename));
|
||||
return $this->adapter->is_writable($filename ? basename($filename) : null);
|
||||
}
|
||||
|
||||
public function exists(string $filename): bool {
|
||||
$scope = Tracer::start(__METHOD__, ['filename' => $filename]);
|
||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
||||
$span->addEvent("DiskCache::exists: $filename");
|
||||
|
||||
$rc = $this->adapter->exists(basename($filename));
|
||||
$scope->close();
|
||||
|
||||
return $rc;
|
||||
}
|
||||
|
@ -259,9 +263,11 @@ class DiskCache implements Cache_Adapter {
|
|||
* @return int|false -1 if the file doesn't exist, false if an error occurred, size in bytes otherwise
|
||||
*/
|
||||
public function get_size(string $filename) {
|
||||
$scope = Tracer::start(__METHOD__, ['filename' => $filename]);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
$span->setAttribute('file.name', $filename);
|
||||
|
||||
$rc = $this->adapter->get_size(basename($filename));
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return $rc;
|
||||
}
|
||||
|
@ -272,9 +278,9 @@ class DiskCache implements Cache_Adapter {
|
|||
* @return int|false Bytes written or false if an error occurred.
|
||||
*/
|
||||
public function put(string $filename, $data) {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
$rc = $this->adapter->put(basename($filename), $data);
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return $rc;
|
||||
}
|
||||
|
@ -320,7 +326,8 @@ class DiskCache implements Cache_Adapter {
|
|||
}
|
||||
|
||||
public function send(string $filename) {
|
||||
$scope = Tracer::start(__METHOD__, ['filename' => $filename]);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
$span->setAttribute('file.name', $filename);
|
||||
|
||||
$filename = basename($filename);
|
||||
|
||||
|
@ -328,8 +335,8 @@ class DiskCache implements Cache_Adapter {
|
|||
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
|
||||
echo "File not found.";
|
||||
|
||||
$scope->getSpan()->setTag('error', '404 not found');
|
||||
$scope->close();
|
||||
$span->setAttribute('error', '404 not found');
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -339,8 +346,8 @@ class DiskCache implements Cache_Adapter {
|
|||
if (($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '') == $gmt_modified || ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') == $file_mtime) {
|
||||
header('HTTP/1.1 304 Not Modified');
|
||||
|
||||
$scope->getSpan()->setTag('error', '304 not modified');
|
||||
$scope->close();
|
||||
$span->setAttribute('error', '304 not modified');
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -359,8 +366,8 @@ class DiskCache implements Cache_Adapter {
|
|||
|
||||
print "Stored file has disallowed content type ($mimetype)";
|
||||
|
||||
$scope->getSpan()->setTag('error', '400 disallowed content type');
|
||||
$scope->close();
|
||||
$span->setAttribute('error', '400 disallowed content type');
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -382,11 +389,11 @@ class DiskCache implements Cache_Adapter {
|
|||
|
||||
header_remove("Pragma");
|
||||
|
||||
$scope->getSpan()->setTag('mimetype', $mimetype);
|
||||
$span->setAttribute('mimetype', $mimetype);
|
||||
|
||||
$rc = $this->adapter->send($filename);
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return $rc;
|
||||
}
|
||||
|
@ -417,12 +424,13 @@ class DiskCache implements Cache_Adapter {
|
|||
// plugins work on original source URLs used before caching
|
||||
// NOTE: URLs should be already absolutized because this is called after sanitize()
|
||||
static public function rewrite_urls(string $str): string {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
||||
$span->addEvent("DiskCache::rewrite_urls");
|
||||
|
||||
$res = trim($str);
|
||||
|
||||
if (!$res) {
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@ -436,7 +444,7 @@ class DiskCache implements Cache_Adapter {
|
|||
$need_saving = false;
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$e_scope = Tracer::start('entry', ['tagName' => $entry->tagName]);
|
||||
$span->addEvent("entry: " . $entry->tagName);
|
||||
|
||||
foreach (array('src', 'poster') as $attr) {
|
||||
if ($entry->hasAttribute($attr)) {
|
||||
|
@ -469,8 +477,6 @@ class DiskCache implements Cache_Adapter {
|
|||
|
||||
$entry->setAttribute("srcset", RSSUtils::encode_srcset($matches));
|
||||
}
|
||||
|
||||
$e_scope->close();
|
||||
}
|
||||
|
||||
if ($need_saving) {
|
||||
|
@ -481,8 +487,6 @@ class DiskCache implements Cache_Adapter {
|
|||
}
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -52,7 +52,6 @@ class FeedParser {
|
|||
}
|
||||
|
||||
function init() : void {
|
||||
$root = $this->doc->firstChild;
|
||||
$xpath = new DOMXPath($this->doc);
|
||||
$xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
|
||||
$xpath->registerNamespace('atom03', 'http://purl.org/atom/ns#');
|
|
@ -65,7 +65,8 @@ class Feeds extends Handler_Protected {
|
|||
|
||||
$disable_cache = false;
|
||||
|
||||
$scope = Tracer::start(__METHOD__, [], func_get_args());
|
||||
$span = Tracer::start(__METHOD__);
|
||||
$span->setAttribute('func.args', json_encode(func_get_args()));
|
||||
|
||||
$reply = [];
|
||||
$rgba_cache = [];
|
||||
|
@ -168,7 +169,7 @@ class Feeds extends Handler_Protected {
|
|||
$reply['search_query'] = [$search, $search_language];
|
||||
$reply['vfeed_group_enabled'] = $vfeed_group_enabled;
|
||||
|
||||
$p_scope = Tracer::start('plugin_menu_items');
|
||||
$span->addEvent('plugin_menu_items');
|
||||
|
||||
$plugin_menu_items = "";
|
||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2,
|
||||
|
@ -202,15 +203,13 @@ class Feeds extends Handler_Protected {
|
|||
},
|
||||
$feed, $cat_view, $qfh_ret);
|
||||
|
||||
$p_scope->close();
|
||||
|
||||
$a_scope = Tracer::start('articles');
|
||||
$span->addEvent('articles');
|
||||
|
||||
$headlines_count = 0;
|
||||
|
||||
if ($result instanceof PDOStatement) {
|
||||
while ($line = $result->fetch(PDO::FETCH_ASSOC)) {
|
||||
$aa_scope = Tracer::start('article', ['id' => $line['id']]);
|
||||
$span->addEvent('article: ' . $line['id']);
|
||||
|
||||
++$headlines_count;
|
||||
|
||||
|
@ -370,7 +369,7 @@ class Feeds extends Handler_Protected {
|
|||
//setting feed headline background color, needs to change text color based on dark/light
|
||||
$fav_color = $line['favicon_avg_color'] ?? false;
|
||||
|
||||
$c_scope = Tracer::start('colors');
|
||||
$span->addEvent("colors");
|
||||
|
||||
require_once "colors.php";
|
||||
|
||||
|
@ -386,7 +385,7 @@ class Feeds extends Handler_Protected {
|
|||
$line['feed_bg_color'] = 'rgba(' . implode(",", $rgba_cache[$feed_id]) . ',0.3)';
|
||||
}
|
||||
|
||||
$c_scope->close();
|
||||
$span->addEvent("HOOK_RENDER_ARTICLE_CDM");
|
||||
|
||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_CDM,
|
||||
function ($result, $plugin) use (&$line) {
|
||||
|
@ -403,13 +402,9 @@ class Feeds extends Handler_Protected {
|
|||
unset($line[$k]);
|
||||
|
||||
array_push($reply['content'], $line);
|
||||
|
||||
$aa_scope->close();
|
||||
}
|
||||
}
|
||||
|
||||
$a_scope->close();
|
||||
|
||||
if (!$headlines_count) {
|
||||
|
||||
if ($result instanceof PDOStatement) {
|
||||
|
@ -469,7 +464,7 @@ class Feeds extends Handler_Protected {
|
|||
}
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return array($topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply);
|
||||
}
|
||||
|
@ -708,6 +703,23 @@ class Feeds extends Handler_Protected {
|
|||
body.css_loading * {
|
||||
display : none;
|
||||
}
|
||||
|
||||
.feed-xml {
|
||||
color : green;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color : gray;
|
||||
}
|
||||
|
||||
.log-timestamp::before {
|
||||
content: "["
|
||||
}
|
||||
|
||||
.log-timestamp::after {
|
||||
content: "]"
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
dojoConfig = {
|
||||
|
@ -738,7 +750,7 @@ class Feeds extends Handler_Protected {
|
|||
<h1>Feed Debugger: <?= "$feed_id: " . $this->_get_title($feed_id) ?></h1>
|
||||
<div class="content">
|
||||
<form method="post" action="" dojoType="dijit.form.Form">
|
||||
<?= \Controls\hidden_tag("op", "feeds") ?>
|
||||
<?= \Controls\hidden_tag("op", "Feeds") ?>
|
||||
<?= \Controls\hidden_tag("method", "updatedebugger") ?>
|
||||
<?= \Controls\hidden_tag("csrf_token", $csrf_token) ?>
|
||||
<?= \Controls\hidden_tag("action", "do_update") ?>
|
||||
|
@ -759,6 +771,10 @@ class Feeds extends Handler_Protected {
|
|||
<label class="checkbox"><?= \Controls\checkbox_tag("force_rehash", isset($_REQUEST["force_rehash"])) ?> Force rehash</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="narrow">
|
||||
<label class="checkbox"><?= \Controls\checkbox_tag("dump_feed_xml", isset($_REQUEST["dump_feed_xml"])) ?> Dump feed XML</label>
|
||||
</fieldset>
|
||||
|
||||
<?= \Controls\submit_tag("Continue") ?>
|
||||
</form>
|
||||
|
||||
|
@ -767,7 +783,7 @@ class Feeds extends Handler_Protected {
|
|||
<pre><?php
|
||||
|
||||
if ($do_update) {
|
||||
RSSUtils::update_rss_feed($feed_id, true);
|
||||
RSSUtils::update_rss_feed($feed_id, true, true);
|
||||
}
|
||||
|
||||
?></pre>
|
||||
|
@ -962,7 +978,9 @@ class Feeds extends Handler_Protected {
|
|||
* @throws PDOException
|
||||
*/
|
||||
static function _get_counters($feed, bool $is_cat = false, bool $unread_only = false, ?int $owner_uid = null): int {
|
||||
$scope = Tracer::start(__METHOD__, [], func_get_args());
|
||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
||||
|
||||
$span->addEvent(__METHOD__ . ": $feed ($is_cat)");
|
||||
|
||||
$n_feed = (int) $feed;
|
||||
$need_entries = false;
|
||||
|
@ -986,14 +1004,14 @@ class Feeds extends Handler_Protected {
|
|||
$handler = PluginHost::getInstance()->get_feed_handler($feed_id);
|
||||
if (implements_interface($handler, 'IVirtualFeed')) {
|
||||
/** @var IVirtualFeed $handler */
|
||||
$scope->close();
|
||||
//$span->end();
|
||||
return $handler->get_unread($feed_id);
|
||||
} else {
|
||||
$scope->close();
|
||||
//$span->end();
|
||||
return 0;
|
||||
}
|
||||
} else if ($n_feed == Feeds::FEED_RECENTLY_READ) {
|
||||
$scope->close();
|
||||
//$span->end();
|
||||
return 0;
|
||||
// tags
|
||||
} else if ($feed != "0" && $n_feed == 0) {
|
||||
|
@ -1007,7 +1025,7 @@ class Feeds extends Handler_Protected {
|
|||
$row = $sth->fetch();
|
||||
|
||||
// Handle 'SUM()' returning null if there are no results
|
||||
$scope->close();
|
||||
//$span->end();
|
||||
return $row["count"] ?? 0;
|
||||
|
||||
} else if ($n_feed == Feeds::FEED_STARRED) {
|
||||
|
@ -1041,7 +1059,7 @@ class Feeds extends Handler_Protected {
|
|||
|
||||
$label_id = Labels::feed_to_label_id($feed);
|
||||
|
||||
$scope->close();
|
||||
//$span->end();
|
||||
return self::_get_label_unread($label_id, $owner_uid);
|
||||
}
|
||||
|
||||
|
@ -1061,7 +1079,7 @@ class Feeds extends Handler_Protected {
|
|||
$sth->execute([$owner_uid]);
|
||||
$row = $sth->fetch();
|
||||
|
||||
$scope->close();
|
||||
//$span->end();
|
||||
return $row["unread"];
|
||||
|
||||
} else {
|
||||
|
@ -1074,7 +1092,7 @@ class Feeds extends Handler_Protected {
|
|||
$sth->execute([$feed, $owner_uid]);
|
||||
$row = $sth->fetch();
|
||||
|
||||
$scope->close();
|
||||
//$span->end();
|
||||
return $row["unread"];
|
||||
}
|
||||
}
|
||||
|
@ -1114,8 +1132,6 @@ class Feeds extends Handler_Protected {
|
|||
return ["code" => 8];
|
||||
}
|
||||
|
||||
$pdo = Db::pdo();
|
||||
|
||||
$url = UrlHelper::validate($url);
|
||||
|
||||
if (!$url) return ["code" => 2];
|
||||
|
@ -1127,7 +1143,7 @@ class Feeds extends Handler_Protected {
|
|||
},
|
||||
$url, $auth_login, $auth_pass);
|
||||
|
||||
$contents = UrlHelper::fetch($url, false, $auth_login, $auth_pass);
|
||||
$contents = UrlHelper::fetch(['url' => $url, 'login' => $auth_login, 'pass' => $auth_pass]);
|
||||
|
||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_SUBSCRIBE_FEED,
|
||||
function ($result) use (&$contents) {
|
||||
|
@ -1259,8 +1275,6 @@ class Feeds extends Handler_Protected {
|
|||
*/
|
||||
static function _find_by_title(string $title, bool $cat = false, int $owner_uid = 0) {
|
||||
|
||||
$res = false;
|
||||
|
||||
if ($cat) {
|
||||
$res = ORM::for_table('ttrss_feed_categories')
|
||||
->where('owner_uid', $owner_uid ? $owner_uid : $_SESSION['uid'])
|
||||
|
@ -1472,7 +1486,8 @@ class Feeds extends Handler_Protected {
|
|||
*/
|
||||
static function _get_headlines($params): array {
|
||||
|
||||
$scope = Tracer::start(__METHOD__, [], func_get_args());
|
||||
$span = Tracer::start(__METHOD__);
|
||||
$span->setAttribute('func.args', json_encode(func_get_args()));
|
||||
|
||||
$pdo = Db::pdo();
|
||||
|
||||
|
@ -1716,7 +1731,6 @@ class Feeds extends Handler_Protected {
|
|||
$vfeed_query_part = $override_vfeed;
|
||||
}
|
||||
|
||||
$feed_title = "";
|
||||
$feed_site_url = "";
|
||||
$last_error = "";
|
||||
$last_updated = "";
|
||||
|
@ -1966,7 +1980,7 @@ class Feeds extends Handler_Protected {
|
|||
$res = $pdo->query($query);
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return array($res, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words, $first_id, $vfeed_query_part != "", $query_error_override);
|
||||
}
|
||||
|
@ -2138,7 +2152,7 @@ class Feeds extends Handler_Protected {
|
|||
}
|
||||
|
||||
static function _clear_access_keys(int $owner_uid): void {
|
||||
$key = ORM::for_table('ttrss_access_keys')
|
||||
ORM::for_table('ttrss_access_keys')
|
||||
->where('owner_uid', $owner_uid)
|
||||
->delete_many();
|
||||
}
|
||||
|
@ -2149,7 +2163,7 @@ class Feeds extends Handler_Protected {
|
|||
* @see Handler_Public#generate_syndicated_feed()
|
||||
*/
|
||||
static function _update_access_key(string $feed_id, bool $is_cat, int $owner_uid): ?string {
|
||||
$key = ORM::for_table('ttrss_access_keys')
|
||||
ORM::for_table('ttrss_access_keys')
|
||||
->where('owner_uid', $owner_uid)
|
||||
->where('feed_id', $feed_id)
|
||||
->where('is_cat', $is_cat)
|
||||
|
@ -2193,8 +2207,6 @@ class Feeds extends Handler_Protected {
|
|||
if (!$purge_interval) $purge_interval = self::_get_purge_interval($feed_id);
|
||||
|
||||
$pdo = Db::pdo();
|
||||
|
||||
$owner_uid = false;
|
||||
$rows_deleted = 0;
|
||||
|
||||
$sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");
|
|
@ -28,7 +28,7 @@ class Handler implements IHandler {
|
|||
/**
|
||||
* @param mixed $p
|
||||
*/
|
||||
protected static function _param_to_bool($p): bool {
|
||||
public static function _param_to_bool($p): bool {
|
||||
$p = clean($p);
|
||||
return $p && ($p !== "f" && $p !== "false");
|
||||
}
|
|
@ -192,7 +192,7 @@ class Handler_Public extends Handler {
|
|||
|
||||
while ($line = $result->fetch()) {
|
||||
|
||||
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content_preview"]), 100, '...'));
|
||||
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
|
||||
$line["tags"] = Article::_get_tags($line["id"], $owner_uid);
|
||||
|
||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
|
||||
|
@ -834,5 +834,11 @@ class Handler_Public extends Handler {
|
|||
exit;
|
||||
}
|
||||
|
||||
// implicit Config::sanity_check() does the actual checking */
|
||||
public function healthcheck() : void {
|
||||
header("Content-Type: text/plain");
|
||||
print "OK";
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
interface IAuthModule2 extends IAuthModule {
|
||||
function change_password(int $owner_uid, string $old_password, string $new_password) : string;
|
||||
}
|
|
@ -12,7 +12,7 @@ class Mailer {
|
|||
$to_address = $params["to_address"];
|
||||
$subject = $params["subject"];
|
||||
$message = $params["message"];
|
||||
$message_html = $params["message_html"] ?? "";
|
||||
// $message_html = $params["message_html"] ?? "";
|
||||
$from_name = $params["from_name"] ?? Config::get(Config::SMTP_FROM_NAME);
|
||||
$from_address = $params["from_address"] ?? Config::get(Config::SMTP_FROM_ADDRESS);
|
||||
$additional_headers = $params["headers"] ?? [];
|
|
@ -612,8 +612,6 @@ class OPML extends Handler_Protected {
|
|||
function opml_import(int $owner_uid, string $filename = "") {
|
||||
if (!$owner_uid) return;
|
||||
|
||||
$doc = false;
|
||||
|
||||
if (!$filename) {
|
||||
if ($_FILES['opml_file']['error'] != 0) {
|
||||
print_error(T_sprintf("Upload failed with error code %d",
|
||||
|
@ -644,8 +642,6 @@ class OPML extends Handler_Protected {
|
|||
return false;
|
||||
}
|
||||
|
||||
$loaded = false;
|
||||
|
||||
$doc = new DOMDocument();
|
||||
|
||||
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
|
|
@ -339,12 +339,15 @@ class PluginHost {
|
|||
*/
|
||||
function chain_hooks_callback(string $hook, Closure $callback, &...$args): void {
|
||||
$method = strtolower((string)$hook);
|
||||
$scope = Tracer::start(__METHOD__, ['hook' => $hook]);
|
||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
||||
$span->addEvent("chain_hooks_callback: $hook");
|
||||
|
||||
foreach ($this->get_hooks((string)$hook) as $plugin) {
|
||||
//Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
|
||||
|
||||
$p_scope = Tracer::start("$hook - " . get_class($plugin));
|
||||
//$p_span = Tracer::start("$hook - " . get_class($plugin));
|
||||
|
||||
$span->addEvent("$hook - " . get_class($plugin));
|
||||
|
||||
try {
|
||||
if ($callback($plugin->$method(...$args), $plugin))
|
||||
|
@ -355,10 +358,10 @@ class PluginHost {
|
|||
user_error($err, E_USER_WARNING);
|
||||
}
|
||||
|
||||
$p_scope->close();
|
||||
//$p_span->end();
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
//$span->end();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -424,6 +427,8 @@ class PluginHost {
|
|||
* @param PluginHost::KIND_* $kind
|
||||
*/
|
||||
function load_all(int $kind, int $owner_uid = null, bool $skip_init = false): void {
|
||||
$span = Tracer::start(__METHOD__);
|
||||
$span->setAttribute('func.args', json_encode(func_get_args()));
|
||||
|
||||
$plugins = [...(glob("plugins/*") ?: []), ...(glob("plugins.local/*") ?: [])];
|
||||
$plugins = array_filter($plugins, "is_dir");
|
||||
|
@ -432,13 +437,16 @@ class PluginHost {
|
|||
asort($plugins);
|
||||
|
||||
$this->load(join(",", $plugins), (int)$kind, $owner_uid, $skip_init);
|
||||
|
||||
$span->end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PluginHost::KIND_* $kind
|
||||
*/
|
||||
function load(string $classlist, int $kind, int $owner_uid = null, bool $skip_init = false): void {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
$span->setAttribute('func.args', json_encode(func_get_args()));
|
||||
|
||||
$plugins = explode(",", $classlist);
|
||||
|
||||
|
@ -448,16 +456,15 @@ class PluginHost {
|
|||
$class = trim($class);
|
||||
$class_file = strtolower(basename(clean($class)));
|
||||
|
||||
$p_scope = Tracer::start("loading $class_file");
|
||||
$span->addEvent("$class_file: load");
|
||||
|
||||
// try system plugin directory first
|
||||
$file = dirname(__DIR__) . "/plugins/$class_file/init.php";
|
||||
$file = Config::get_self_dir() . "/plugins/$class_file/init.php";
|
||||
|
||||
if (!file_exists($file)) {
|
||||
$file = dirname(__DIR__) . "/plugins.local/$class_file/init.php";
|
||||
$file = Config::get_self_dir() . "/plugins.local/$class_file/init.php";
|
||||
|
||||
if (!file_exists($file)) {
|
||||
$p_scope->close();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -476,8 +483,7 @@ class PluginHost {
|
|||
|
||||
$_SESSION["safe_mode"] = 1;
|
||||
|
||||
$p_scope->getSpan()->setTag('error', 'plugin is blacklisted');
|
||||
$p_scope->close();
|
||||
$span->setAttribute('error', 'plugin is blacklisted');
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -489,8 +495,7 @@ class PluginHost {
|
|||
} catch (Error $err) {
|
||||
user_error($err, E_USER_WARNING);
|
||||
|
||||
$p_scope->getSpan()->setTag('error', $err);
|
||||
$p_scope->close();
|
||||
$span->setAttribute('error', $err);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -501,8 +506,7 @@ class PluginHost {
|
|||
if ($plugin_api < self::API_VERSION) {
|
||||
user_error("Plugin $class is not compatible with current API version (need: " . self::API_VERSION . ", got: $plugin_api)", E_USER_WARNING);
|
||||
|
||||
$p_scope->getSpan()->setTag('error', 'plugin is not compatible with API version');
|
||||
$p_scope->close();
|
||||
$span->setAttribute('error', 'plugin is not compatible with API version');
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -511,7 +515,7 @@ class PluginHost {
|
|||
_bind_textdomain_codeset($class, "UTF-8");
|
||||
}
|
||||
|
||||
$i_scope = Tracer::start('init and register plugin');
|
||||
$span->addEvent("$class_file: initialize");
|
||||
|
||||
try {
|
||||
switch ($kind) {
|
||||
|
@ -537,17 +541,12 @@ class PluginHost {
|
|||
} catch (Error $err) {
|
||||
user_error($err, E_USER_WARNING);
|
||||
}
|
||||
|
||||
$i_scope->close();
|
||||
|
||||
}
|
||||
}
|
||||
$p_scope->close();
|
||||
}
|
||||
|
||||
$this->load_data();
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
}
|
||||
|
||||
function is_system(Plugin $plugin): bool {
|
||||
|
@ -640,26 +639,28 @@ class PluginHost {
|
|||
}
|
||||
|
||||
private function load_data(): void {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
||||
$span->addEvent('load plugin data');
|
||||
|
||||
if ($this->owner_uid && !$this->data_loaded && get_schema_version() > 100) {
|
||||
if ($this->owner_uid && !$this->data_loaded && Config::get_schema_version() > 100) {
|
||||
$sth = $this->pdo->prepare("SELECT name, content FROM ttrss_plugin_storage
|
||||
WHERE owner_uid = ?");
|
||||
$sth->execute([$this->owner_uid]);
|
||||
|
||||
while ($line = $sth->fetch()) {
|
||||
$span->addEvent($line["name"] . ': unserialize');
|
||||
|
||||
$this->storage[$line["name"]] = unserialize($line["content"]);
|
||||
}
|
||||
|
||||
$this->data_loaded = true;
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
}
|
||||
|
||||
private function save_data(string $plugin): void {
|
||||
if ($this->owner_uid) {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
||||
$span->addEvent(__METHOD__ . ": $plugin");
|
||||
|
||||
if (!$this->pdo_data)
|
||||
$this->pdo_data = Db::instance()->pdo_connect();
|
||||
|
@ -687,7 +688,6 @@ class PluginHost {
|
|||
}
|
||||
|
||||
$this->pdo_data->commit();
|
||||
$scope->close();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -889,7 +889,7 @@ class PluginHost {
|
|||
}
|
||||
|
||||
/**
|
||||
* handled by classes/pluginhandler.php, requires valid session
|
||||
* handled by classes/PluginHandler.php, requires valid session
|
||||
*
|
||||
* @param array<int|string, mixed> $params
|
||||
*/
|
|
@ -39,12 +39,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||
/**
|
||||
* @return array<int, array<string, bool|int|string>>
|
||||
*/
|
||||
private function get_category_items(int $cat_id): array {
|
||||
|
||||
if (clean($_REQUEST['mode'] ?? 0) != 2)
|
||||
$search = $_SESSION["prefs_feed_search"] ?? "";
|
||||
else
|
||||
$search = "";
|
||||
private function get_category_items(int $cat_id, string $search): array {
|
||||
|
||||
// first one is set by API
|
||||
$show_empty_cats = self::_param_to_bool($_REQUEST['force_show_empty'] ?? false) ||
|
||||
|
@ -64,7 +59,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||
'id' => 'CAT:' . $feed_category->id,
|
||||
'bare_id' => (int)$feed_category->id,
|
||||
'name' => $feed_category->title,
|
||||
'items' => $this->get_category_items($feed_category->id),
|
||||
'items' => $this->get_category_items($feed_category->id, $search),
|
||||
'checkbox' => false,
|
||||
'type' => 'category',
|
||||
'unread' => -1,
|
||||
|
@ -121,7 +116,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||
if (clean($_REQUEST['mode'] ?? 0) != 2)
|
||||
$search = $_SESSION["prefs_feed_search"] ?? "";
|
||||
else
|
||||
$search = "";
|
||||
$search = $_REQUEST['search'] ?? '';
|
||||
|
||||
$root = array();
|
||||
$root['id'] = 'root';
|
||||
|
@ -226,7 +221,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||
'bare_id' => (int) $feed_category->id,
|
||||
'auxcounter' => -1,
|
||||
'name' => $feed_category->title,
|
||||
'items' => $this->get_category_items($feed_category->id),
|
||||
'items' => $this->get_category_items($feed_category->id, $search),
|
||||
'checkbox' => false,
|
||||
'type' => 'category',
|
||||
'unread' => -1,
|
||||
|
@ -620,7 +615,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||
?>
|
||||
|
||||
<?= \Controls\hidden_tag("ids", $feed_ids) ?>
|
||||
<?= \Controls\hidden_tag("op", "pref-feeds") ?>
|
||||
<?= \Controls\hidden_tag("op", "Pref_Feeds") ?>
|
||||
<?= \Controls\hidden_tag("method", "batchEditSave") ?>
|
||||
|
||||
<div dojoType="dijit.layout.TabContainer" style="height : 450px">
|
||||
|
@ -782,16 +777,6 @@ class Pref_Feeds extends Handler_Protected {
|
|||
$qparams = [];
|
||||
|
||||
switch ($k) {
|
||||
case "title":
|
||||
$qpart = "title = ?";
|
||||
$qparams = [$feed_title];
|
||||
break;
|
||||
|
||||
case "feed_url":
|
||||
$qpart = "feed_url = ?";
|
||||
$qparams = [$this->pdo->quote($feed_url)];
|
||||
break;
|
||||
|
||||
case "update_interval":
|
||||
$qpart = "update_interval = ?";
|
||||
$qparams = [$upd_intl];
|
||||
|
@ -851,7 +836,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||
|
||||
case "feed_language":
|
||||
$qpart = "feed_language = ?";
|
||||
$qparams = [$this->pdo->quote($feed_language)];
|
||||
$qparams = [$feed_language];
|
||||
break;
|
||||
|
||||
}
|
||||
|
@ -969,7 +954,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||
</div>
|
||||
<div style="padding : 0px" dojoType="dijit.layout.ContentPane" region="center">
|
||||
<div dojoType="fox.PrefFeedStore" jsId="feedStore"
|
||||
url="backend.php?op=pref-feeds&method=getfeedtree">
|
||||
url="backend.php?op=Pref_Feeds&method=getfeedtree">
|
||||
</div>
|
||||
|
||||
<div dojoType="lib.CheckBoxStoreModel" jsId="feedModel" store="feedStore"
|
||||
|
@ -998,7 +983,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||
<label class='dijitButton'><?= __("Choose file...") ?>
|
||||
<input style='display : none' id='opml_file' name='opml_file' type='file'>
|
||||
</label>
|
||||
<input type='hidden' name='op' value='pref-feeds'>
|
||||
<input type='hidden' name='op' value='Pref_Feeds'>
|
||||
<input type='hidden' name='csrf_token' value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type='hidden' name='method' value='importOpml'>
|
||||
<button dojoType='dijit.form.Button' class='alt-primary' onclick="return Helpers.OPML.import()" type="submit">
|
||||
|
@ -1090,6 +1075,9 @@ class Pref_Feeds extends Handler_Protected {
|
|||
* @return array<string, mixed>
|
||||
*/
|
||||
private function feedlist_init_cat(int $cat_id): array {
|
||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
||||
$span->addEvent(__METHOD__ . ": $cat_id");
|
||||
|
||||
return [
|
||||
'id' => 'CAT:' . $cat_id,
|
||||
'items' => array(),
|
||||
|
@ -1104,7 +1092,8 @@ class Pref_Feeds extends Handler_Protected {
|
|||
* @return array<string, mixed>
|
||||
*/
|
||||
private function feedlist_init_feed(int $feed_id, ?string $title = null, bool $unread = false, string $error = '', string $updated = ''): array {
|
||||
$scope = Tracer::start(__METHOD__, []);
|
||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
||||
$span->addEvent(__METHOD__ . ": $feed_id");
|
||||
|
||||
if (!$title)
|
||||
$title = Feeds::_get_title($feed_id, false);
|
||||
|
@ -1112,8 +1101,6 @@ class Pref_Feeds extends Handler_Protected {
|
|||
if ($unread === false)
|
||||
$unread = Feeds::_get_counters($feed_id, false, true);
|
||||
|
||||
$scope->close();
|
||||
|
||||
return [
|
||||
'id' => 'FEED:' . $feed_id,
|
||||
'name' => $title,
|
|
@ -696,7 +696,7 @@ class Pref_Filters extends Handler_Protected {
|
|||
</div>
|
||||
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
|
||||
<div dojoType="fox.PrefFilterStore" jsId="filterStore"
|
||||
url="backend.php?op=pref-filters&method=getfiltertree">
|
||||
url="backend.php?op=Pref_Filters&method=getfiltertree">
|
||||
</div>
|
||||
<div dojoType="lib.CheckBoxStoreModel" jsId="filterModel" store="filterStore"
|
||||
query="{id:'root'}" rootId="root" rootLabel="Filters"
|
|
@ -199,7 +199,7 @@ class Pref_Labels extends Handler_Protected {
|
|||
|
||||
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
|
||||
<div dojoType='dojo.data.ItemFileWriteStore' jsId='labelStore'
|
||||
url='backend.php?op=pref-labels&method=getlabeltree'>
|
||||
url='backend.php?op=Pref_Labels&method=getlabeltree'>
|
||||
</div>
|
||||
|
||||
<div dojoType='lib.CheckBoxStoreModel' jsId='labelModel' store='labelStore'
|
|
@ -74,6 +74,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
Prefs::DIGEST_ENABLE,
|
||||
Prefs::DIGEST_CATCHUP,
|
||||
Prefs::DIGEST_PREFERRED_TIME,
|
||||
Prefs::DIGEST_MIN_SCORE,
|
||||
],
|
||||
__('Advanced') => [
|
||||
Prefs::BLACKLISTED_TAGS,
|
||||
|
@ -127,6 +128,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
Prefs::DEBUG_HEADLINE_IDS => array(__("Show article and feed IDs"), __("In the headlines buffer")),
|
||||
Prefs::DISABLE_CONDITIONAL_COUNTERS => array(__("Disable conditional counter updates"), __("May increase server load")),
|
||||
Prefs::CDM_ENABLE_GRID => array(__("Grid view"), __("On wider screens, if always expanded")),
|
||||
Prefs::DIGEST_MIN_SCORE => array(__("Required score"), __("Include articles with this or above score")),
|
||||
];
|
||||
|
||||
// hidden in the main prefs UI (use to hide things that have description set above)
|
||||
|
@ -174,7 +176,8 @@ class Pref_Prefs extends Handler_Protected {
|
|||
|
||||
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
|
||||
|
||||
if (method_exists($authenticator, "change_password")) {
|
||||
if (implements_interface($authenticator, "IAuthModule2")) {
|
||||
/** @var IAuthModule2 $authenticator */
|
||||
print format_notice($authenticator->change_password($_SESSION["uid"], $old_pw, $new_pw));
|
||||
} else {
|
||||
print "ERROR: ".format_error("Function not supported by authentication module.");
|
||||
|
@ -285,7 +288,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
?>
|
||||
<form dojoType='dijit.form.Form'>
|
||||
|
||||
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
|
||||
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
|
||||
<?= \Controls\hidden_tag("method", "changePersonalData") ?>
|
||||
|
||||
<script type="dojo/method" event="onSubmit" args="evt">
|
||||
|
@ -325,16 +328,14 @@ class Pref_Prefs extends Handler_Protected {
|
|||
$authenticator = false;
|
||||
}
|
||||
|
||||
$otp_enabled = UserHelper::is_otp_enabled($_SESSION["uid"]);
|
||||
|
||||
if ($authenticator && method_exists($authenticator, "change_password")) {
|
||||
if ($authenticator && implements_interface($authenticator, "IAuthModule2")) {
|
||||
?>
|
||||
|
||||
<div style='display : none' id='pwd_change_infobox'></div>
|
||||
|
||||
<form dojoType='dijit.form.Form'>
|
||||
|
||||
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
|
||||
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
|
||||
<?= \Controls\hidden_tag("method", "changepassword") ?>
|
||||
|
||||
<!-- TODO: return JSON the backend call -->
|
||||
|
@ -426,7 +427,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
?>
|
||||
|
||||
<form dojoType='dijit.form.Form'>
|
||||
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
|
||||
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
|
||||
<?= \Controls\hidden_tag("method", "otpdisable") ?>
|
||||
|
||||
<!-- TODO: return JSON from the backend call -->
|
||||
|
@ -473,7 +474,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
|
||||
<form dojoType='dijit.form.Form'>
|
||||
|
||||
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
|
||||
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
|
||||
<?= \Controls\hidden_tag("method", "otpenable") ?>
|
||||
|
||||
<fieldset>
|
||||
|
@ -689,7 +690,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
}
|
||||
|
||||
} else if (in_array($pref_name, [Prefs::FRESH_ARTICLE_MAX_AGE,
|
||||
Prefs::PURGE_OLD_DAYS, Prefs::LONG_DATE_FORMAT, Prefs::SHORT_DATE_FORMAT])) {
|
||||
Prefs::PURGE_OLD_DAYS, Prefs::LONG_DATE_FORMAT, Prefs::SHORT_DATE_FORMAT, Prefs::DIGEST_MIN_SCORE])) {
|
||||
|
||||
if ($pref_name == Prefs::PURGE_OLD_DAYS && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
|
||||
$attributes = ["disabled" => true, "required" => true];
|
||||
|
@ -746,7 +747,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
private function index_prefs(): void {
|
||||
?>
|
||||
<form dojoType='dijit.form.Form' id='changeSettingsForm'>
|
||||
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
|
||||
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
|
||||
<?= \Controls\hidden_tag("method", "saveconfig") ?>
|
||||
|
||||
<script type="dojo/method" event="onSubmit" args="evt, quit">
|
||||
|
@ -836,7 +837,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
?>
|
||||
<form dojoType="dijit.form.Form" id="changePluginsForm">
|
||||
|
||||
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
|
||||
<?= \Controls\hidden_tag("op", "Pref_Prefs") ?>
|
||||
<?= \Controls\hidden_tag("method", "setplugins") ?>
|
||||
|
||||
<div dojoType="dijit.layout.BorderContainer" gutters="false">
|
||||
|
@ -936,7 +937,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
<script type='dojo/method' event='onSelected' args='evt'>
|
||||
if (this.domNode.querySelector('.loading'))
|
||||
window.setTimeout(() => {
|
||||
xhr.post("backend.php", {op: 'pref-prefs', method: 'index_auth'}, (reply) => {
|
||||
xhr.post("backend.php", {op: 'Pref_Prefs', method: 'index_auth'}, (reply) => {
|
||||
this.attr('content', reply);
|
||||
});
|
||||
}, 100);
|
||||
|
@ -1061,7 +1062,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
* @return array<int, array{'plugin': string, 'rv': array{'stdout': false|string, 'stderr': false|string, 'git_status': int, 'need_update': bool}|null}>
|
||||
*/
|
||||
static function _get_updated_plugins(): array {
|
||||
$root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
|
||||
$root_dir = Config::get_self_dir();
|
||||
$plugin_dirs = array_filter(glob("$root_dir/plugins.local/*"), "is_dir");
|
||||
$rv = [];
|
||||
|
||||
|
@ -1185,7 +1186,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
$plugin_name = basename(clean($_REQUEST['plugin']));
|
||||
$status = 0;
|
||||
|
||||
$plugin_dir = dirname(dirname(__DIR__)) . "/plugins.local/$plugin_name";
|
||||
$plugin_dir = Config::get_self_dir() . "/plugins.local/$plugin_name";
|
||||
|
||||
if (is_dir($plugin_dir)) {
|
||||
$status = $this->_recursive_rmdir($plugin_dir);
|
||||
|
@ -1199,7 +1200,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
|
||||
$plugin_name = basename(clean($_REQUEST['plugin']));
|
||||
$all_plugins = $this->_get_available_plugins();
|
||||
$plugin_dir = dirname(dirname(__DIR__)) . "/plugins.local";
|
||||
$plugin_dir = Config::get_self_dir() . "/plugins.local";
|
||||
|
||||
$work_dir = "$plugin_dir/plugin-installer";
|
||||
|
||||
|
@ -1224,13 +1225,10 @@ class Pref_Prefs extends Handler_Protected {
|
|||
$proc = proc_open("git clone " . escapeshellarg($plugin['clone_url']) . " " . $tmp_dir,
|
||||
$descriptorspec, $pipes, sys_get_temp_dir());
|
||||
|
||||
$status = 0;
|
||||
|
||||
if (is_resource($proc)) {
|
||||
$rv["stdout"] = stream_get_contents($pipes[1]);
|
||||
$rv["stderr"] = stream_get_contents($pipes[2]);
|
||||
$status = proc_close($proc);
|
||||
$rv["git_status"] = $status;
|
||||
$rv["git_status"] = proc_close($proc);
|
||||
|
||||
// yeah I know about mysterious RC = -1
|
||||
if (file_exists("$tmp_dir/init.php")) {
|
||||
|
@ -1306,7 +1304,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
function checkForPluginUpdates(): void {
|
||||
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::CHECK_FOR_UPDATES) && Config::get(Config::CHECK_FOR_PLUGIN_UPDATES)) {
|
||||
$plugin_name = $_REQUEST["name"] ?? "";
|
||||
$root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
|
||||
$root_dir = Config::get_self_dir();
|
||||
|
||||
$rv = empty($plugin_name) ? self::_get_updated_plugins() : [
|
||||
["plugin" => $plugin_name, "rv" => self::_plugin_needs_update($root_dir, $plugin_name)],
|
||||
|
@ -1324,8 +1322,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
$plugins = array_filter($plugins, 'strlen');
|
||||
}
|
||||
|
||||
# we're in classes/pref/
|
||||
$root_dir = dirname(dirname(__DIR__));
|
||||
$root_dir = Config::get_self_dir();
|
||||
|
||||
$rv = [];
|
||||
|
||||
|
@ -1548,7 +1545,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||
}
|
||||
|
||||
function deleteAppPasswords(): void {
|
||||
$passwords = ORM::for_table('ttrss_app_passwords')
|
||||
ORM::for_table('ttrss_app_passwords')
|
||||
->where('owner_uid', $_SESSION['uid'])
|
||||
->where_in('id', $_REQUEST['ids'] ?? [])
|
||||
->delete_many();
|
|
@ -194,12 +194,16 @@ class Pref_System extends Handler_Administrative {
|
|||
}
|
||||
</script>
|
||||
|
||||
<?= \Controls\hidden_tag("op", "pref-system") ?>
|
||||
<?= \Controls\hidden_tag("op", "Pref_System") ?>
|
||||
<?= \Controls\hidden_tag("method", "sendTestEmail") ?>
|
||||
|
||||
<?php
|
||||
$user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]);
|
||||
?>
|
||||
|
||||
<fieldset>
|
||||
<label><?= __("To:") ?></label>
|
||||
<?= \Controls\input_tag("mail_address", "", "text", ['required' => 1]) ?>
|
||||
<?= \Controls\input_tag("mail_address",$user->email, "text", ['required' => 1]) ?>
|
||||
<?= \Controls\submit_tag(__("Send test email")) ?>
|
||||
<span style="display: none; margin-left : 10px" class="alert alert-error" id="mail-test-result">...</span>
|
||||
</fieldset>
|
||||
|
@ -210,7 +214,7 @@ class Pref_System extends Handler_Administrative {
|
|||
<script type='dojo/method' event='onSelected' args='evt'>
|
||||
if (this.domNode.querySelector('.loading'))
|
||||
window.setTimeout(() => {
|
||||
xhr.post("backend.php", {op: 'pref-system', method: 'getphpinfo'}, (reply) => {
|
||||
xhr.post("backend.php", {op: 'Pref_System', method: 'getphpinfo'}, (reply) => {
|
||||
this.attr('content', `<div class='phpinfo'>${reply}</div>`);
|
||||
});
|
||||
}, 200);
|
|
@ -61,6 +61,7 @@ class Prefs {
|
|||
const DISABLE_CONDITIONAL_COUNTERS = "DISABLE_CONDITIONAL_COUNTERS";
|
||||
const WIDESCREEN_MODE = "WIDESCREEN_MODE";
|
||||
const CDM_ENABLE_GRID = "CDM_ENABLE_GRID";
|
||||
const DIGEST_MIN_SCORE = "DIGEST_MIN_SCORE";
|
||||
|
||||
private const _DEFAULTS = [
|
||||
Prefs::PURGE_OLD_DAYS => [ 60, Config::T_INT ],
|
||||
|
@ -122,6 +123,7 @@ class Prefs {
|
|||
Prefs::DISABLE_CONDITIONAL_COUNTERS => [ false, Config::T_BOOL ],
|
||||
Prefs::WIDESCREEN_MODE => [ false, Config::T_BOOL ],
|
||||
Prefs::CDM_ENABLE_GRID => [ false, Config::T_BOOL ],
|
||||
Prefs::DIGEST_MIN_SCORE => [ 0, Config::T_INT ],
|
||||
];
|
||||
|
||||
const _PROFILE_BLACKLIST = [
|
||||
|
@ -138,6 +140,7 @@ class Prefs {
|
|||
//Prefs::SORT_HEADLINES_BY_FEED_DATE,
|
||||
Prefs::SSL_CERT_SERIAL,
|
||||
Prefs::DIGEST_PREFERRED_TIME,
|
||||
Prefs::DIGEST_MIN_SCORE,
|
||||
Prefs::_PREFS_MIGRATED
|
||||
];
|
||||
|
||||
|
@ -247,7 +250,7 @@ class Prefs {
|
|||
/**
|
||||
* @return bool|int|null|string
|
||||
*/
|
||||
static function get(string $pref_name, int $owner_uid, ?int $profile_id) {
|
||||
static function get(string $pref_name, int $owner_uid, ?int $profile_id = null) {
|
||||
return self::get_instance()->_get($pref_name, $owner_uid, $profile_id);
|
||||
}
|
||||
|
||||
|
@ -260,8 +263,6 @@ class Prefs {
|
|||
|
||||
list ($def_val, $type_hint) = self::_DEFAULTS[$pref_name];
|
||||
|
||||
$cached_value = $this->_get_cache($pref_name, $owner_uid, $profile_id);
|
||||
|
||||
if ($this->_is_cached($pref_name, $owner_uid, $profile_id)) {
|
||||
$cached_value = $this->_get_cache($pref_name, $owner_uid, $profile_id);
|
||||
return Config::cast_to($cached_value, $type_hint);
|
||||
|
@ -343,7 +344,7 @@ class Prefs {
|
|||
$value = Config::cast_to($value, $type_hint);
|
||||
|
||||
if ($value == $this->_get($pref_name, $owner_uid, $profile_id))
|
||||
return false;
|
||||
return true; // no need to actually set this to the same value, let's just say we did
|
||||
|
||||
$this->_set_cache($pref_name, $value, $owner_uid, $profile_id);
|
||||
|
|
@ -106,7 +106,7 @@ class RPC extends Handler_Protected {
|
|||
}
|
||||
|
||||
function getAllCounters(): void {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
@$seq = (int) $_REQUEST['seq'];
|
||||
|
||||
|
@ -134,7 +134,7 @@ class RPC extends Handler_Protected {
|
|||
'seq' => $seq
|
||||
];
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
print json_encode($reply);
|
||||
}
|
||||
|
||||
|
@ -176,6 +176,8 @@ class RPC extends Handler_Protected {
|
|||
}
|
||||
|
||||
function sanityCheck(): void {
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$_SESSION["hasSandbox"] = self::_param_to_bool($_REQUEST["hasSandbox"] ?? false);
|
||||
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
|
||||
|
||||
|
@ -207,6 +209,8 @@ class RPC extends Handler_Protected {
|
|||
} else {
|
||||
print Errors::to_json($error, $error_params);
|
||||
}
|
||||
|
||||
$span->end();
|
||||
}
|
||||
|
||||
/*function completeLabels() {
|
||||
|
@ -250,6 +254,7 @@ class RPC extends Handler_Protected {
|
|||
}
|
||||
|
||||
static function updaterandomfeed_real(): void {
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$default_interval = (int) Prefs::get_default(Prefs::DEFAULT_UPDATE_INTERVAL);
|
||||
|
||||
|
@ -340,6 +345,7 @@ class RPC extends Handler_Protected {
|
|||
print json_encode(array("message" => "NOTHING_TO_UPDATE"));
|
||||
}
|
||||
|
||||
$span->end();
|
||||
}
|
||||
|
||||
function updaterandomfeed(): void {
|
||||
|
@ -395,6 +401,8 @@ class RPC extends Handler_Protected {
|
|||
}
|
||||
|
||||
function log(): void {
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$msg = clean($_REQUEST['msg'] ?? "");
|
||||
$file = basename(clean($_REQUEST['file'] ?? ""));
|
||||
$line = (int) clean($_REQUEST['line'] ?? 0);
|
||||
|
@ -406,9 +414,13 @@ class RPC extends Handler_Protected {
|
|||
|
||||
echo json_encode(array("message" => "HOST_ERROR_LOGGED"));
|
||||
}
|
||||
|
||||
$span->end();
|
||||
}
|
||||
|
||||
function checkforupdates(): void {
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$rv = ["changeset" => [], "plugins" => []];
|
||||
|
||||
$version = Config::get_version(false);
|
||||
|
@ -434,6 +446,8 @@ class RPC extends Handler_Protected {
|
|||
$rv["plugins"] = Pref_Prefs::_get_updated_plugins();
|
||||
}
|
||||
|
||||
$span->end();
|
||||
|
||||
print json_encode($rv);
|
||||
}
|
||||
|
||||
|
@ -441,6 +455,8 @@ class RPC extends Handler_Protected {
|
|||
* @return array<string, mixed>
|
||||
*/
|
||||
private function _make_init_params(): array {
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$params = array();
|
||||
|
||||
foreach ([Prefs::ON_CATCHUP_SHOW_NEXT_FEED, Prefs::HIDE_READ_FEEDS,
|
||||
|
@ -493,6 +509,8 @@ class RPC extends Handler_Protected {
|
|||
$params["icon_blank"] = $this->image_to_base64("images/blank_icon.gif");
|
||||
$params["labels"] = Labels::get_all($_SESSION["uid"]);
|
||||
|
||||
$span->end();
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
@ -512,6 +530,8 @@ class RPC extends Handler_Protected {
|
|||
* @return array<string, mixed>
|
||||
*/
|
||||
static function _make_runtime_info(): array {
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$data = array();
|
||||
|
||||
$pdo = Db::pdo();
|
||||
|
@ -577,6 +597,8 @@ class RPC extends Handler_Protected {
|
|||
}
|
||||
}
|
||||
|
||||
$span->end();
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
class RSSUtils {
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $article
|
||||
*/
|
||||
|
@ -68,7 +69,7 @@ class RSSUtils {
|
|||
* @param array<string, false|string> $options
|
||||
*/
|
||||
static function update_daemon_common(int $limit = 0, array $options = []): int {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
if (!$limit) $limit = Config::get(Config::DAEMON_FEED_LIMIT);
|
||||
|
||||
|
@ -285,7 +286,7 @@ class RSSUtils {
|
|||
// Send feed digests by email if needed.
|
||||
Digest::send_headlines_digests();
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return $nf;
|
||||
}
|
||||
|
@ -351,9 +352,12 @@ class RSSUtils {
|
|||
}
|
||||
}
|
||||
|
||||
static function update_rss_feed(int $feed, bool $no_cache = false) : bool {
|
||||
static function update_rss_feed(int $feed, bool $no_cache = false, bool $html_output = false) : bool {
|
||||
|
||||
$scope = Tracer::start(__METHOD__, [], func_get_args());
|
||||
$span = Tracer::start(__METHOD__);
|
||||
$span->setAttribute('func.args', json_encode(func_get_args()));
|
||||
|
||||
Debug::enable_html($html_output);
|
||||
Debug::log("start", Debug::LOG_VERBOSE);
|
||||
|
||||
$pdo = Db::pdo();
|
||||
|
@ -388,19 +392,19 @@ class RSSUtils {
|
|||
if ($user) {
|
||||
if ($user->access_level == UserHelper::ACCESS_LEVEL_READONLY) {
|
||||
Debug::log("error: denied update for $feed: permission denied by owner access level");
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// this would indicate database corruption of some kind
|
||||
Debug::log("error: owner not found for feed: $feed");
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
Debug::log("error: feeds table record not found for feed: $feed");
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -424,6 +428,7 @@ class RSSUtils {
|
|||
$rss_hash = false;
|
||||
|
||||
$force_refetch = isset($_REQUEST["force_refetch"]);
|
||||
$dump_feed_xml = isset($_REQUEST["dump_feed_xml"]);
|
||||
$feed_data = "";
|
||||
|
||||
Debug::log("running HOOK_FETCH_FEED handlers...", Debug::LOG_VERBOSE);
|
||||
|
@ -558,7 +563,7 @@ class RSSUtils {
|
|||
$feed_obj->save();
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return $error_message == "";
|
||||
}
|
||||
|
||||
|
@ -569,6 +574,14 @@ class RSSUtils {
|
|||
$pff_owner_uid = $feed_obj->owner_uid;
|
||||
$pff_feed_url = $feed_obj->feed_url;
|
||||
|
||||
if ($dump_feed_xml) {
|
||||
Debug::log("feed data before hooks:", Debug::LOG_VERBOSE);
|
||||
|
||||
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
|
||||
print("<code class='feed-xml'>" . htmlspecialchars($feed_data). "</code>\n");
|
||||
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
|
||||
}
|
||||
|
||||
$start_ts = microtime(true);
|
||||
$pluginhost->chain_hooks_callback(PluginHost::HOOK_FEED_FETCHED,
|
||||
function ($result, $plugin) use (&$feed_data, $start_ts) {
|
||||
|
@ -583,6 +596,14 @@ class RSSUtils {
|
|||
Debug::log("feed data has not been modified by a plugin.", Debug::LOG_VERBOSE);
|
||||
}
|
||||
|
||||
if ($dump_feed_xml) {
|
||||
Debug::log("feed data after hooks:", Debug::LOG_VERBOSE);
|
||||
|
||||
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
|
||||
print("<code class='feed-xml'>" . htmlspecialchars($feed_data). "</code>\n");
|
||||
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
|
||||
}
|
||||
|
||||
$rss = new FeedParser($feed_data);
|
||||
$rss->init();
|
||||
|
||||
|
@ -684,7 +705,7 @@ class RSSUtils {
|
|||
]);
|
||||
|
||||
$feed_obj->save();
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return true; // no articles
|
||||
}
|
||||
|
||||
|
@ -693,12 +714,11 @@ class RSSUtils {
|
|||
$tstart = time();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$a_scope = Tracer::start('article');
|
||||
$a_span = Tracer::start('article');
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
Debug::log("=================================================================================================================================",
|
||||
Debug::LOG_VERBOSE);
|
||||
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
|
||||
|
||||
if (Debug::get_loglevel() >= 3) {
|
||||
print_r($item);
|
||||
|
@ -853,7 +873,7 @@ class RSSUtils {
|
|||
|
||||
$pdo->commit();
|
||||
|
||||
$entry_obj = ORM::for_table('ttrss_entries')
|
||||
ORM::for_table('ttrss_entries')
|
||||
->find_one($base_entry_id)
|
||||
->set('date_updated', Db::NOW())
|
||||
->save();
|
||||
|
@ -1008,7 +1028,7 @@ class RSSUtils {
|
|||
WHERE guid IN (?, ?, ?)");
|
||||
$csth->execute([$entry_guid, $entry_guid_hashed, $entry_guid_hashed_compat]);
|
||||
|
||||
if (!$row = $csth->fetch()) {
|
||||
if (!$csth->fetch()) {
|
||||
|
||||
Debug::log("base guid [$entry_guid or $entry_guid_hashed] not found, creating...", Debug::LOG_VERBOSE);
|
||||
|
||||
|
@ -1164,7 +1184,7 @@ class RSSUtils {
|
|||
|
||||
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
||||
$params[":ts_lang"] = $feed_language;
|
||||
$params[":ts_content"] = mb_substr(strip_tags($entry_title . " " . $entry_content), 0, 900000);
|
||||
$params[":ts_content"] = mb_substr(strip_tags($entry_title) . " " . \Soundasleep\Html2Text::convert($entry_content), 0, 900000);
|
||||
}
|
||||
|
||||
$sth->execute($params);
|
||||
|
@ -1287,11 +1307,10 @@ class RSSUtils {
|
|||
Debug::log("article processed.", Debug::LOG_VERBOSE);
|
||||
|
||||
$pdo->commit();
|
||||
$a_scope->close();
|
||||
$a_span->end();
|
||||
}
|
||||
|
||||
Debug::log("=================================================================================================================================",
|
||||
Debug::LOG_VERBOSE);
|
||||
Debug::log(Debug::SEPARATOR, Debug::LOG_VERBOSE);
|
||||
|
||||
Debug::log("purging feed...", Debug::LOG_VERBOSE);
|
||||
|
||||
|
@ -1329,12 +1348,12 @@ class RSSUtils {
|
|||
unset($rss);
|
||||
|
||||
Debug::log("update failed.", Debug::LOG_VERBOSE);
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug::log("update done.", Debug::LOG_VERBOSE);
|
||||
$scope->close();
|
||||
$span->end();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1499,7 +1518,7 @@ class RSSUtils {
|
|||
* @return array<int, array<string, string>> An array of filter action arrays with keys "type" and "param"
|
||||
*/
|
||||
static function get_article_filters(array $filters, string $title, string $content, string $link, string $author, array $tags, array &$matched_rules = null, array &$matched_filters = null): array {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = Tracer::start(__METHOD__);
|
||||
|
||||
$matches = array();
|
||||
|
||||
|
@ -1508,6 +1527,7 @@ class RSSUtils {
|
|||
$inverse = $filter["inverse"] ?? false;
|
||||
$filter_match = false;
|
||||
$last_processed_rule = false;
|
||||
$regexp_matches = [];
|
||||
|
||||
foreach ($filter["rules"] as $rule) {
|
||||
$match = false;
|
||||
|
@ -1521,32 +1541,32 @@ class RSSUtils {
|
|||
|
||||
switch ($rule["type"]) {
|
||||
case "title":
|
||||
$match = @preg_match("/$reg_exp/iu", $title);
|
||||
$match = @preg_match("/$reg_exp/iu", $title, $regexp_matches);
|
||||
break;
|
||||
case "content":
|
||||
// we don't need to deal with multiline regexps
|
||||
$content = (string)preg_replace("/[\r\n\t]/", "", $content);
|
||||
|
||||
$match = @preg_match("/$reg_exp/iu", $content);
|
||||
$match = @preg_match("/$reg_exp/iu", $content, $regexp_matches);
|
||||
break;
|
||||
case "both":
|
||||
// we don't need to deal with multiline regexps
|
||||
$content = (string)preg_replace("/[\r\n\t]/", "", $content);
|
||||
|
||||
$match = (@preg_match("/$reg_exp/iu", $title) || @preg_match("/$reg_exp/iu", $content));
|
||||
$match = (@preg_match("/$reg_exp/iu", $title, $regexp_matches) || @preg_match("/$reg_exp/iu", $content, $regexp_matches));
|
||||
break;
|
||||
case "link":
|
||||
$match = @preg_match("/$reg_exp/iu", $link);
|
||||
$match = @preg_match("/$reg_exp/iu", $link, $regexp_matches);
|
||||
break;
|
||||
case "author":
|
||||
$match = @preg_match("/$reg_exp/iu", $author);
|
||||
$match = @preg_match("/$reg_exp/iu", $author, $regexp_matches);
|
||||
break;
|
||||
case "tag":
|
||||
if (count($tags) == 0)
|
||||
array_push($tags, ''); // allow matching if there are no tags
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (@preg_match("/$reg_exp/iu", $tag)) {
|
||||
if (@preg_match("/$reg_exp/iu", $tag, $regexp_matches)) {
|
||||
$match = true;
|
||||
break;
|
||||
}
|
||||
|
@ -1572,6 +1592,8 @@ class RSSUtils {
|
|||
if ($inverse) $filter_match = !$filter_match;
|
||||
|
||||
if ($filter_match) {
|
||||
$last_processed_rule["regexp_matches"] = $regexp_matches;
|
||||
|
||||
if (is_array($matched_rules)) array_push($matched_rules, $last_processed_rule);
|
||||
if (is_array($matched_filters)) array_push($matched_filters, $filter);
|
||||
|
||||
|
@ -1584,7 +1606,7 @@ class RSSUtils {
|
|||
}
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
$span->end();
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
@ -1724,7 +1746,6 @@ class RSSUtils {
|
|||
/** migrates favicons from legacy storage in feed-icons/ to cache/feed-icons/using new naming (sans .ico suffix) */
|
||||
static function migrate_feed_icons() : void {
|
||||
$old_dir = Config::get(Config::ICONS_DIR);
|
||||
$new_dir = Config::get(Config::CACHE_DIR) . '/feed-icons';
|
||||
|
||||
$dh = opendir($old_dir);
|
||||
|
||||
|
@ -1985,7 +2006,7 @@ class RSSUtils {
|
|||
|
||||
$favicon_urls = [];
|
||||
|
||||
if ($html = @UrlHelper::fetch($url)) {
|
||||
if ($html = @UrlHelper::fetch(['url' => $url])) {
|
||||
|
||||
$doc = new DOMDocument();
|
||||
if (@$doc->loadHTML($html)) {
|
|
@ -63,7 +63,8 @@ class Sanitizer {
|
|||
* @return false|string The HTML, or false if an error occurred.
|
||||
*/
|
||||
public static function sanitize(string $str, ?bool $force_remove_images = false, int $owner = null, string $site_url = null, array $highlight_words = null, int $article_id = null) {
|
||||
$scope = Tracer::start(__METHOD__);
|
||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
||||
$span->addEvent("Sanitizer::sanitize");
|
||||
|
||||
if (!$owner && isset($_SESSION["uid"]))
|
||||
$owner = $_SESSION["uid"];
|
||||
|
@ -224,8 +225,6 @@ class Sanitizer {
|
|||
|
||||
$res = $doc->saveHTML();
|
||||
|
||||
$scope->close();
|
||||
|
||||
/* strip everything outside of <body>...</body> */
|
||||
|
||||
$res_frag = array();
|
|
@ -0,0 +1,216 @@
|
|||
<?php
|
||||
|
||||
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
|
||||
use OpenTelemetry\API\Trace\SpanContextInterface;
|
||||
use OpenTelemetry\API\Trace\SpanInterface;
|
||||
use OpenTelemetry\API\Trace\SpanKind;
|
||||
use OpenTelemetry\API\Trace\TraceFlags;
|
||||
use OpenTelemetry\API\Trace\TraceStateInterface;
|
||||
use OpenTelemetry\Context\ContextInterface;
|
||||
use OpenTelemetry\Context\ContextKey;
|
||||
use OpenTelemetry\Context\ContextKeyInterface;
|
||||
use OpenTelemetry\Context\ImplicitContextKeyedInterface;
|
||||
use OpenTelemetry\Context\ScopeInterface;
|
||||
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
|
||||
use OpenTelemetry\Contrib\Otlp\SpanExporter;
|
||||
use OpenTelemetry\SDK\Common\Attribute\Attributes;
|
||||
use OpenTelemetry\SDK\Resource\ResourceInfo;
|
||||
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
|
||||
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
|
||||
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
|
||||
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
|
||||
use OpenTelemetry\SDK\Trace\TracerProvider;
|
||||
use OpenTelemetry\SemConv\ResourceAttributes;
|
||||
|
||||
class DummyContextInterface implements ContextInterface {
|
||||
|
||||
/** @var DummyContextInterface */
|
||||
private static $instance;
|
||||
|
||||
public function __construct() {
|
||||
self::$instance = $this;
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public static function createKey(string $key): ContextKeyInterface { return new ContextKey(); }
|
||||
|
||||
public static function getCurrent(): ContextInterface { return self::$instance; }
|
||||
|
||||
public function activate(): ScopeInterface { return new DummyScopeInterface(); }
|
||||
|
||||
public function with(ContextKeyInterface $key, $value): ContextInterface { return $this; }
|
||||
|
||||
public function withContextValue(ImplicitContextKeyedInterface $value): ContextInterface { return $this; }
|
||||
|
||||
public function get(ContextKeyInterface $key) { return new ContextKey(); }
|
||||
|
||||
}
|
||||
|
||||
class DummySpanContextInterface implements SpanContextInterface {
|
||||
|
||||
/** @var DummySpanContextInterface $instance */
|
||||
private static $instance;
|
||||
|
||||
public function __construct() {
|
||||
self::$instance = $this;
|
||||
}
|
||||
|
||||
public static function createFromRemoteParent(string $traceId, string $spanId, int $traceFlags = TraceFlags::DEFAULT, ?TraceStateInterface $traceState = null): SpanContextInterface { return self::$instance; }
|
||||
|
||||
public static function getInvalid(): SpanContextInterface { return self::$instance; }
|
||||
|
||||
public static function create(string $traceId, string $spanId, int $traceFlags = TraceFlags::DEFAULT, ?TraceStateInterface $traceState = null): SpanContextInterface { return self::$instance; }
|
||||
|
||||
public function getTraceId(): string { return ""; }
|
||||
|
||||
public function getTraceIdBinary(): string { return ""; }
|
||||
|
||||
public function getSpanId(): string { return ""; }
|
||||
|
||||
public function getSpanIdBinary(): string { return ""; }
|
||||
|
||||
public function getTraceFlags(): int { return 0; }
|
||||
|
||||
public function getTraceState(): ?TraceStateInterface { return null; }
|
||||
|
||||
public function isValid(): bool { return false; }
|
||||
|
||||
public function isRemote(): bool { return false; }
|
||||
|
||||
public function isSampled(): bool { return false; }
|
||||
}
|
||||
|
||||
class DummyScopeInterface implements ScopeInterface {
|
||||
public function detach(): int { return 0; }
|
||||
}
|
||||
|
||||
class DummySpanInterface implements SpanInterface {
|
||||
|
||||
/** @var DummySpanInterface $instance */
|
||||
private static $instance;
|
||||
|
||||
public function __construct() {
|
||||
self::$instance = $this;
|
||||
}
|
||||
|
||||
public static function fromContext(ContextInterface $context): SpanInterface { return self::$instance; }
|
||||
|
||||
public static function getCurrent(): SpanInterface { return self::$instance; }
|
||||
|
||||
public static function getInvalid(): SpanInterface { return self::$instance; }
|
||||
|
||||
public static function wrap(SpanContextInterface $spanContext): SpanInterface { return self::$instance; }
|
||||
|
||||
public function getContext(): SpanContextInterface { return new DummySpanContextInterface(); }
|
||||
|
||||
public function isRecording(): bool { return false; }
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function setAttribute(string $key, $value): SpanInterface { return self::$instance; }
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function setAttributes(iterable $attributes): SpanInterface { return self::$instance; }
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function addEvent(string $name, iterable $attributes = [], ?int $timestamp = null): SpanInterface { return $this; }
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function recordException(Throwable $exception, iterable $attributes = []): SpanInterface { return $this; }
|
||||
|
||||
public function updateName(string $name): SpanInterface { return $this; }
|
||||
|
||||
public function setStatus(string $code, ?string $description = null): SpanInterface { return $this; }
|
||||
|
||||
public function end(?int $endEpochNanos = null): void { }
|
||||
|
||||
public function activate(): ScopeInterface { return new DummyScopeInterface(); }
|
||||
|
||||
public function storeInContext(ContextInterface $context): ContextInterface { return new DummyContextInterface(); }
|
||||
|
||||
}
|
||||
|
||||
class Tracer {
|
||||
/** @var Tracer $instance */
|
||||
private static $instance = null;
|
||||
|
||||
/** @var OpenTelemetry\SDK\Trace\TracerProviderInterface $tracerProvider */
|
||||
private $tracerProvider = null;
|
||||
|
||||
/** @var OpenTelemetry\API\Trace\TracerInterface $tracer */
|
||||
private $tracer = null;
|
||||
|
||||
public function __construct() {
|
||||
$OPENTELEMETRY_ENDPOINT = Config::get(Config::OPENTELEMETRY_ENDPOINT);
|
||||
|
||||
if ($OPENTELEMETRY_ENDPOINT) {
|
||||
$transport = (new OtlpHttpTransportFactory())->create($OPENTELEMETRY_ENDPOINT, 'application/x-protobuf');
|
||||
$exporter = new SpanExporter($transport);
|
||||
|
||||
$resource = ResourceInfoFactory::emptyResource()->merge(
|
||||
ResourceInfo::create(Attributes::create(
|
||||
[ResourceAttributes::SERVICE_NAME => Config::get(Config::OPENTELEMETRY_SERVICE)]
|
||||
), ResourceAttributes::SCHEMA_URL),
|
||||
);
|
||||
|
||||
$this->tracerProvider = TracerProvider::builder()
|
||||
->addSpanProcessor(new SimpleSpanProcessor($exporter))
|
||||
->setResource($resource)
|
||||
->setSampler(new ParentBased(new AlwaysOnSampler()))
|
||||
->build();
|
||||
|
||||
$this->tracer = $this->tracerProvider->getTracer('io.opentelemetry.contrib.php');
|
||||
|
||||
$context = TraceContextPropagator::getInstance()->extract(getallheaders());
|
||||
|
||||
$span = $this->tracer->spanBuilder($_SESSION['name'] ?? 'not logged in')
|
||||
->setParent($context)
|
||||
->setSpanKind(SpanKind::KIND_SERVER)
|
||||
->setAttribute('php.request', json_encode($_REQUEST))
|
||||
->setAttribute('php.server', json_encode($_SERVER))
|
||||
->setAttribute('php.session', json_encode($_SESSION ?? []))
|
||||
->startSpan();
|
||||
|
||||
$scope = $span->activate();
|
||||
|
||||
register_shutdown_function(function() use ($span, $scope) {
|
||||
$span->end();
|
||||
$scope->detach();
|
||||
$this->tracerProvider->shutdown();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return OpenTelemetry\API\Trace\SpanInterface
|
||||
*/
|
||||
private function _start(string $name) {
|
||||
if ($this->tracer != null) {
|
||||
$span = $this->tracer
|
||||
->spanBuilder($name)
|
||||
->setSpanKind(SpanKind::KIND_SERVER)
|
||||
->startSpan();
|
||||
|
||||
$span->activate();
|
||||
} else {
|
||||
$span = new DummySpanInterface();
|
||||
}
|
||||
|
||||
return $span;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return OpenTelemetry\API\Trace\SpanInterface
|
||||
*/
|
||||
public static function start(string $name) {
|
||||
return self::get_instance()->_start($name);
|
||||
}
|
||||
|
||||
public static function get_instance() : Tracer {
|
||||
if (self::$instance == null)
|
||||
self::$instance = new self();
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,505 @@
|
|||
<?php
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
class UrlHelper {
|
||||
const EXTRA_HREF_SCHEMES = [
|
||||
"magnet",
|
||||
"mailto",
|
||||
"tel"
|
||||
];
|
||||
|
||||
const EXTRA_SCHEMES_BY_CONTENT_TYPE = [
|
||||
"application/x-bittorrent" => [ "magnet" ],
|
||||
];
|
||||
|
||||
static string $fetch_last_error;
|
||||
static int $fetch_last_error_code;
|
||||
static string $fetch_last_error_content;
|
||||
static string $fetch_last_content_type;
|
||||
static string $fetch_last_modified;
|
||||
static string $fetch_effective_url;
|
||||
static string $fetch_effective_ip_addr;
|
||||
|
||||
public static ?GuzzleHttp\ClientInterface $client = null;
|
||||
|
||||
private static function get_client(): GuzzleHttp\ClientInterface {
|
||||
if (self::$client == null) {
|
||||
self::$client = new GuzzleHttp\Client([
|
||||
GuzzleHttp\RequestOptions::COOKIES => false,
|
||||
GuzzleHttp\RequestOptions::PROXY => Config::get(Config::HTTP_PROXY) ?: null,
|
||||
]);
|
||||
}
|
||||
|
||||
return self::$client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|int> $parts
|
||||
*/
|
||||
static function build_url(array $parts): string {
|
||||
$tmp = $parts['scheme'] . "://" . $parts['host'];
|
||||
|
||||
if (isset($parts['path'])) $tmp .= $parts['path'];
|
||||
if (isset($parts['query'])) $tmp .= '?' . $parts['query'];
|
||||
if (isset($parts['fragment'])) $tmp .= '#' . $parts['fragment'];
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a (possibly) relative URL to a absolute one, using provided base URL.
|
||||
* Provides some exceptions for additional schemes like data: if called with owning element/attribute.
|
||||
*
|
||||
* @param string $base_url Base URL (i.e. from where the document is)
|
||||
* @param string $rel_url Possibly relative URL in the document
|
||||
* @param string $owner_element Owner element tag name (i.e. "a") (optional)
|
||||
* @param string $owner_attribute Owner attribute (i.e. "href") (optional)
|
||||
* @param string $content_type URL content type as specified by enclosures, etc.
|
||||
*
|
||||
* @return false|string Absolute URL or false on failure (either during URL parsing or validation)
|
||||
*/
|
||||
public static function rewrite_relative($base_url,
|
||||
$rel_url,
|
||||
string $owner_element = "",
|
||||
string $owner_attribute = "",
|
||||
string $content_type = "") {
|
||||
|
||||
$rel_parts = parse_url($rel_url);
|
||||
|
||||
if (!$rel_url) return $base_url;
|
||||
|
||||
/**
|
||||
* If parse_url failed to parse $rel_url return false to match the current "invalid thing" behavior
|
||||
* of UrlHelper::validate().
|
||||
*
|
||||
* TODO: There are many places where a string return value is assumed. We should either update those
|
||||
* to account for the possibility of failure, or look into updating this function's return values.
|
||||
*/
|
||||
if ($rel_parts === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($rel_parts['host']) && !empty($rel_parts['scheme'])) {
|
||||
return self::validate($rel_url);
|
||||
|
||||
// protocol-relative URL (rare but they exist)
|
||||
} else if (strpos($rel_url, "//") === 0) {
|
||||
return self::validate("https:" . $rel_url);
|
||||
// allow some extra schemes for A href
|
||||
} else if (in_array($rel_parts["scheme"] ?? "", self::EXTRA_HREF_SCHEMES, true) &&
|
||||
$owner_element == "a" &&
|
||||
$owner_attribute == "href") {
|
||||
return $rel_url;
|
||||
// allow some extra schemes for links with feed-specified content type i.e. enclosures
|
||||
} else if ($content_type &&
|
||||
isset(self::EXTRA_SCHEMES_BY_CONTENT_TYPE[$content_type]) &&
|
||||
in_array($rel_parts["scheme"], self::EXTRA_SCHEMES_BY_CONTENT_TYPE[$content_type])) {
|
||||
return $rel_url;
|
||||
// allow limited subset of inline base64-encoded images for IMG elements
|
||||
} else if (($rel_parts["scheme"] ?? "") == "data" &&
|
||||
preg_match('%^image/(webp|gif|jpg|png|svg);base64,%', $rel_parts["path"]) &&
|
||||
$owner_element == "img" &&
|
||||
$owner_attribute == "src") {
|
||||
return $rel_url;
|
||||
} else {
|
||||
$base_parts = parse_url($base_url);
|
||||
|
||||
$rel_parts['host'] = $base_parts['host'] ?? "";
|
||||
$rel_parts['scheme'] = $base_parts['scheme'] ?? "";
|
||||
|
||||
if ($rel_parts['path'] ?? "") {
|
||||
|
||||
// we append dirname() of base path to relative URL path as per RFC 3986 section 5.2.2
|
||||
$base_path = with_trailing_slash(dirname($base_parts['path'] ?? ""));
|
||||
|
||||
// 1. absolute relative path (/test.html) = no-op, proceed as is
|
||||
|
||||
// 2. dotslash relative URI (./test.html) - strip "./", append base path
|
||||
if (strpos($rel_parts['path'], './') === 0) {
|
||||
$rel_parts['path'] = $base_path . substr($rel_parts['path'], 2);
|
||||
// 3. anything else relative (test.html) - append dirname() of base path
|
||||
} else if (strpos($rel_parts['path'], '/') !== 0) {
|
||||
$rel_parts['path'] = $base_path . $rel_parts['path'];
|
||||
}
|
||||
|
||||
//$rel_parts['path'] = str_replace("/./", "/", $rel_parts['path']);
|
||||
//$rel_parts['path'] = str_replace("//", "/", $rel_parts['path']);
|
||||
}
|
||||
|
||||
return self::validate(self::build_url($rel_parts));
|
||||
}
|
||||
}
|
||||
|
||||
/** extended filtering involves validation for safe ports and loopback
|
||||
* @return false|string false if something went wrong, otherwise the URL string
|
||||
*/
|
||||
static function validate(string $url, bool $extended_filtering = false) {
|
||||
|
||||
$url = clean($url);
|
||||
|
||||
# fix protocol-relative URLs
|
||||
if (strpos($url, "//") === 0)
|
||||
$url = "https:" . $url;
|
||||
|
||||
$tokens = parse_url($url);
|
||||
|
||||
// this isn't really necessary because filter_var(... FILTER_VALIDATE_URL) requires host and scheme
|
||||
// as per https://php.watch/versions/7.3/filter-var-flag-deprecation but it might save time
|
||||
if (empty($tokens['host']))
|
||||
return false;
|
||||
|
||||
if (!in_array(strtolower($tokens['scheme']), ['http', 'https']))
|
||||
return false;
|
||||
|
||||
//convert IDNA hostname to punycode if possible
|
||||
if (function_exists("idn_to_ascii")) {
|
||||
if (mb_detect_encoding($tokens['host']) != 'ASCII') {
|
||||
if (defined('IDNA_NONTRANSITIONAL_TO_ASCII') && defined('INTL_IDNA_VARIANT_UTS46')) {
|
||||
$tokens['host'] = idn_to_ascii($tokens['host'], IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
|
||||
} else {
|
||||
$tokens['host'] = idn_to_ascii($tokens['host']);
|
||||
}
|
||||
|
||||
// if `idn_to_ascii` failed
|
||||
if ($tokens['host'] === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// separate set of tokens with urlencoded 'path' because filter_var() rightfully fails on non-latin characters
|
||||
// (used for validation only, we actually request the original URL, in case of urlencode breaking it)
|
||||
$tokens_filter_var = $tokens;
|
||||
|
||||
if ($tokens['path'] ?? false) {
|
||||
$tokens_filter_var['path'] = implode("/",
|
||||
array_map("rawurlencode",
|
||||
array_map("rawurldecode",
|
||||
explode("/", $tokens['path']))));
|
||||
}
|
||||
|
||||
$url = self::build_url($tokens);
|
||||
$url_filter_var = self::build_url($tokens_filter_var);
|
||||
|
||||
if (filter_var($url_filter_var, FILTER_VALIDATE_URL) === false)
|
||||
return false;
|
||||
|
||||
if ($extended_filtering) {
|
||||
if (!in_array($tokens['port'] ?? '', [80, 443, '']))
|
||||
return false;
|
||||
|
||||
if (strtolower($tokens['host']) == 'localhost' || $tokens['host'] == '::1' || strpos($tokens['host'], '127.') === 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|string
|
||||
*/
|
||||
static function resolve_redirects(string $url, int $timeout) {
|
||||
$span = Tracer::start(__METHOD__);
|
||||
$span->setAttribute('func.args', json_encode(func_get_args()));
|
||||
$client = self::get_client();
|
||||
|
||||
try {
|
||||
$response = $client->request('HEAD', $url, [
|
||||
GuzzleHttp\RequestOptions::CONNECT_TIMEOUT => $timeout ?: Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT),
|
||||
GuzzleHttp\RequestOptions::TIMEOUT => $timeout ?: Config::get(Config::FILE_FETCH_TIMEOUT),
|
||||
GuzzleHttp\RequestOptions::ALLOW_REDIRECTS => ['max' => 10, 'track_redirects' => true],
|
||||
GuzzleHttp\RequestOptions::HTTP_ERRORS => false,
|
||||
GuzzleHttp\RequestOptions::HEADERS => [
|
||||
'User-Agent' => Config::get_user_agent(),
|
||||
'Connection' => 'close',
|
||||
],
|
||||
]);
|
||||
} catch (Exception $ex) {
|
||||
$span->setAttribute('error', (string) $ex);
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a history header value doesn't exist there was no redirection and the original URL is fine.
|
||||
$history_header = $response->getHeader(GuzzleHttp\RedirectMiddleware::HISTORY_HEADER);
|
||||
$span->end();
|
||||
return ($history_header ? end($history_header) : $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool|int|string>|string $options
|
||||
* @return false|string false if something went wrong, otherwise string contents
|
||||
*/
|
||||
// TODO: max_size currently only works for CURL transfers
|
||||
// TODO: multiple-argument way is deprecated, first parameter is a hash now
|
||||
public static function fetch($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
|
||||
4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false, 8: $encoding = false,
|
||||
9: $auth_type = "basic" */) {
|
||||
$span = Tracer::start(__METHOD__);
|
||||
$span->setAttribute('func.args', json_encode(func_get_args()));
|
||||
|
||||
self::$fetch_last_error = "";
|
||||
self::$fetch_last_error_code = -1;
|
||||
self::$fetch_last_error_content = "";
|
||||
self::$fetch_last_content_type = "";
|
||||
self::$fetch_last_modified = "";
|
||||
self::$fetch_effective_url = "";
|
||||
self::$fetch_effective_ip_addr = "";
|
||||
|
||||
if (!is_array($options)) {
|
||||
|
||||
// falling back on compatibility shim
|
||||
$option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent", "encoding", "auth_type" ];
|
||||
$tmp = [];
|
||||
|
||||
for ($i = 0; $i < func_num_args(); $i++) {
|
||||
$tmp[$option_names[$i]] = func_get_arg($i);
|
||||
}
|
||||
|
||||
$options = $tmp;
|
||||
|
||||
/*$options = array(
|
||||
"url" => func_get_arg(0),
|
||||
"type" => @func_get_arg(1),
|
||||
"login" => @func_get_arg(2),
|
||||
"pass" => @func_get_arg(3),
|
||||
"post_query" => @func_get_arg(4),
|
||||
"timeout" => @func_get_arg(5),
|
||||
"timestamp" => @func_get_arg(6),
|
||||
"useragent" => @func_get_arg(7),
|
||||
"encoding" => @func_get_arg(8),
|
||||
"auth_type" => @func_get_arg(9),
|
||||
); */
|
||||
}
|
||||
|
||||
$url = $options["url"];
|
||||
$type = isset($options["type"]) ? $options["type"] : false;
|
||||
$login = isset($options["login"]) ? $options["login"] : false;
|
||||
$pass = isset($options["pass"]) ? $options["pass"] : false;
|
||||
$auth_type = isset($options["auth_type"]) ? $options["auth_type"] : "basic";
|
||||
$post_query = isset($options["post_query"]) ? $options["post_query"] : false;
|
||||
$timeout = isset($options["timeout"]) ? $options["timeout"] : false;
|
||||
$last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
|
||||
$useragent = isset($options["useragent"]) ? $options["useragent"] : false;
|
||||
$followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
|
||||
$max_size = isset($options["max_size"]) ? $options["max_size"] : Config::get(Config::MAX_DOWNLOAD_FILE_SIZE); // in bytes
|
||||
$http_accept = isset($options["http_accept"]) ? $options["http_accept"] : false;
|
||||
$http_referrer = isset($options["http_referrer"]) ? $options["http_referrer"] : false;
|
||||
$encoding = isset($options["encoding"]) ? $options["encoding"] : false;
|
||||
|
||||
$url = ltrim($url, ' ');
|
||||
$url = str_replace(' ', '%20', $url);
|
||||
|
||||
Debug::log("[UrlHelper] fetching: $url", Debug::LOG_EXTENDED);
|
||||
|
||||
$url = self::validate($url, true);
|
||||
|
||||
if (!$url) {
|
||||
self::$fetch_last_error = 'Requested URL failed extended validation.';
|
||||
$span->setAttribute('error', self::$fetch_last_error);
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
$url_host = parse_url($url, PHP_URL_HOST);
|
||||
$ip_addr = gethostbyname($url_host);
|
||||
|
||||
if (!$ip_addr || strpos($ip_addr, '127.') === 0) {
|
||||
self::$fetch_last_error = "URL hostname failed to resolve or resolved to a loopback address ($ip_addr)";
|
||||
$span->setAttribute('error', self::$fetch_last_error);
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
$req_options = [
|
||||
GuzzleHttp\RequestOptions::CONNECT_TIMEOUT => $timeout ?: Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT),
|
||||
GuzzleHttp\RequestOptions::TIMEOUT => $timeout ?: Config::get(Config::FILE_FETCH_TIMEOUT),
|
||||
GuzzleHttp\RequestOptions::HEADERS => [
|
||||
'User-Agent' => $useragent ?: Config::get_user_agent(),
|
||||
],
|
||||
'curl' => [],
|
||||
];
|
||||
|
||||
if ($followlocation) {
|
||||
$req_options[GuzzleHttp\RequestOptions::ALLOW_REDIRECTS] = [
|
||||
'max' => 20,
|
||||
'track_redirects' => true,
|
||||
'on_redirect' => function(RequestInterface $request, ResponseInterface $response, UriInterface $uri) {
|
||||
if (!self::validate($uri, true)) {
|
||||
self::$fetch_effective_url = (string) $uri;
|
||||
throw new GuzzleHttp\Exception\RequestException('URL received during redirection failed extended validation.',
|
||||
$request, $response);
|
||||
}
|
||||
},
|
||||
];
|
||||
} else {
|
||||
$req_options[GuzzleHttp\RequestOptions::ALLOW_REDIRECTS] = false;
|
||||
}
|
||||
|
||||
if ($last_modified && !$post_query)
|
||||
$req_options[GuzzleHttp\RequestOptions::HEADERS]['If-Modified-Since'] = $last_modified;
|
||||
|
||||
if ($http_accept)
|
||||
$req_options[GuzzleHttp\RequestOptions::HEADERS]['Accept'] = $http_accept;
|
||||
|
||||
if ($encoding)
|
||||
$req_options[GuzzleHttp\RequestOptions::HEADERS]['Accept-Encoding'] = $encoding;
|
||||
|
||||
if ($http_referrer)
|
||||
$req_options[GuzzleHttp\RequestOptions::HEADERS]['Referer'] = $http_referrer;
|
||||
|
||||
if ($login && $pass && in_array($auth_type, ['basic', 'digest', 'ntlm'])) {
|
||||
// Let Guzzle handle the details for auth types it supports
|
||||
$req_options[GuzzleHttp\RequestOptions::AUTH] = [$login, $pass, $auth_type];
|
||||
} elseif ($auth_type === 'any') {
|
||||
// https://docs.guzzlephp.org/en/stable/faq.html#how-can-i-add-custom-curl-options
|
||||
$req_options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_ANY;
|
||||
if ($login && $pass)
|
||||
$req_options['curl'][\CURLOPT_USERPWD] = "$login:$pass";
|
||||
}
|
||||
|
||||
if ($post_query)
|
||||
$req_options[GuzzleHttp\RequestOptions::FORM_PARAMS] = $post_query;
|
||||
|
||||
if ($max_size) {
|
||||
$req_options[GuzzleHttp\RequestOptions::PROGRESS] = function($download_size, $downloaded, $upload_size, $uploaded) use(&$max_size, $url) {
|
||||
//Debug::log("[curl progressfunction] $downloaded $max_size", Debug::$LOG_EXTENDED);
|
||||
|
||||
if ($downloaded > $max_size) {
|
||||
Debug::log("[UrlHelper] fetch error: max size of $max_size bytes exceeded when downloading $url . Aborting.", Debug::LOG_VERBOSE);
|
||||
throw new \LengthException("Download exceeded size limit");
|
||||
}
|
||||
};
|
||||
|
||||
# Alternative/supplement to `progress` checking
|
||||
$req_options[GuzzleHttp\RequestOptions::ON_HEADERS] = function(ResponseInterface $response) use(&$max_size, $url) {
|
||||
$content_length = $response->getHeaderLine('Content-Length');
|
||||
if ($content_length > $max_size) {
|
||||
Debug::log("[UrlHelper] fetch error: server indicated (via 'Content-Length: {$content_length}') max size of $max_size bytes " .
|
||||
"would be exceeded when downloading $url . Aborting.", Debug::LOG_VERBOSE);
|
||||
throw new \LengthException("Server sent 'Content-Length' exceeding download limit");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$client = self::get_client();
|
||||
|
||||
try {
|
||||
$response = $client->request($post_query ? 'POST' : 'GET', $url, $req_options);
|
||||
} catch (\LengthException $ex) {
|
||||
// Either 'Content-Length' indicated the download limit would be exceeded, or the transfer actually exceeded the download limit.
|
||||
self::$fetch_last_error = $ex->getMessage();
|
||||
$span->setAttribute('error', self::$fetch_last_error);
|
||||
$span->end();
|
||||
return false;
|
||||
} catch (GuzzleHttp\Exception\GuzzleException $ex) {
|
||||
self::$fetch_last_error = $ex->getMessage();
|
||||
|
||||
if ($ex instanceof GuzzleHttp\Exception\RequestException) {
|
||||
if ($ex instanceof GuzzleHttp\Exception\BadResponseException) {
|
||||
// 4xx or 5xx
|
||||
self::$fetch_last_error_code = $ex->getResponse()->getStatusCode();
|
||||
|
||||
// If credentials were provided and we got a 403 back, retry once with auth type 'any'
|
||||
// to attempt compatibility with unusual configurations.
|
||||
if ($login && $pass && self::$fetch_last_error_code === 403 && $auth_type !== 'any') {
|
||||
$options['auth_type'] = 'any';
|
||||
$span->end();
|
||||
return self::fetch($options);
|
||||
}
|
||||
|
||||
self::$fetch_last_content_type = $ex->getResponse()->getHeaderLine('content-type');
|
||||
|
||||
if ($type && strpos(self::$fetch_last_content_type, "$type") === false)
|
||||
self::$fetch_last_error_content = (string) $ex->getResponse()->getBody();
|
||||
} elseif (array_key_exists('errno', $ex->getHandlerContext())) {
|
||||
$errno = (int) $ex->getHandlerContext()['errno'];
|
||||
|
||||
// By default, all supported encoding types are sent via `Accept-Encoding` and decoding of
|
||||
// responses with `Content-Encoding` is automatically attempted. If this fails, we do a
|
||||
// single retry with `Accept-Encoding: none` to try and force an unencoded response.
|
||||
if (($errno === \CURLE_WRITE_ERROR || $errno === \CURLE_BAD_CONTENT_ENCODING) &&
|
||||
$ex->getRequest()->getHeaderLine('accept-encoding') !== 'none') {
|
||||
$options['encoding'] = 'none';
|
||||
$span->end();
|
||||
return self::fetch($options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$span->setAttribute('error', self::$fetch_last_error);
|
||||
$span->end();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep setting expected 'fetch_last_error_code' and 'fetch_last_error' values
|
||||
self::$fetch_last_error_code = $response->getStatusCode();
|
||||
self::$fetch_last_error = "HTTP/{$response->getProtocolVersion()} {$response->getStatusCode()} {$response->getReasonPhrase()}";
|
||||
self::$fetch_last_modified = $response->getHeaderLine('last-modified');
|
||||
self::$fetch_last_content_type = $response->getHeaderLine('content-type');
|
||||
|
||||
// If a history header value doesn't exist there was no redirection and the original URL is fine.
|
||||
$history_header = $response->getHeader(GuzzleHttp\RedirectMiddleware::HISTORY_HEADER);
|
||||
self::$fetch_effective_url = $history_header ? end($history_header) : $url;
|
||||
|
||||
// This shouldn't be necessary given the checks that occur during potential redirects, but we'll do it anyway.
|
||||
if (!self::validate(self::$fetch_effective_url, true)) {
|
||||
self::$fetch_last_error = "URL received after redirection failed extended validation.";
|
||||
$span->setAttribute('error', self::$fetch_last_error);
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
self::$fetch_effective_ip_addr = gethostbyname(parse_url(self::$fetch_effective_url, PHP_URL_HOST));
|
||||
|
||||
if (!self::$fetch_effective_ip_addr || strpos(self::$fetch_effective_ip_addr, '127.') === 0) {
|
||||
self::$fetch_last_error = 'URL hostname received after redirection failed to resolve or resolved to a loopback address (' .
|
||||
self::$fetch_effective_ip_addr . ')';
|
||||
$span->setAttribute('error', self::$fetch_last_error);
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = (string) $response->getBody();
|
||||
|
||||
if (!$body) {
|
||||
self::$fetch_last_error = 'Successful response, but no content was received.';
|
||||
$span->setAttribute('error', self::$fetch_last_error);
|
||||
$span->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
$span->end();
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|string false if the provided URL didn't match expected patterns, otherwise the video ID string
|
||||
*/
|
||||
public static function url_to_youtube_vid(string $url) {
|
||||
$url = str_replace("youtube.com", "youtube-nocookie.com", $url);
|
||||
|
||||
$regexps = [
|
||||
"/\/\/www\.youtube-nocookie\.com\/v\/([\w-]+)/",
|
||||
"/\/\/www\.youtube-nocookie\.com\/embed\/([\w-]+)/",
|
||||
"/\/\/www\.youtube-nocookie\.com\/watch?v=([\w-]+)/",
|
||||
"/\/\/youtu.be\/([\w-]+)/",
|
||||
];
|
||||
|
||||
foreach ($regexps as $re) {
|
||||
$matches = [];
|
||||
|
||||
if (preg_match($re, $url, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -250,7 +250,6 @@ class UserHelper {
|
|||
static function reset_password(int $uid, bool $format_output = false, string $new_password = ""): void {
|
||||
|
||||
$user = ORM::for_table('ttrss_users')->find_one($uid);
|
||||
$message = "";
|
||||
|
||||
if ($user) {
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<?php
|
||||
use OpenTracing\GlobalTracer;
|
||||
use OpenTracing\Scope;
|
||||
|
||||
class Tracer {
|
||||
/** @var Tracer $instance */
|
||||
private static $instance;
|
||||
|
||||
public function __construct() {
|
||||
$jaeger_host = Config::get(Config::JAEGER_REPORTING_HOST);
|
||||
|
||||
if ($jaeger_host) {
|
||||
$config = new \Jaeger\Config(
|
||||
[
|
||||
'sampler' => [
|
||||
'type' => \Jaeger\SAMPLER_TYPE_CONST,
|
||||
'param' => true,
|
||||
],
|
||||
'logging' => true,
|
||||
"local_agent" => [
|
||||
"reporting_host" => $jaeger_host,
|
||||
"reporting_port" => 6832
|
||||
],
|
||||
'dispatch_mode' => \Jaeger\Config::JAEGER_OVER_BINARY_UDP,
|
||||
],
|
||||
Config::get(Config::JAEGER_SERVICE_NAME)
|
||||
);
|
||||
|
||||
$config->initializeTracer();
|
||||
|
||||
register_shutdown_function(function() {
|
||||
$tracer = GlobalTracer::get();
|
||||
$tracer->flush();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array<string>|array<string, array<string, mixed>> $tags
|
||||
* @param array<string> $args
|
||||
* @return Scope
|
||||
*/
|
||||
private function _start(string $name, array $tags = [], array $args = []): Scope {
|
||||
$tracer = GlobalTracer::get();
|
||||
|
||||
$tags['args'] = json_encode($args);
|
||||
|
||||
return $tracer->startActiveSpan($name, ['tags' => $tags]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array<string>|array<string, array<string, mixed>> $tags
|
||||
* @param array<string> $args
|
||||
* @return Scope
|
||||
*/
|
||||
public static function start(string $name, array $tags = [], array $args = []) : Scope {
|
||||
return self::get_instance()->_start($name, $tags, $args);
|
||||
}
|
||||
|
||||
public static function get_instance() : Tracer {
|
||||
if (self::$instance == null)
|
||||
self::$instance = new self();
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,654 +0,0 @@
|
|||
<?php
|
||||
class UrlHelper {
|
||||
const EXTRA_HREF_SCHEMES = [
|
||||
"magnet",
|
||||
"mailto",
|
||||
"tel"
|
||||
];
|
||||
|
||||
const EXTRA_SCHEMES_BY_CONTENT_TYPE = [
|
||||
"application/x-bittorrent" => [ "magnet" ],
|
||||
];
|
||||
|
||||
static string $fetch_last_error;
|
||||
static int $fetch_last_error_code;
|
||||
static string $fetch_last_error_content;
|
||||
static string $fetch_last_content_type;
|
||||
static string $fetch_last_modified;
|
||||
static string $fetch_effective_url;
|
||||
static string $fetch_effective_ip_addr;
|
||||
static bool $fetch_curl_used;
|
||||
|
||||
/**
|
||||
* @param array<string, string|int> $parts
|
||||
*/
|
||||
static function build_url(array $parts): string {
|
||||
$tmp = $parts['scheme'] . "://" . $parts['host'];
|
||||
|
||||
if (isset($parts['path'])) $tmp .= $parts['path'];
|
||||
if (isset($parts['query'])) $tmp .= '?' . $parts['query'];
|
||||
if (isset($parts['fragment'])) $tmp .= '#' . $parts['fragment'];
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a (possibly) relative URL to a absolute one, using provided base URL.
|
||||
* Provides some exceptions for additional schemes like data: if called with owning element/attribute.
|
||||
*
|
||||
* @param string $base_url Base URL (i.e. from where the document is)
|
||||
* @param string $rel_url Possibly relative URL in the document
|
||||
* @param string $owner_element Owner element tag name (i.e. "a") (optional)
|
||||
* @param string $owner_attribute Owner attribute (i.e. "href") (optional)
|
||||
* @param string $content_type URL content type as specified by enclosures, etc.
|
||||
*
|
||||
* @return false|string Absolute URL or false on failure (either during URL parsing or validation)
|
||||
*/
|
||||
public static function rewrite_relative($base_url,
|
||||
$rel_url,
|
||||
string $owner_element = "",
|
||||
string $owner_attribute = "",
|
||||
string $content_type = "") {
|
||||
|
||||
$rel_parts = parse_url($rel_url);
|
||||
|
||||
if (!$rel_url) return $base_url;
|
||||
|
||||
/**
|
||||
* If parse_url failed to parse $rel_url return false to match the current "invalid thing" behavior
|
||||
* of UrlHelper::validate().
|
||||
*
|
||||
* TODO: There are many places where a string return value is assumed. We should either update those
|
||||
* to account for the possibility of failure, or look into updating this function's return values.
|
||||
*/
|
||||
if ($rel_parts === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($rel_parts['host']) && !empty($rel_parts['scheme'])) {
|
||||
return self::validate($rel_url);
|
||||
|
||||
// protocol-relative URL (rare but they exist)
|
||||
} else if (strpos($rel_url, "//") === 0) {
|
||||
return self::validate("https:" . $rel_url);
|
||||
// allow some extra schemes for A href
|
||||
} else if (in_array($rel_parts["scheme"] ?? "", self::EXTRA_HREF_SCHEMES, true) &&
|
||||
$owner_element == "a" &&
|
||||
$owner_attribute == "href") {
|
||||
return $rel_url;
|
||||
// allow some extra schemes for links with feed-specified content type i.e. enclosures
|
||||
} else if ($content_type &&
|
||||
isset(self::EXTRA_SCHEMES_BY_CONTENT_TYPE[$content_type]) &&
|
||||
in_array($rel_parts["scheme"], self::EXTRA_SCHEMES_BY_CONTENT_TYPE[$content_type])) {
|
||||
return $rel_url;
|
||||
// allow limited subset of inline base64-encoded images for IMG elements
|
||||
} else if (($rel_parts["scheme"] ?? "") == "data" &&
|
||||
preg_match('%^image/(webp|gif|jpg|png|svg);base64,%', $rel_parts["path"]) &&
|
||||
$owner_element == "img" &&
|
||||
$owner_attribute == "src") {
|
||||
return $rel_url;
|
||||
} else {
|
||||
$base_parts = parse_url($base_url);
|
||||
|
||||
$rel_parts['host'] = $base_parts['host'] ?? "";
|
||||
$rel_parts['scheme'] = $base_parts['scheme'] ?? "";
|
||||
|
||||
if ($rel_parts['path'] ?? "") {
|
||||
|
||||
// we append dirname() of base path to relative URL path as per RFC 3986 section 5.2.2
|
||||
$base_path = with_trailing_slash(dirname($base_parts['path'] ?? ""));
|
||||
|
||||
// 1. absolute relative path (/test.html) = no-op, proceed as is
|
||||
|
||||
// 2. dotslash relative URI (./test.html) - strip "./", append base path
|
||||
if (strpos($rel_parts['path'], './') === 0) {
|
||||
$rel_parts['path'] = $base_path . substr($rel_parts['path'], 2);
|
||||
// 3. anything else relative (test.html) - append dirname() of base path
|
||||
} else if (strpos($rel_parts['path'], '/') !== 0) {
|
||||
$rel_parts['path'] = $base_path . $rel_parts['path'];
|
||||
}
|
||||
|
||||
//$rel_parts['path'] = str_replace("/./", "/", $rel_parts['path']);
|
||||
//$rel_parts['path'] = str_replace("//", "/", $rel_parts['path']);
|
||||
}
|
||||
|
||||
return self::validate(self::build_url($rel_parts));
|
||||
}
|
||||
}
|
||||
|
||||
/** extended filtering involves validation for safe ports and loopback
|
||||
* @return false|string false if something went wrong, otherwise the URL string
|
||||
*/
|
||||
static function validate(string $url, bool $extended_filtering = false) {
|
||||
|
||||
$url = clean($url);
|
||||
|
||||
# fix protocol-relative URLs
|
||||
if (strpos($url, "//") === 0)
|
||||
$url = "https:" . $url;
|
||||
|
||||
$tokens = parse_url($url);
|
||||
|
||||
// this isn't really necessary because filter_var(... FILTER_VALIDATE_URL) requires host and scheme
|
||||
// as per https://php.watch/versions/7.3/filter-var-flag-deprecation but it might save time
|
||||
if (empty($tokens['host']))
|
||||
return false;
|
||||
|
||||
if (!in_array(strtolower($tokens['scheme']), ['http', 'https']))
|
||||
return false;
|
||||
|
||||
//convert IDNA hostname to punycode if possible
|
||||
if (function_exists("idn_to_ascii")) {
|
||||
if (mb_detect_encoding($tokens['host']) != 'ASCII') {
|
||||
if (defined('IDNA_NONTRANSITIONAL_TO_ASCII') && defined('INTL_IDNA_VARIANT_UTS46')) {
|
||||
$tokens['host'] = idn_to_ascii($tokens['host'], IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
|
||||
} else {
|
||||
$tokens['host'] = idn_to_ascii($tokens['host']);
|
||||
}
|
||||
|
||||
// if `idn_to_ascii` failed
|
||||
if ($tokens['host'] === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// separate set of tokens with urlencoded 'path' because filter_var() rightfully fails on non-latin characters
|
||||
// (used for validation only, we actually request the original URL, in case of urlencode breaking it)
|
||||
$tokens_filter_var = $tokens;
|
||||
|
||||
if ($tokens['path'] ?? false) {
|
||||
$tokens_filter_var['path'] = implode("/",
|
||||
array_map("rawurlencode",
|
||||
array_map("rawurldecode",
|
||||
explode("/", $tokens['path']))));
|
||||
}
|
||||
|
||||
$url = self::build_url($tokens);
|
||||
$url_filter_var = self::build_url($tokens_filter_var);
|
||||
|
||||
if (filter_var($url_filter_var, FILTER_VALIDATE_URL) === false)
|
||||
return false;
|
||||
|
||||
if ($extended_filtering) {
|
||||
if (!in_array($tokens['port'] ?? '', [80, 443, '']))
|
||||
return false;
|
||||
|
||||
if (strtolower($tokens['host']) == 'localhost' || $tokens['host'] == '::1' || strpos($tokens['host'], '127.') === 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|string
|
||||
*/
|
||||
static function resolve_redirects(string $url, int $timeout, int $nest = 0) {
|
||||
$scope = Tracer::start(__METHOD__, ['url' => $url]);
|
||||
|
||||
// too many redirects
|
||||
if ($nest > 10) {
|
||||
$scope->getSpan()->setTag('error', 'too many redirects');
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
$context_options = array(
|
||||
'http' => array(
|
||||
'header' => array(
|
||||
'Connection: close'
|
||||
),
|
||||
'method' => 'HEAD',
|
||||
'timeout' => $timeout,
|
||||
'protocol_version'=> 1.1)
|
||||
);
|
||||
|
||||
if (Config::get(Config::HTTP_PROXY)) {
|
||||
$context_options['http']['request_fulluri'] = true;
|
||||
$context_options['http']['proxy'] = Config::get(Config::HTTP_PROXY);
|
||||
}
|
||||
|
||||
$context = stream_context_create($context_options);
|
||||
|
||||
// PHP 8 changed the second param from int to bool, but we still support PHP >= 7.4.0
|
||||
// @phpstan-ignore-next-line
|
||||
$headers = get_headers($url, 0, $context);
|
||||
|
||||
if (is_array($headers)) {
|
||||
$headers = array_reverse($headers); // last one is the correct one
|
||||
|
||||
foreach($headers as $header) {
|
||||
if (stripos($header, 'Location:') === 0) {
|
||||
$url = self::rewrite_relative($url, trim(substr($header, strlen('Location:'))));
|
||||
|
||||
return self::resolve_redirects($url, $timeout, $nest + 1);
|
||||
}
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
return $url;
|
||||
}
|
||||
|
||||
$scope->getSpan()->setTag('error', 'request failed');
|
||||
$scope->close();
|
||||
// request failed?
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool|int|string>|string $options
|
||||
* @return false|string false if something went wrong, otherwise string contents
|
||||
*/
|
||||
// TODO: max_size currently only works for CURL transfers
|
||||
// TODO: multiple-argument way is deprecated, first parameter is a hash now
|
||||
public static function fetch($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
|
||||
4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
|
||||
|
||||
self::$fetch_last_error = "";
|
||||
self::$fetch_last_error_code = -1;
|
||||
self::$fetch_last_error_content = "";
|
||||
self::$fetch_last_content_type = "";
|
||||
self::$fetch_curl_used = false;
|
||||
self::$fetch_last_modified = "";
|
||||
self::$fetch_effective_url = "";
|
||||
self::$fetch_effective_ip_addr = "";
|
||||
|
||||
if (!is_array($options)) {
|
||||
|
||||
// falling back on compatibility shim
|
||||
$option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
|
||||
$tmp = [];
|
||||
|
||||
for ($i = 0; $i < func_num_args(); $i++) {
|
||||
$tmp[$option_names[$i]] = func_get_arg($i);
|
||||
}
|
||||
|
||||
$options = $tmp;
|
||||
|
||||
/*$options = array(
|
||||
"url" => func_get_arg(0),
|
||||
"type" => @func_get_arg(1),
|
||||
"login" => @func_get_arg(2),
|
||||
"pass" => @func_get_arg(3),
|
||||
"post_query" => @func_get_arg(4),
|
||||
"timeout" => @func_get_arg(5),
|
||||
"timestamp" => @func_get_arg(6),
|
||||
"useragent" => @func_get_arg(7)
|
||||
); */
|
||||
}
|
||||
$url = $options["url"];
|
||||
|
||||
$scope = Tracer::start(__METHOD__, ['url' => $url]);
|
||||
|
||||
$type = isset($options["type"]) ? $options["type"] : false;
|
||||
$login = isset($options["login"]) ? $options["login"] : false;
|
||||
$pass = isset($options["pass"]) ? $options["pass"] : false;
|
||||
$post_query = isset($options["post_query"]) ? $options["post_query"] : false;
|
||||
$timeout = isset($options["timeout"]) ? $options["timeout"] : false;
|
||||
$last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
|
||||
$useragent = isset($options["useragent"]) ? $options["useragent"] : false;
|
||||
$followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
|
||||
$max_size = isset($options["max_size"]) ? $options["max_size"] : Config::get(Config::MAX_DOWNLOAD_FILE_SIZE); // in bytes
|
||||
$http_accept = isset($options["http_accept"]) ? $options["http_accept"] : false;
|
||||
$http_referrer = isset($options["http_referrer"]) ? $options["http_referrer"] : false;
|
||||
|
||||
$url = ltrim($url, ' ');
|
||||
$url = str_replace(' ', '%20', $url);
|
||||
|
||||
Debug::log("[UrlHelper] fetching: $url", Debug::LOG_EXTENDED);
|
||||
|
||||
$url = self::validate($url, true);
|
||||
|
||||
if (!$url) {
|
||||
self::$fetch_last_error = "Requested URL failed extended validation.";
|
||||
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
$url_host = parse_url($url, PHP_URL_HOST);
|
||||
$ip_addr = gethostbyname($url_host);
|
||||
|
||||
if (!$ip_addr || strpos($ip_addr, "127.") === 0) {
|
||||
self::$fetch_last_error = "URL hostname failed to resolve or resolved to a loopback address ($ip_addr)";
|
||||
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (function_exists('curl_init') && !ini_get("open_basedir")) {
|
||||
|
||||
self::$fetch_curl_used = true;
|
||||
|
||||
$ch = curl_init($url);
|
||||
|
||||
if (!$ch) {
|
||||
self::$fetch_last_error = "curl_init() failed";
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
$curl_http_headers = [];
|
||||
|
||||
if ($last_modified && !$post_query)
|
||||
array_push($curl_http_headers, "If-Modified-Since: $last_modified");
|
||||
|
||||
if ($http_accept)
|
||||
array_push($curl_http_headers, "Accept: " . $http_accept);
|
||||
|
||||
if (count($curl_http_headers) > 0)
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_http_headers);
|
||||
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT));
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : Config::get(Config::FILE_FETCH_TIMEOUT));
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $followlocation);
|
||||
curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
|
||||
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent : Config::get_user_agent());
|
||||
curl_setopt($ch, CURLOPT_ENCODING, "");
|
||||
curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
|
||||
|
||||
if ($http_referrer)
|
||||
curl_setopt($ch, CURLOPT_REFERER, $http_referrer);
|
||||
|
||||
if ($max_size) {
|
||||
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
|
||||
curl_setopt($ch, CURLOPT_BUFFERSIZE, 16384); // needed to get 5 arguments in progress function?
|
||||
|
||||
// holy shit closures in php
|
||||
// download & upload are *expected* sizes respectively, could be zero
|
||||
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($curl_handle, $download_size, $downloaded, $upload_size, $uploaded) use(&$max_size, $url) {
|
||||
//Debug::log("[curl progressfunction] $downloaded $max_size", Debug::$LOG_EXTENDED);
|
||||
|
||||
if ($downloaded > $max_size) {
|
||||
Debug::log("[UrlHelper] fetch error: curl reached max size of $max_size bytes downloading $url, aborting.", Debug::LOG_VERBOSE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (Config::get(Config::HTTP_PROXY)) {
|
||||
curl_setopt($ch, CURLOPT_PROXY, Config::get(Config::HTTP_PROXY));
|
||||
}
|
||||
|
||||
if ($post_query) {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_query);
|
||||
}
|
||||
|
||||
if ($login && $pass)
|
||||
curl_setopt($ch, CURLOPT_USERPWD, "$login:$pass");
|
||||
|
||||
$ret = @curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
// CURLAUTH_BASIC didn't work, let's retry with CURLAUTH_ANY in case it's actually something
|
||||
// unusual like NTLM...
|
||||
if ($http_code == 403 && $login && $pass) {
|
||||
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
|
||||
|
||||
$ret = @curl_exec($ch);
|
||||
}
|
||||
|
||||
if (curl_errno($ch) === 23 || curl_errno($ch) === 61) {
|
||||
curl_setopt($ch, CURLOPT_ENCODING, 'none');
|
||||
$ret = @curl_exec($ch);
|
||||
}
|
||||
|
||||
$headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$headers = explode("\r\n", substr($ret, 0, $headers_length));
|
||||
$contents = substr($ret, $headers_length);
|
||||
|
||||
foreach ($headers as $header) {
|
||||
if (strstr($header, ": ") !== false) {
|
||||
list ($key, $value) = explode(": ", $header);
|
||||
|
||||
if (strtolower($key) == "last-modified") {
|
||||
self::$fetch_last_modified = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (substr(strtolower($header), 0, 7) == 'http/1.') {
|
||||
self::$fetch_last_error_code = (int) substr($header, 9, 3);
|
||||
self::$fetch_last_error = $header;
|
||||
}
|
||||
}
|
||||
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
self::$fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
|
||||
self::$fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
|
||||
|
||||
if (!self::validate(self::$fetch_effective_url, true)) {
|
||||
self::$fetch_last_error = "URL received after redirection failed extended validation.";
|
||||
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
self::$fetch_effective_ip_addr = gethostbyname(parse_url(self::$fetch_effective_url, PHP_URL_HOST));
|
||||
|
||||
if (!self::$fetch_effective_ip_addr || strpos(self::$fetch_effective_ip_addr, "127.") === 0) {
|
||||
self::$fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address (".self::$fetch_effective_ip_addr.")";
|
||||
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
self::$fetch_last_error_code = $http_code;
|
||||
|
||||
if ($http_code != 200 || $type && strpos(self::$fetch_last_content_type, "$type") === false) {
|
||||
|
||||
if (curl_errno($ch) != 0) {
|
||||
self::$fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
|
||||
} else {
|
||||
self::$fetch_last_error = "HTTP Code: $http_code ";
|
||||
}
|
||||
|
||||
self::$fetch_last_error_content = $contents;
|
||||
curl_close($ch);
|
||||
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$contents) {
|
||||
if (curl_errno($ch) === 0) {
|
||||
self::$fetch_last_error = 'Successful response, but no content was received.';
|
||||
} else {
|
||||
self::$fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
$is_gzipped = RSSUtils::is_gzipped($contents);
|
||||
|
||||
if ($is_gzipped && is_string($contents)) {
|
||||
$tmp = @gzdecode($contents);
|
||||
|
||||
if ($tmp) $contents = $tmp;
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
|
||||
return $contents;
|
||||
} else {
|
||||
|
||||
self::$fetch_curl_used = false;
|
||||
|
||||
if ($login && $pass){
|
||||
$url_parts = array();
|
||||
|
||||
preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
|
||||
|
||||
$pass = urlencode($pass);
|
||||
|
||||
if ($url_parts[1] && $url_parts[2]) {
|
||||
$url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: should this support POST requests or not? idk
|
||||
|
||||
$context_options = array(
|
||||
'http' => array(
|
||||
'header' => array(
|
||||
'Connection: close'
|
||||
),
|
||||
'method' => 'GET',
|
||||
'ignore_errors' => true,
|
||||
'timeout' => $timeout ? $timeout : Config::get(Config::FILE_FETCH_TIMEOUT),
|
||||
'protocol_version'=> 1.1)
|
||||
);
|
||||
|
||||
if (!$post_query && $last_modified)
|
||||
array_push($context_options['http']['header'], "If-Modified-Since: $last_modified");
|
||||
|
||||
if ($http_accept)
|
||||
array_push($context_options['http']['header'], "Accept: $http_accept");
|
||||
|
||||
if ($http_referrer)
|
||||
array_push($context_options['http']['header'], "Referer: $http_referrer");
|
||||
|
||||
if (Config::get(Config::HTTP_PROXY)) {
|
||||
$context_options['http']['request_fulluri'] = true;
|
||||
$context_options['http']['proxy'] = Config::get(Config::HTTP_PROXY);
|
||||
}
|
||||
|
||||
$context = stream_context_create($context_options);
|
||||
|
||||
$old_error = error_get_last();
|
||||
|
||||
self::$fetch_effective_url = self::resolve_redirects($url, $timeout ? $timeout : Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT));
|
||||
|
||||
if (!self::validate(self::$fetch_effective_url, true)) {
|
||||
self::$fetch_last_error = "URL received after redirection failed extended validation.";
|
||||
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
self::$fetch_effective_ip_addr = gethostbyname(parse_url(self::$fetch_effective_url, PHP_URL_HOST));
|
||||
|
||||
if (!self::$fetch_effective_ip_addr || strpos(self::$fetch_effective_ip_addr, "127.") === 0) {
|
||||
self::$fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address (".self::$fetch_effective_ip_addr.")";
|
||||
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = @file_get_contents($url, false, $context);
|
||||
|
||||
if ($data === false) {
|
||||
self::$fetch_last_error = "'file_get_contents' failed.";
|
||||
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($http_response_header as $header) {
|
||||
if (strstr($header, ": ") !== false) {
|
||||
list ($key, $value) = explode(": ", $header);
|
||||
|
||||
$key = strtolower($key);
|
||||
|
||||
if ($key == 'content-type') {
|
||||
self::$fetch_last_content_type = $value;
|
||||
// don't abort here b/c there might be more than one
|
||||
// e.g. if we were being redirected -- last one is the right one
|
||||
} else if ($key == 'last-modified') {
|
||||
self::$fetch_last_modified = $value;
|
||||
} else if ($key == 'location') {
|
||||
self::$fetch_effective_url = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (substr(strtolower($header), 0, 7) == 'http/1.') {
|
||||
self::$fetch_last_error_code = (int) substr($header, 9, 3);
|
||||
self::$fetch_last_error = $header;
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$fetch_last_error_code != 200) {
|
||||
$error = error_get_last();
|
||||
|
||||
if (($error['message'] ?? '') != ($old_error['message'] ?? '')) {
|
||||
self::$fetch_last_error .= "; " . $error["message"];
|
||||
}
|
||||
|
||||
self::$fetch_last_error_content = $data;
|
||||
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($data) {
|
||||
$is_gzipped = RSSUtils::is_gzipped($data);
|
||||
|
||||
if ($is_gzipped) {
|
||||
$tmp = @gzdecode($data);
|
||||
|
||||
if ($tmp) $data = $tmp;
|
||||
}
|
||||
|
||||
$scope->close();
|
||||
return $data;
|
||||
} else {
|
||||
self::$fetch_last_error = 'Successful response, but no content was received.';
|
||||
|
||||
$scope->getSpan()->setTag('error', self::$fetch_last_error);
|
||||
$scope->close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|string false if the provided URL didn't match expected patterns, otherwise the video ID string
|
||||
*/
|
||||
public static function url_to_youtube_vid(string $url) {
|
||||
$url = str_replace("youtube.com", "youtube-nocookie.com", $url);
|
||||
|
||||
$regexps = [
|
||||
"/\/\/www\.youtube-nocookie\.com\/v\/([\w-]+)/",
|
||||
"/\/\/www\.youtube-nocookie\.com\/embed\/([\w-]+)/",
|
||||
"/\/\/www\.youtube-nocookie\.com\/watch?v=([\w-]+)/",
|
||||
"/\/\/youtu.be\/([\w-]+)/",
|
||||
];
|
||||
|
||||
foreach ($regexps as $re) {
|
||||
$matches = [];
|
||||
|
||||
if (preg_match($re, $url, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"config": {
|
||||
"platform-check": false
|
||||
"platform-check": false,
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
|
@ -9,15 +12,24 @@
|
|||
"url": "https://dev.tt-rss.org/fox/idiorm.git"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"": "classes/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"spomky-labs/otphp": "^10.0",
|
||||
"chillerlan/php-qrcode": "^4.3.3",
|
||||
"mervick/material-design-icons": "^2.2",
|
||||
"j4mie/idiorm": "dev-master",
|
||||
"jonahgeorge/jaeger-client-php": "^1.4"
|
||||
"open-telemetry/exporter-otlp": "^1.0",
|
||||
"php-http/guzzle7-adapter": "^1.0",
|
||||
"soundasleep/html2text": "^2.1",
|
||||
"guzzlehttp/guzzle": "^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "1.10.3",
|
||||
"phpunit/phpunit": "9.5.16"
|
||||
"phpunit/phpunit": "9.5.16",
|
||||
"phpunit/php-code-coverage": "^9.2"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,59 @@
|
|||
#
|
||||
# simplified compose FOR LOCAL DEVELOPMENT.
|
||||
#
|
||||
# please don't use this in production: it has no database persistence and maps to tt-rss source directly.
|
||||
#
|
||||
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- POSTGRES_USER=${TTRSS_DB_USER}
|
||||
- POSTGRES_PASSWORD=${TTRSS_DB_PASS}
|
||||
- POSTGRES_DB=${TTRSS_DB_NAME}
|
||||
|
||||
app:
|
||||
image: cthulhoo/ttrss-fpm-pgsql-static:latest
|
||||
environment:
|
||||
SKIP_RSYNC_ON_STARTUP: true
|
||||
build:
|
||||
dockerfile: .docker/app/Dockerfile
|
||||
context: .
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- .:/var/www/html/tt-rss
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
updater:
|
||||
image: cthulhoo/ttrss-fpm-pgsql-static:latest
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- .:/var/www/html/tt-rss
|
||||
depends_on:
|
||||
- app
|
||||
command: /opt/tt-rss/updater.sh
|
||||
|
||||
web-nginx:
|
||||
image: cthulhoo/ttrss-web-nginx:latest
|
||||
build:
|
||||
dockerfile: .docker/web-nginx/Dockerfile
|
||||
context: .
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- ${HTTP_PORT}:80
|
||||
volumes:
|
||||
- .:/var/www/html/tt-rss:ro
|
||||
depends_on:
|
||||
- app
|
|
@ -1,17 +1,2 @@
|
|||
<?php
|
||||
spl_autoload_register(function($class) {
|
||||
|
||||
$root_dir = dirname(__DIR__); // we were in tt-rss/include
|
||||
|
||||
// - internal tt-rss classes are loaded from classes/ and use special naming logic instead of namespaces
|
||||
// - plugin classes are loaded by PluginHandler from plugins.local/ and plugins/
|
||||
|
||||
$class_file = "$root_dir/classes/" . str_replace("_", "/", strtolower($class)) . ".php";
|
||||
|
||||
if (file_exists($class_file))
|
||||
include $class_file;
|
||||
|
||||
});
|
||||
|
||||
// also pull composer autoloader
|
||||
require_once "vendor/autoload.php";
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
}
|
||||
|
||||
function pluginhandler_tags(\Plugin $plugin, string $method): string {
|
||||
return hidden_tag("op", "pluginhandler") .
|
||||
return hidden_tag("op", "PluginHandler") .
|
||||
hidden_tag("plugin", strtolower(get_class($plugin))) .
|
||||
hidden_tag("method", $method);
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
|
||||
/**
|
||||
* @return bool|int|null|string
|
||||
*
|
||||
* @deprecated by Prefs::get()
|
||||
*/
|
||||
function get_pref(string $pref_name, int $owner_uid = null) {
|
||||
return Prefs::get($pref_name, $owner_uid ? $owner_uid : $_SESSION["uid"], $_SESSION["profile"] ?? null);
|
||||
|
@ -45,6 +47,8 @@
|
|||
|
||||
/**
|
||||
* @param bool|int|string $value
|
||||
*
|
||||
* @deprecated by Prefs::set()
|
||||
*/
|
||||
function set_pref(string $pref_name, $value, int $owner_uid = null, bool $strip_tags = true): bool {
|
||||
return Prefs::set($pref_name, $value, $owner_uid ? $owner_uid : $_SESSION["uid"], $_SESSION["profile"] ?? null, $strip_tags);
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
|
||||
</script>
|
||||
|
||||
<?php $return = urlencode(!empty($_REQUEST['return']) ? $_REQUEST['return'] : with_trailing_slash(Config::make_self_url())) ?>
|
||||
<?php $return = urlencode(!empty($_REQUEST['return']) ? $_REQUEST['return'] : with_trailing_slash(Config::get_self_url())) ?>
|
||||
|
||||
<div class="container">
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
$sth = \Db::pdo()->prepare("SELECT id FROM ttrss_sessions WHERE id=?");
|
||||
$sth->execute([$id]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
if ($sth->fetch()) {
|
||||
$sth = \Db::pdo()->prepare("UPDATE ttrss_sessions SET data=?, expire=? WHERE id=?");
|
||||
$sth->execute([$data, $expire, $id]);
|
||||
} else {
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
<style type="text/css">
|
||||
<?php
|
||||
foreach (PluginHost::getInstance()->get_plugins() as $n => $p) {
|
||||
foreach (PluginHost::getInstance()->get_plugins() as $p) {
|
||||
if (method_exists($p, "get_css")) {
|
||||
echo $p->get_css();
|
||||
}
|
||||
|
@ -263,6 +263,7 @@
|
|||
|
||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcPrefs')"><?= __('Preferences...') ?></div>
|
||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcSearch')"><?= __('Search...') ?></div>
|
||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcFilterFeeds')"><?= __('Search feeds...') ?></div>
|
||||
<div dojoType="dijit.MenuItem" disabled="1"><?= __('Feed actions:') ?></div>
|
||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcAddFeed')"><?= __('Subscribe to feed...') ?></div>
|
||||
<div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcEditFeed')"><?= __('Edit this feed...') ?></div>
|
||||
|
|
31
js/App.js
31
js/App.js
|
@ -153,7 +153,7 @@ const App = {
|
|||
return dijit.getEnclosingWidget(elem.closest('.dijitDialog'));
|
||||
},
|
||||
getPhArgs(plugin, method, args = {}) {
|
||||
return {...{op: "pluginhandler", plugin: plugin, method: method}, ...args};
|
||||
return {...{op: "PluginHandler", plugin: plugin, method: method}, ...args};
|
||||
},
|
||||
label_to_feed_id: function(label) {
|
||||
return this.LABEL_BASE_INDEX - 1 - Math.abs(label);
|
||||
|
@ -291,7 +291,7 @@ const App = {
|
|||
setCombinedMode: function(combined) {
|
||||
const value = combined ? "true" : "false";
|
||||
|
||||
xhr.post("backend.php", {op: "rpc", method: "setpref", key: "COMBINED_DISPLAY_MODE", value: value}, () => {
|
||||
xhr.post("backend.php", {op: "RPC", method: "setpref", key: "COMBINED_DISPLAY_MODE", value: value}, () => {
|
||||
this.setInitParam("combined_display_mode",
|
||||
!this.getInitParam("combined_display_mode"));
|
||||
|
||||
|
@ -306,7 +306,7 @@ const App = {
|
|||
if (App.isCombinedMode()) {
|
||||
const value = expand ? "true" : "false";
|
||||
|
||||
xhr.post("backend.php", {op: "rpc", method: "setpref", key: "CDM_EXPANDED", value: value}, () => {
|
||||
xhr.post("backend.php", {op: "RPC", method: "setpref", key: "CDM_EXPANDED", value: value}, () => {
|
||||
this.setInitParam("cdm_expanded", !this.getInitParam("cdm_expanded"));
|
||||
Headlines.renderAgain();
|
||||
});
|
||||
|
@ -440,7 +440,7 @@ const App = {
|
|||
}
|
||||
},
|
||||
hotkeyHelp: function() {
|
||||
xhr.post("backend.php", {op: "rpc", method: "hotkeyHelp"}, (reply) => {
|
||||
xhr.post("backend.php", {op: "RPC", method: "hotkeyHelp"}, (reply) => {
|
||||
const dialog = new fox.SingleUseDialog({
|
||||
title: __("Keyboard shortcuts"),
|
||||
content: reply,
|
||||
|
@ -621,7 +621,7 @@ const App = {
|
|||
|
||||
try {
|
||||
xhr.post("backend.php",
|
||||
{op: "rpc", method: "log",
|
||||
{op: "RPC", method: "log",
|
||||
file: params.filename ? params.filename : error.fileName,
|
||||
line: params.lineno ? params.lineno : error.lineNumber,
|
||||
msg: message,
|
||||
|
@ -703,7 +703,7 @@ const App = {
|
|||
this.initHotkeyActions();
|
||||
|
||||
const params = {
|
||||
op: "rpc",
|
||||
op: "RPC",
|
||||
method: "sanityCheck",
|
||||
clientTzOffset: new Date().getTimezoneOffset() * 60,
|
||||
hasSandbox: "sandbox" in document.createElement("iframe"),
|
||||
|
@ -737,7 +737,7 @@ const App = {
|
|||
return errorMsg == "";
|
||||
},
|
||||
updateRuntimeInfo: function() {
|
||||
xhr.json("backend.php", {op: "rpc", method: "getruntimeinfo"}, () => {
|
||||
xhr.json("backend.php", {op: "RPC", method: "getruntimeinfo"}, () => {
|
||||
// handled by xhr.json()
|
||||
});
|
||||
},
|
||||
|
@ -858,7 +858,7 @@ const App = {
|
|||
checkForUpdates: function() {
|
||||
console.log('checking for updates...');
|
||||
|
||||
xhr.json("backend.php", {op: 'rpc', method: 'checkforupdates'})
|
||||
xhr.json("backend.php", {op: 'RPC', method: 'checkforupdates'})
|
||||
.then((reply) => {
|
||||
console.log('update reply', reply);
|
||||
|
||||
|
@ -965,7 +965,7 @@ const App = {
|
|||
|
||||
if (article_id) Article.view(article_id);
|
||||
|
||||
xhr.post("backend.php", {op: "rpc", method: "setWidescreen", wide: wide ? 1 : 0});
|
||||
xhr.post("backend.php", {op: "RPC", method: "setWidescreen", wide: wide ? 1 : 0});
|
||||
},
|
||||
initHotkeyActions: function() {
|
||||
if (this.is_prefs) {
|
||||
|
@ -1149,7 +1149,7 @@ const App = {
|
|||
if (!Feeds.activeIsCat() && parseInt(Feeds.getActive()) > 0) {
|
||||
|
||||
/* global __csrf_token */
|
||||
App.postOpenWindow("backend.php", {op: "feeds", method: "updatedebugger",
|
||||
App.postOpenWindow("backend.php", {op: "Feeds", method: "updatedebugger",
|
||||
feed_id: Feeds.getActive(), csrf_token: __csrf_token});
|
||||
|
||||
} else {
|
||||
|
@ -1158,7 +1158,7 @@ const App = {
|
|||
};
|
||||
|
||||
this.hotkey_actions["feed_debug_viewfeed"] = () => {
|
||||
App.postOpenWindow("backend.php", {op: "feeds", method: "view",
|
||||
App.postOpenWindow("backend.php", {op: "Feeds", method: "view",
|
||||
feed: Feeds.getActive(), timestamps: 1, debug: 1, cat: Feeds.activeIsCat(), csrf_token: __csrf_token});
|
||||
};
|
||||
|
||||
|
@ -1177,13 +1177,13 @@ const App = {
|
|||
Headlines.reverse();
|
||||
};
|
||||
this.hotkey_actions["feed_toggle_grid"] = () => {
|
||||
xhr.json("backend.php", {op: "rpc", method: "togglepref", key: "CDM_ENABLE_GRID"}, (reply) => {
|
||||
xhr.json("backend.php", {op: "RPC", method: "togglepref", key: "CDM_ENABLE_GRID"}, (reply) => {
|
||||
App.setInitParam("cdm_enable_grid", reply.value);
|
||||
Headlines.renderAgain();
|
||||
})
|
||||
};
|
||||
this.hotkey_actions["feed_toggle_vgroup"] = () => {
|
||||
xhr.post("backend.php", {op: "rpc", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => {
|
||||
xhr.post("backend.php", {op: "RPC", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => {
|
||||
Feeds.reloadCurrent();
|
||||
})
|
||||
};
|
||||
|
@ -1270,11 +1270,14 @@ const App = {
|
|||
case "qmcSearch":
|
||||
Feeds.search();
|
||||
break;
|
||||
case "qmcFilterFeeds":
|
||||
Feeds.filter();
|
||||
break;
|
||||
case "qmcAddFeed":
|
||||
CommonDialogs.subscribeToFeed();
|
||||
break;
|
||||
case "qmcDigest":
|
||||
window.location.href = "backend.php?op=digest";
|
||||
window.location.href = "backend.php?op=Digest";
|
||||
break;
|
||||
case "qmcEditFeed":
|
||||
if (Feeds.activeIsCat())
|
||||
|
|
|
@ -123,7 +123,7 @@ const Article = {
|
|||
Article.setActive(0);
|
||||
},
|
||||
displayUrl: function (id) {
|
||||
const query = {op: "article", method: "getmetadatabyid", id: id};
|
||||
const query = {op: "Article", method: "getmetadatabyid", id: id};
|
||||
|
||||
xhr.json("backend.php", query, (reply) => {
|
||||
if (reply && reply.link) {
|
||||
|
@ -136,7 +136,7 @@ const Article = {
|
|||
openInNewWindow: function (id) {
|
||||
/* global __csrf_token */
|
||||
App.postOpenWindow("backend.php",
|
||||
{ "op": "article", "method": "redirect", "id": id, "csrf_token": __csrf_token });
|
||||
{ "op": "Article", "method": "redirect", "id": id, "csrf_token": __csrf_token });
|
||||
|
||||
Headlines.toggleUnread(id, 0);
|
||||
},
|
||||
|
@ -352,7 +352,7 @@ const Article = {
|
|||
title: __("Article tags"),
|
||||
content: `
|
||||
${App.FormFields.hidden_tag("id", id.toString())}
|
||||
${App.FormFields.hidden_tag("op", "article")}
|
||||
${App.FormFields.hidden_tag("op", "Article")}
|
||||
${App.FormFields.hidden_tag("method", "setArticleTags")}
|
||||
|
||||
<header class='horizontal'>
|
||||
|
@ -395,7 +395,7 @@ const Article = {
|
|||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
||||
dojo.disconnect(tmph);
|
||||
|
||||
xhr.json("backend.php", {op: "article", method: "printArticleTags", id: id}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Article", method: "printArticleTags", id: id}, (reply) => {
|
||||
|
||||
dijit.getEnclosingWidget(App.byId("tags_str"))
|
||||
.attr('value', reply.tags.join(", "))
|
||||
|
@ -404,7 +404,7 @@ const Article = {
|
|||
App.byId('tags_str').onkeyup = (e) => {
|
||||
const last_tag = e.target.value.split(',').pop().trim();
|
||||
|
||||
xhr.json("backend.php", {op: 'article', method: 'completeTags', search: last_tag}, (data) => {
|
||||
xhr.json("backend.php", {op: 'Article', method: 'completeTags', search: last_tag}, (data) => {
|
||||
App.byId("tags_choices").innerHTML = `${data.map((tag) =>
|
||||
`<a href="#" onclick="Article.autocompleteInject(this, 'tags_str')">${tag}</a>` )
|
||||
.join(', ')}`
|
||||
|
|
|
@ -28,14 +28,14 @@ const CommonDialogs = {
|
|||
},
|
||||
subscribeToFeed: function() {
|
||||
xhr.json("backend.php",
|
||||
{op: "feeds", method: "subscribeToFeed"},
|
||||
{op: "Feeds", method: "subscribeToFeed"},
|
||||
(reply) => {
|
||||
const dialog = new fox.SingleUseDialog({
|
||||
title: __("Subscribe to feed"),
|
||||
content: `
|
||||
<form onsubmit='return false'>
|
||||
|
||||
${App.FormFields.hidden_tag("op", "feeds")}
|
||||
${App.FormFields.hidden_tag("op", "Feeds")}
|
||||
${App.FormFields.hidden_tag("method", "add")}
|
||||
|
||||
<div id='fadd_error_message' style='display : none' class='alert alert-danger'></div>
|
||||
|
@ -215,7 +215,7 @@ const CommonDialogs = {
|
|||
},
|
||||
showFeedsWithErrors: function() {
|
||||
|
||||
xhr.json("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Feeds", method: "feedsWithErrors"}, (reply) => {
|
||||
|
||||
const dialog = new fox.SingleUseDialog({
|
||||
id: "errorFeedsDlg",
|
||||
|
@ -231,7 +231,7 @@ const CommonDialogs = {
|
|||
Notify.progress("Removing selected feeds...", true);
|
||||
|
||||
const query = {
|
||||
op: "pref-feeds", method: "remove",
|
||||
op: "Pref_Feeds", method: "remove",
|
||||
ids: sel_rows.toString()
|
||||
};
|
||||
|
||||
|
@ -268,7 +268,7 @@ const CommonDialogs = {
|
|||
<table width='100%' id='error-feeds-list'>
|
||||
|
||||
${reply.map((row) => `
|
||||
<tr data-row-id='${row.id}'>
|
||||
<tr data-row-id='${row.id}' style='vertical-align: top'>
|
||||
<td class='checkbox'>
|
||||
<input onclick='Tables.onRowChecked(this)' dojoType="dijit.form.CheckBox"
|
||||
type="checkbox">
|
||||
|
@ -305,7 +305,7 @@ const CommonDialogs = {
|
|||
|
||||
if (caption != undefined && caption.trim().length > 0) {
|
||||
|
||||
const query = {op: "pref-labels", method: "add", caption: caption.trim()};
|
||||
const query = {op: "Pref_Labels", method: "add", caption: caption.trim()};
|
||||
|
||||
Notify.progress("Loading, please wait...", true);
|
||||
|
||||
|
@ -325,7 +325,7 @@ const CommonDialogs = {
|
|||
if (typeof title == "undefined" || confirm(msg)) {
|
||||
Notify.progress("Removing feed...");
|
||||
|
||||
const query = {op: "pref-feeds", quiet: 1, method: "remove", ids: feed_id};
|
||||
const query = {op: "Pref_Feeds", quiet: 1, method: "remove", ids: feed_id};
|
||||
|
||||
xhr.post("backend.php", query, () => {
|
||||
if (App.isPrefs()) {
|
||||
|
@ -348,7 +348,7 @@ const CommonDialogs = {
|
|||
if (feed_id <= 0)
|
||||
return alert(__("You can't edit this kind of feed."));
|
||||
|
||||
const query = {op: "pref-feeds", method: "editfeed", id: feed_id};
|
||||
const query = {op: "Pref_Feeds", method: "editfeed", id: feed_id};
|
||||
|
||||
console.log("editFeed", query);
|
||||
|
||||
|
@ -378,7 +378,7 @@ const CommonDialogs = {
|
|||
const fd = new FormData();
|
||||
fd.append('icon_file', icon_file)
|
||||
fd.append('feed_id', feed_id);
|
||||
fd.append('op', 'pref-feeds');
|
||||
fd.append('op', 'Pref_Feeds');
|
||||
fd.append('method', 'uploadIcon');
|
||||
fd.append('csrf_token', App.getInitParam("csrf_token"));
|
||||
|
||||
|
@ -427,7 +427,7 @@ const CommonDialogs = {
|
|||
if (confirm(__("Remove stored feed icon?"))) {
|
||||
Notify.progress("Removing feed icon...", true);
|
||||
|
||||
xhr.post("backend.php", {op: "pref-feeds", method: "removeicon", feed_id: id}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Feeds", method: "removeicon", feed_id: id}, () => {
|
||||
Notify.info("Feed icon removed.");
|
||||
|
||||
if (App.isPrefs())
|
||||
|
@ -470,7 +470,7 @@ const CommonDialogs = {
|
|||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
||||
dojo.disconnect(tmph);
|
||||
|
||||
xhr.json("backend.php", {op: "pref-feeds", method: "editfeed", id: feed_id}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Feeds", method: "editfeed", id: feed_id}, (reply) => {
|
||||
const feed = reply.feed;
|
||||
const is_readonly = reply.user.access_level == App.UserAccessLevels.ACCESS_LEVEL_READONLY;
|
||||
|
||||
|
@ -493,7 +493,7 @@ const CommonDialogs = {
|
|||
<div dojoType="dijit.layout.ContentPane" title="${__('General')}">
|
||||
|
||||
${App.FormFields.hidden_tag("id", feed_id)}
|
||||
${App.FormFields.hidden_tag("op", "pref-feeds")}
|
||||
${App.FormFields.hidden_tag("op", "Pref_Feeds")}
|
||||
${App.FormFields.hidden_tag("method", "editSave")}
|
||||
|
||||
<section>
|
||||
|
@ -621,7 +621,7 @@ const CommonDialogs = {
|
|||
|
||||
Notify.progress("Loading, please wait...", true);
|
||||
|
||||
xhr.json("backend.php", {op: "pref-feeds", method: "getsharedurl", id: feed, is_cat: is_cat, search: search}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Feeds", method: "getsharedurl", id: feed, is_cat: is_cat, search: search}, (reply) => {
|
||||
try {
|
||||
const dialog = new fox.SingleUseDialog({
|
||||
title: __("Show as feed"),
|
||||
|
@ -630,7 +630,7 @@ const CommonDialogs = {
|
|||
|
||||
Notify.progress("Trying to change address...", true);
|
||||
|
||||
const query = {op: "pref-feeds", method: "regenFeedKey", id: feed, is_cat: is_cat};
|
||||
const query = {op: "Pref_Feeds", method: "regenFeedKey", id: feed, is_cat: is_cat};
|
||||
|
||||
xhr.json("backend.php", query, (reply) => {
|
||||
const new_link = reply.link;
|
||||
|
|
|
@ -115,7 +115,7 @@ const Filters = {
|
|||
insertRule: function(parentNode, replaceNode) {
|
||||
const rule = dojo.formToJson("filter_new_rule_form");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-filters", method: "printrulename", rule: rule}, (reply) => {
|
||||
xhr.post("backend.php", {op: "Pref_Filters", method: "printrulename", rule: rule}, (reply) => {
|
||||
try {
|
||||
const li = document.createElement('li');
|
||||
li.addClassName("rule");
|
||||
|
@ -147,7 +147,7 @@ const Filters = {
|
|||
|
||||
const action = dojo.formToJson(form);
|
||||
|
||||
xhr.post("backend.php", { op: "pref-filters", method: "printactionname", action: action }, (reply) => {
|
||||
xhr.post("backend.php", { op: "Pref_Filters", method: "printactionname", action: action }, (reply) => {
|
||||
try {
|
||||
const li = document.createElement('li');
|
||||
li.addClassName("action");
|
||||
|
@ -200,7 +200,7 @@ const Filters = {
|
|||
|
||||
console.log(rule, dialog.filter_info);
|
||||
|
||||
xhr.json("backend.php", {op: "pref-filters", method: "editrule", ids: rule.feed_id.join(",")}, function (editrule) {
|
||||
xhr.json("backend.php", {op: "Pref_Filters", method: "editrule", ids: rule.feed_id.join(",")}, function (editrule) {
|
||||
edit_rule_dialog.attr('content',
|
||||
`
|
||||
<form name="filter_new_rule_form" id="filter_new_rule_form" onsubmit="return false">
|
||||
|
@ -326,7 +326,7 @@ const Filters = {
|
|||
|
||||
dijit.byId("filterDlg_actionSelect").attr('value', action.action_id);
|
||||
|
||||
/*xhr.post("backend.php", {op: 'pref-filters', method: 'newaction', action: actionStr}, (reply) => {
|
||||
/*xhr.post("backend.php", {op: 'Pref_Filters', method: 'newaction', action: actionStr}, (reply) => {
|
||||
edit_action_dialog.attr('content', reply);
|
||||
|
||||
setTimeout(() => {
|
||||
|
@ -365,7 +365,7 @@ const Filters = {
|
|||
|
||||
Notify.progress("Removing filter...");
|
||||
|
||||
const query = {op: "pref-filters", method: "remove", ids: this.attr('value').id};
|
||||
const query = {op: "Pref_Filters", method: "remove", ids: this.attr('value').id};
|
||||
|
||||
xhr.post("backend.php", query, () => {
|
||||
const tree = dijit.byId("filterTree");
|
||||
|
@ -411,7 +411,7 @@ const Filters = {
|
|||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
||||
dojo.disconnect(tmph);
|
||||
|
||||
xhr.json("backend.php", {op: "pref-filters", method: "edit", id: filter_id}, function (filter) {
|
||||
xhr.json("backend.php", {op: "Pref_Filters", method: "edit", id: filter_id}, function (filter) {
|
||||
|
||||
dialog.filter_info = filter;
|
||||
|
||||
|
@ -425,7 +425,7 @@ const Filters = {
|
|||
`
|
||||
<form onsubmit='return false'>
|
||||
|
||||
${App.FormFields.hidden_tag("op", "pref-filters")}
|
||||
${App.FormFields.hidden_tag("op", "Pref_Filters")}
|
||||
${App.FormFields.hidden_tag("id", filter_id)}
|
||||
${App.FormFields.hidden_tag("method", filter_id ? "editSave" : "add")}
|
||||
${App.FormFields.hidden_tag("csrf_token", App.getInitParam('csrf_token'))}
|
||||
|
@ -541,7 +541,7 @@ const Filters = {
|
|||
|
||||
dialog.editRule(null, dojo.toJson(rule));
|
||||
} else {
|
||||
const query = {op: "article", method: "getmetadatabyid", id: Article.getActive()};
|
||||
const query = {op: "Article", method: "getmetadatabyid", id: Article.getActive()};
|
||||
|
||||
xhr.json("backend.php", query, (reply) => {
|
||||
let title;
|
||||
|
|
|
@ -104,7 +104,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
|
|||
menu.addChild(new dijit.MenuItem({
|
||||
label: __("Open site"),
|
||||
onClick: function() {
|
||||
App.postOpenWindow("backend.php", {op: "feeds", method: "opensite",
|
||||
App.postOpenWindow("backend.php", {op: "Feeds", method: "opensite",
|
||||
feed_id: this.getParent().row_id, csrf_token: __csrf_token});
|
||||
}}));
|
||||
|
||||
|
@ -114,7 +114,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
|
|||
label: __("Debug feed"),
|
||||
onClick: function() {
|
||||
/* global __csrf_token */
|
||||
App.postOpenWindow("backend.php", {op: "feeds", method: "updatedebugger",
|
||||
App.postOpenWindow("backend.php", {op: "Feeds", method: "updatedebugger",
|
||||
feed_id: this.getParent().row_id, csrf_token: __csrf_token});
|
||||
}}));
|
||||
}
|
||||
|
|
69
js/Feeds.js
69
js/Feeds.js
|
@ -23,6 +23,7 @@ const Feeds = {
|
|||
infscroll_in_progress: 0,
|
||||
infscroll_disabled: 0,
|
||||
_infscroll_timeout: false,
|
||||
_filter_query: false, // TODO: figure out the UI for this
|
||||
_search_query: false,
|
||||
last_search_query: [],
|
||||
_viewfeed_wait_timeout: false,
|
||||
|
@ -154,13 +155,17 @@ const Feeds = {
|
|||
toggle: function() {
|
||||
Element.toggle("feeds-holder");
|
||||
},
|
||||
cancelFilter: function() {
|
||||
this._filter_query = "";
|
||||
this.reload();
|
||||
},
|
||||
cancelSearch: function() {
|
||||
this._search_query = "";
|
||||
this.reloadCurrent();
|
||||
},
|
||||
// null = get all data, [] would give empty response for specific type
|
||||
requestCounters: function(feed_ids = null, label_ids = null) {
|
||||
xhr.json("backend.php", {op: "rpc",
|
||||
xhr.json("backend.php", {op: "RPC",
|
||||
method: "getAllCounters",
|
||||
"feed_ids[]": feed_ids,
|
||||
"feed_id_count": feed_ids ? feed_ids.length : -1,
|
||||
|
@ -178,8 +183,14 @@ const Feeds = {
|
|||
dijit.byId("feedTree").destroyRecursive();
|
||||
}
|
||||
|
||||
let query = {op: 'Pref_Feeds', method: 'getfeedtree', mode: 2};
|
||||
|
||||
if (this._filter_query) {
|
||||
query = Object.assign(query, this._filter_query);
|
||||
}
|
||||
|
||||
const store = new dojo.data.ItemFileWriteStore({
|
||||
url: "backend.php?op=pref_feeds&method=getfeedtree&mode=2"
|
||||
url: "backend.php?" + dojo.objectToQuery(query)
|
||||
});
|
||||
|
||||
// noinspection JSUnresolvedFunction
|
||||
|
@ -347,7 +358,7 @@ const Feeds = {
|
|||
toggleUnread: function() {
|
||||
const hide = !App.getInitParam("hide_read_feeds");
|
||||
|
||||
xhr.post("backend.php", {op: "rpc", method: "setpref", key: "HIDE_READ_FEEDS", value: hide}, () => {
|
||||
xhr.post("backend.php", {op: "RPC", method: "setpref", key: "HIDE_READ_FEEDS", value: hide}, () => {
|
||||
this.hideOrShowFeeds(hide);
|
||||
App.setInitParam("hide_read_feeds", hide);
|
||||
});
|
||||
|
@ -386,7 +397,7 @@ const Feeds = {
|
|||
}, 10 * 1000);
|
||||
}
|
||||
|
||||
let query = {...{op: "feeds", method: "view", feed: feed}, ...dojo.formToObject("toolbar-main")};
|
||||
let query = {...{op: "Feeds", method: "view", feed: feed}, ...dojo.formToObject("toolbar-main")};
|
||||
|
||||
if (method) query.m = method;
|
||||
|
||||
|
@ -435,7 +446,7 @@ const Feeds = {
|
|||
|
||||
Notify.progress("Marking all feeds as read...");
|
||||
|
||||
xhr.json("backend.php", {op: "feeds", method: "catchupAll"}, () => {
|
||||
xhr.json("backend.php", {op: "Feeds", method: "catchupAll"}, () => {
|
||||
this.reloadCurrent();
|
||||
});
|
||||
|
||||
|
@ -473,7 +484,7 @@ const Feeds = {
|
|||
}
|
||||
|
||||
const catchup_query = {
|
||||
op: 'rpc', method: 'catchupFeed', feed_id: feed,
|
||||
op: 'RPC', method: 'catchupFeed', feed_id: feed,
|
||||
is_cat: is_cat, mode: mode, search_query: this.last_search_query[0],
|
||||
search_lang: this.last_search_query[1]
|
||||
};
|
||||
|
@ -612,7 +623,7 @@ const Feeds = {
|
|||
},
|
||||
search: function() {
|
||||
xhr.json("backend.php",
|
||||
{op: "feeds", method: "search"},
|
||||
{op: "Feeds", method: "search"},
|
||||
(reply) => {
|
||||
try {
|
||||
const dialog = new fox.SingleUseDialog({
|
||||
|
@ -682,11 +693,53 @@ const Feeds = {
|
|||
}
|
||||
});
|
||||
|
||||
},
|
||||
filter: function() {
|
||||
const dialog = new fox.SingleUseDialog({
|
||||
content: `
|
||||
<form onsubmit='return false'>
|
||||
<section>
|
||||
<fieldset>
|
||||
<input dojoType='dijit.form.ValidationTextBox' id='filter_query'
|
||||
style='font-size : 16px; width : 540px;'
|
||||
placeHolder="${__("Show feeds matching...")}"
|
||||
name='search' type='search' value=''>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
${App.FormFields.submit_tag(App.FormFields.icon("search") + " " + __('Search'), {onclick: "App.dialogOf(this).execute()"})}
|
||||
${App.FormFields.cancel_dialog_tag(__('Cancel'))}
|
||||
</footer>
|
||||
</form>
|
||||
`,
|
||||
title: __("Search feeds"),
|
||||
execute: function () {
|
||||
if (this.validate()) {
|
||||
Feeds._filter_query = this.attr('value');
|
||||
|
||||
this.hide();
|
||||
Feeds.reload();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
||||
dojo.disconnect(tmph);
|
||||
|
||||
if (Feeds._filter_query && Feeds._filter_query.search) {
|
||||
dijit.byId('filter_query')
|
||||
.attr('value', Feeds._filter_query.search);
|
||||
}
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
|
||||
},
|
||||
updateRandom: function() {
|
||||
console.log("in update_random_feed");
|
||||
|
||||
xhr.json("backend.php", {op: "rpc", method: "updaterandomfeed"}, () => {
|
||||
xhr.json("backend.php", {op: "RPC", method: "updaterandomfeed"}, () => {
|
||||
//
|
||||
});
|
||||
},
|
||||
|
|
|
@ -160,26 +160,26 @@ const Headlines = {
|
|||
|
||||
if (ops.tmark.length != 0)
|
||||
promises.push(xhr.post("backend.php",
|
||||
{op: "rpc", method: "markSelected", "ids[]": ops.tmark, cmode: 2}));
|
||||
{op: "RPC", method: "markSelected", "ids[]": ops.tmark, cmode: 2}));
|
||||
|
||||
if (ops.tpub.length != 0)
|
||||
promises.push(xhr.post("backend.php",
|
||||
{op: "rpc", method: "publishSelected", "ids[]": ops.tpub, cmode: 2}));
|
||||
{op: "RPC", method: "publishSelected", "ids[]": ops.tpub, cmode: 2}));
|
||||
|
||||
if (ops.read.length != 0)
|
||||
promises.push(xhr.post("backend.php",
|
||||
{op: "rpc", method: "catchupSelected", "ids[]": ops.read, cmode: 0}));
|
||||
{op: "RPC", method: "catchupSelected", "ids[]": ops.read, cmode: 0}));
|
||||
|
||||
if (ops.unread.length != 0)
|
||||
promises.push(xhr.post("backend.php",
|
||||
{op: "rpc", method: "catchupSelected", "ids[]": ops.unread, cmode: 1}));
|
||||
{op: "RPC", method: "catchupSelected", "ids[]": ops.unread, cmode: 1}));
|
||||
|
||||
const scores = Object.keys(ops.rescore);
|
||||
|
||||
if (scores.length != 0) {
|
||||
scores.forEach((score) => {
|
||||
promises.push(xhr.post("backend.php",
|
||||
{op: "article", method: "setScore", "ids[]": ops.rescore[score], score: score}));
|
||||
{op: "Article", method: "setScore", "ids[]": ops.rescore[score], score: score}));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1132,7 +1132,7 @@ const Headlines = {
|
|||
}
|
||||
|
||||
const query = {
|
||||
op: "article", method: "removeFromLabel",
|
||||
op: "Article", method: "removeFromLabel",
|
||||
ids: ids.toString(), lid: id
|
||||
};
|
||||
|
||||
|
@ -1149,7 +1149,7 @@ const Headlines = {
|
|||
}
|
||||
|
||||
const query = {
|
||||
op: "article", method: "assignToLabel",
|
||||
op: "Article", method: "assignToLabel",
|
||||
ids: ids.toString(), lid: id
|
||||
};
|
||||
|
||||
|
@ -1181,7 +1181,7 @@ const Headlines = {
|
|||
return;
|
||||
}
|
||||
|
||||
const query = {op: "rpc", method: "delete", ids: rows.toString()};
|
||||
const query = {op: "RPC", method: "delete", ids: rows.toString()};
|
||||
|
||||
xhr.json("backend.php", query, () => {
|
||||
Feeds.reloadCurrent();
|
||||
|
@ -1586,7 +1586,7 @@ const Headlines = {
|
|||
menu.addChild(new dijit.MenuItem({
|
||||
label: __("Open site"),
|
||||
onClick: function() {
|
||||
App.postOpenWindow("backend.php", {op: "feeds", method: "opensite",
|
||||
App.postOpenWindow("backend.php", {op: "Feeds", method: "opensite",
|
||||
feed_id: this.getParent().currentTarget.getAttribute("data-feed-id"), csrf_token: __csrf_token});
|
||||
}}));
|
||||
|
||||
|
@ -1596,7 +1596,7 @@ const Headlines = {
|
|||
label: __("Debug feed"),
|
||||
onClick: function() {
|
||||
/* global __csrf_token */
|
||||
App.postOpenWindow("backend.php", {op: "feeds", method: "updatedebugger",
|
||||
App.postOpenWindow("backend.php", {op: "Feeds", method: "updatedebugger",
|
||||
feed_id: this.getParent().currentTarget.getAttribute("data-feed-id"), csrf_token: __csrf_token});
|
||||
}}));
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ define(["dojo/_base/declare", "dojo/data/ItemFileWriteStore"], function (declare
|
|||
|
||||
dojo.xhrPost({
|
||||
url: "backend.php",
|
||||
content: {op: "pref-feeds", method: "savefeedorder",
|
||||
content: {op: "Pref_Feeds", method: "savefeedorder",
|
||||
payload: newFileContentString},
|
||||
error: saveFailedCallback,
|
||||
load: saveCompleteCallback});
|
||||
|
|
|
@ -150,7 +150,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
const searchElem = App.byId("feed_search");
|
||||
const search = (searchElem) ? searchElem.value : "";
|
||||
|
||||
xhr.post("backend.php", { op: "pref-feeds", search: search }, (reply) => {
|
||||
xhr.post("backend.php", { op: "Pref_Feeds", search: search }, (reply) => {
|
||||
dijit.byId('feedsTab').attr('content', reply);
|
||||
Notify.close();
|
||||
});
|
||||
|
@ -185,14 +185,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
resetFeedOrder: function() {
|
||||
Notify.progress("Loading, please wait...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-feeds", method: "feedsortreset"}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Feeds", method: "feedsortreset"}, () => {
|
||||
this.reload();
|
||||
});
|
||||
},
|
||||
resetCatOrder: function() {
|
||||
Notify.progress("Loading, please wait...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-feeds", method: "catsortreset"}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Feeds", method: "catsortreset"}, () => {
|
||||
this.reload();
|
||||
});
|
||||
},
|
||||
|
@ -200,7 +200,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
if (confirm(__("Remove category %s? Any nested feeds would be placed into Uncategorized.").replace("%s", item.name))) {
|
||||
Notify.progress("Removing category...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-feeds", method: "removeCat", ids: id}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Feeds", method: "removeCat", ids: id}, () => {
|
||||
Notify.close();
|
||||
this.reload();
|
||||
});
|
||||
|
@ -215,7 +215,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
Notify.progress("Unsubscribing from selected feeds...", true);
|
||||
|
||||
const query = {
|
||||
op: "pref-feeds", method: "remove",
|
||||
op: "Pref_Feeds", method: "remove",
|
||||
ids: sel_rows.toString()
|
||||
};
|
||||
|
||||
|
@ -231,14 +231,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
return false;
|
||||
},
|
||||
checkErrorFeeds: function() {
|
||||
xhr.json("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Feeds", method: "feedsWithErrors"}, (reply) => {
|
||||
if (reply.length > 0) {
|
||||
Element.show(dijit.byId("pref_feeds_errors_btn").domNode);
|
||||
}
|
||||
});
|
||||
},
|
||||
checkInactiveFeeds: function() {
|
||||
xhr.json("backend.php", {op: "pref-feeds", method: "inactivefeeds"}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Feeds", method: "inactivefeeds"}, (reply) => {
|
||||
if (reply.length > 0) {
|
||||
Element.show(dijit.byId("pref_feeds_inactive_btn").domNode);
|
||||
}
|
||||
|
@ -264,7 +264,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
Notify.progress("Removing selected categories...");
|
||||
|
||||
const query = {
|
||||
op: "pref-feeds", method: "removeCat",
|
||||
op: "Pref_Feeds", method: "removeCat",
|
||||
ids: sel_rows.toString()
|
||||
};
|
||||
|
||||
|
@ -316,7 +316,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
|
||||
Notify.progress("Loading, please wait...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-feeds", method: "editfeeds", ids: rows.toString()}, (reply) => {
|
||||
xhr.post("backend.php", {op: "Pref_Feeds", method: "editfeeds", ids: rows.toString()}, (reply) => {
|
||||
Notify.close();
|
||||
|
||||
try {
|
||||
|
@ -393,7 +393,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
|
||||
Notify.progress("Loading, please wait...");
|
||||
|
||||
xhr.post("backend.php", { op: 'pref-feeds', method: 'renamecat', id: id, title: new_name }, () => {
|
||||
xhr.post("backend.php", { op: 'Pref_Feeds', method: 'renamecat', id: id, title: new_name }, () => {
|
||||
this.reload();
|
||||
});
|
||||
}
|
||||
|
@ -404,14 +404,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
if (title) {
|
||||
Notify.progress("Creating category...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-feeds", method: "addCat", cat: title}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Feeds", method: "addCat", cat: title}, () => {
|
||||
Notify.close();
|
||||
this.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
batchSubscribe: function() {
|
||||
xhr.json("backend.php", {op: 'pref-feeds', method: 'batchSubscribe'}, (reply) => {
|
||||
xhr.json("backend.php", {op: 'Pref_Feeds', method: 'batchSubscribe'}, (reply) => {
|
||||
const dialog = new fox.SingleUseDialog({
|
||||
id: "batchSubDlg",
|
||||
title: __("Batch subscribe"),
|
||||
|
@ -431,7 +431,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
},
|
||||
content: `
|
||||
<form onsubmit='return false'>
|
||||
${App.FormFields.hidden_tag("op", "pref-feeds")}
|
||||
${App.FormFields.hidden_tag("op", "Pref_Feeds")}
|
||||
${App.FormFields.hidden_tag("method", "batchaddfeeds")}
|
||||
|
||||
<header class='horizontal'>
|
||||
|
@ -484,7 +484,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
});
|
||||
},
|
||||
showInactiveFeeds: function() {
|
||||
xhr.json("backend.php", {op: 'pref-feeds', method: 'inactivefeeds'}, function (reply) {
|
||||
xhr.json("backend.php", {op: 'Pref_Feeds', method: 'inactivefeeds'}, function (reply) {
|
||||
|
||||
const dialog = new fox.SingleUseDialog({
|
||||
id: "inactiveFeedsDlg",
|
||||
|
@ -500,7 +500,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
|
|||
Notify.progress("Removing selected feeds...", true);
|
||||
|
||||
const query = {
|
||||
op: "pref-feeds", method: "remove",
|
||||
op: "Pref_Feeds", method: "remove",
|
||||
ids: sel_rows.toString()
|
||||
};
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ define(["dojo/_base/declare", "dojo/data/ItemFileWriteStore"], function (declare
|
|||
dojo.xhrPost({
|
||||
url: "backend.php",
|
||||
content: {
|
||||
op: "pref-filters", method: "savefilterorder",
|
||||
op: "Pref_Filters", method: "savefilterorder",
|
||||
payload: newFileContentString
|
||||
},
|
||||
error: saveFailedCallback,
|
||||
|
|
|
@ -107,7 +107,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||
let search = "";
|
||||
if (user_search) { search = user_search.value; }
|
||||
|
||||
xhr.post("backend.php", { op: "pref-filters", search: search }, (reply) => {
|
||||
xhr.post("backend.php", { op: "Pref_Filters", search: search }, (reply) => {
|
||||
dijit.byId('filtersTab').attr('content', reply);
|
||||
Notify.close();
|
||||
});
|
||||
|
@ -125,7 +125,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||
resetFilterOrder: function() {
|
||||
Notify.progress("Loading, please wait...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-filters", method: "filtersortreset"}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Filters", method: "filtersortreset"}, () => {
|
||||
this.reload();
|
||||
});
|
||||
},
|
||||
|
@ -140,7 +140,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||
if (confirm(__("Combine selected filters?"))) {
|
||||
Notify.progress("Joining filters...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-filters", method: "join", ids: rows.toString()}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Filters", method: "join", ids: rows.toString()}, () => {
|
||||
this.reload();
|
||||
});
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||
Notify.progress("Removing selected filters...");
|
||||
|
||||
const query = {
|
||||
op: "pref-filters", method: "remove",
|
||||
op: "Pref_Filters", method: "remove",
|
||||
ids: sel_rows.toString()
|
||||
};
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ const Helpers = {
|
|||
alert("No passwords selected.");
|
||||
} else if (confirm(__("Remove selected app passwords?"))) {
|
||||
|
||||
xhr.post("backend.php", {op: "pref-prefs", method: "deleteAppPasswords", "ids[]": rows}, (reply) => {
|
||||
xhr.post("backend.php", {op: "Pref_Prefs", method: "deleteAppPasswords", "ids[]": rows}, (reply) => {
|
||||
this.updateContent(reply);
|
||||
Notify.close();
|
||||
});
|
||||
|
@ -31,7 +31,7 @@ const Helpers = {
|
|||
const title = prompt("Password description:")
|
||||
|
||||
if (title) {
|
||||
xhr.post("backend.php", {op: "pref-prefs", method: "generateAppPassword", title: title}, (reply) => {
|
||||
xhr.post("backend.php", {op: "Pref_Prefs", method: "generateAppPassword", title: title}, (reply) => {
|
||||
this.updateContent(reply);
|
||||
Notify.close();
|
||||
});
|
||||
|
@ -45,7 +45,7 @@ const Helpers = {
|
|||
if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) {
|
||||
Notify.progress("Clearing URLs...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-feeds", method: "clearKeys"}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Feeds", method: "clearKeys"}, () => {
|
||||
Notify.info("Generated URLs cleared.");
|
||||
});
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ const Helpers = {
|
|||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
||||
dojo.disconnect(tmph);
|
||||
|
||||
xhr.json("backend.php", {op: "pref-prefs", method: "previewDigest"}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Prefs", method: "previewDigest"}, (reply) => {
|
||||
dialog.domNode.querySelector('.digest-preview').innerHTML = reply[0];
|
||||
});
|
||||
});
|
||||
|
@ -91,7 +91,7 @@ const Helpers = {
|
|||
},
|
||||
update: function() {
|
||||
xhr.post("backend.php", {
|
||||
op: "pref-system",
|
||||
op: "Pref_System",
|
||||
severity: dijit.byId("severity").attr('value'),
|
||||
page: Helpers.EventLog.log_page
|
||||
}, (reply) => {
|
||||
|
@ -114,7 +114,7 @@ const Helpers = {
|
|||
|
||||
Notify.progress("Loading, please wait...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-system", method: "clearLog"}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_System", method: "clearLog"}, () => {
|
||||
Helpers.EventLog.refresh();
|
||||
});
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ const Helpers = {
|
|||
const new_title = prompt(__("Name for cloned profile:"));
|
||||
|
||||
if (new_title) {
|
||||
xhr.post("backend.php", {op: "pref-prefs", method: "cloneprofile", "new_title": new_title, "old_profile": sel_rows[0]}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Prefs", method: "cloneprofile", "new_title": new_title, "old_profile": sel_rows[0]}, () => {
|
||||
Notify.close();
|
||||
dialog.refresh();
|
||||
});
|
||||
|
@ -153,7 +153,7 @@ const Helpers = {
|
|||
if (confirm(__("Remove selected profiles? Active and default profiles will not be removed."))) {
|
||||
Notify.progress("Removing selected profiles...", true);
|
||||
|
||||
xhr.post("backend.php", {op: "pref-prefs", method: "remprofiles", "ids[]": sel_rows}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Prefs", method: "remprofiles", "ids[]": sel_rows}, () => {
|
||||
Notify.close();
|
||||
dialog.refresh();
|
||||
});
|
||||
|
@ -167,7 +167,7 @@ const Helpers = {
|
|||
if (this.validate()) {
|
||||
Notify.progress("Creating profile...", true);
|
||||
|
||||
const query = {op: "pref-prefs", method: "addprofile", title: dialog.attr('value').newprofile};
|
||||
const query = {op: "Pref_Prefs", method: "addprofile", title: dialog.attr('value').newprofile};
|
||||
|
||||
xhr.post("backend.php", query, () => {
|
||||
Notify.close();
|
||||
|
@ -177,7 +177,7 @@ const Helpers = {
|
|||
}
|
||||
},
|
||||
refresh: function() {
|
||||
xhr.json("backend.php", {op: 'pref-prefs', method: 'getprofiles'}, (reply) => {
|
||||
xhr.json("backend.php", {op: 'Pref_Prefs', method: 'getprofiles'}, (reply) => {
|
||||
dialog.attr('content', `
|
||||
<div dojoType='fox.Toolbar'>
|
||||
<div dojoType='fox.form.DropDownButton'>
|
||||
|
@ -210,7 +210,7 @@ const Helpers = {
|
|||
profile-id='${profile.id}'>${profile.title}
|
||||
<script type='dojo/method' event='onChange' args='value'>
|
||||
xhr.post("backend.php",
|
||||
{op: 'pref-prefs', method: 'saveprofile', value: value, id: this.attr('profile-id')}, () => {
|
||||
{op: 'Pref_Prefs', method: 'saveprofile', value: value, id: this.attr('profile-id')}, () => {
|
||||
//
|
||||
});
|
||||
</script>
|
||||
|
@ -242,7 +242,7 @@ const Helpers = {
|
|||
if (confirm(__("Activate selected profile?"))) {
|
||||
Notify.progress("Loading, please wait...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-prefs", method: "activateprofile", id: sel_rows.toString()}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Prefs", method: "activateprofile", id: sel_rows.toString()}, () => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
@ -280,7 +280,7 @@ const Helpers = {
|
|||
${__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here.")}
|
||||
</div>
|
||||
|
||||
${App.FormFields.hidden_tag('op', 'rpc')}
|
||||
${App.FormFields.hidden_tag('op', 'RPC')}
|
||||
${App.FormFields.hidden_tag('method', 'setpref')}
|
||||
${App.FormFields.hidden_tag('key', 'USER_STYLESHEET')}
|
||||
|
||||
|
@ -312,7 +312,7 @@ const Helpers = {
|
|||
const tmph = dojo.connect(dialog, 'onShow', function () {
|
||||
dojo.disconnect(tmph);
|
||||
|
||||
xhr.json("backend.php", {op: "pref-prefs", method: "customizeCSS"}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Prefs", method: "customizeCSS"}, (reply) => {
|
||||
|
||||
const editor = dijit.getEnclosingWidget(dialog.domNode.querySelector(".user-css-editor"));
|
||||
|
||||
|
@ -327,14 +327,14 @@ const Helpers = {
|
|||
},
|
||||
confirmReset: function() {
|
||||
if (confirm(__("Reset to defaults?"))) {
|
||||
xhr.post("backend.php", {op: "pref-prefs", method: "resetconfig"}, (reply) => {
|
||||
xhr.post("backend.php", {op: "Pref_Prefs", method: "resetconfig"}, (reply) => {
|
||||
Helpers.Prefs.refresh();
|
||||
Notify.info(reply);
|
||||
});
|
||||
}
|
||||
},
|
||||
refresh: function() {
|
||||
xhr.post("backend.php", { op: "pref-prefs" }, (reply) => {
|
||||
xhr.post("backend.php", { op: "Pref_Prefs" }, (reply) => {
|
||||
dijit.byId('prefsTab').attr('content', reply);
|
||||
Notify.close();
|
||||
});
|
||||
|
@ -360,7 +360,7 @@ const Helpers = {
|
|||
this.render_contents();
|
||||
},
|
||||
reload: function() {
|
||||
xhr.json("backend.php", {op: "pref-prefs", method: "getPluginsList"}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Prefs", method: "getPluginsList"}, (reply) => {
|
||||
this._list_of_plugins = reply;
|
||||
this.render_contents();
|
||||
}, (e) => {
|
||||
|
@ -444,7 +444,7 @@ const Helpers = {
|
|||
if (confirm(__("Clear stored data for %s?").replace("%s", name))) {
|
||||
Notify.progress("Loading, please wait...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-prefs", method: "clearPluginData", name: name}, () => {
|
||||
xhr.post("backend.php", {op: "Pref_Prefs", method: "clearPluginData", name: name}, () => {
|
||||
Helpers.Prefs.refresh();
|
||||
});
|
||||
}
|
||||
|
@ -455,7 +455,7 @@ const Helpers = {
|
|||
if (confirm(msg)) {
|
||||
Notify.progress("Loading, please wait...");
|
||||
|
||||
xhr.json("backend.php", {op: "pref-prefs", method: "uninstallPlugin", plugin: plugin}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Prefs", method: "uninstallPlugin", plugin: plugin}, (reply) => {
|
||||
if (reply && reply.status == 1)
|
||||
Helpers.Plugins.reload();
|
||||
else {
|
||||
|
@ -504,7 +504,7 @@ const Helpers = {
|
|||
|
||||
const container = install_dialog.domNode.querySelector(".contents");
|
||||
|
||||
xhr.json("backend.php", {op: "pref-prefs", method: "installPlugin", plugin: plugin}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Prefs", method: "installPlugin", plugin: plugin}, (reply) => {
|
||||
if (!reply) {
|
||||
container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`;
|
||||
} else {
|
||||
|
@ -603,7 +603,7 @@ const Helpers = {
|
|||
const container = dialog.domNode.querySelector(".contents");
|
||||
container.innerHTML = `<li class='text-center'>${__("Looking for plugins...")}</li>`;
|
||||
|
||||
xhr.json("backend.php", {op: "pref-prefs", method: "getAvailablePlugins"}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Prefs", method: "getAvailablePlugins"}, (reply) => {
|
||||
dialog.entries = reply;
|
||||
dialog.render_contents();
|
||||
});
|
||||
|
@ -656,7 +656,7 @@ const Helpers = {
|
|||
container.innerHTML = `<li class='text-center'>${__("Updating, please wait...")}</li>`;
|
||||
let enable_update_btn = false;
|
||||
|
||||
xhr.json("backend.php", {op: "pref-prefs", method: "updateLocalPlugins", plugins: dialog.plugins_to_update.join(",")}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Prefs", method: "updateLocalPlugins", plugins: dialog.plugins_to_update.join(",")}, (reply) => {
|
||||
|
||||
if (!reply) {
|
||||
container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`;
|
||||
|
@ -717,7 +717,7 @@ const Helpers = {
|
|||
|
||||
//container.innerHTML = `<li class='text-center'>${__("Checking: %s...").replace("%s", name)}</li>`;
|
||||
|
||||
xhr.json("backend.php", {op: "pref-prefs", method: "checkForPluginUpdates", name: name}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Prefs", method: "checkForPluginUpdates", name: name}, (reply) => {
|
||||
|
||||
if (!reply) {
|
||||
container.innerHTML += `<li class='text-error'>${__("%s: Operation failed: check event log.").replace("%s", name)}</li>`;
|
||||
|
@ -834,7 +834,7 @@ const Helpers = {
|
|||
},
|
||||
export: function() {
|
||||
console.log("export");
|
||||
window.open("backend.php?op=opml&method=export&" + dojo.formToQuery("opmlExportForm"));
|
||||
window.open("backend.php?op=OPML&method=export&" + dojo.formToQuery("opmlExportForm"));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -55,13 +55,13 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
|
|||
return rv;
|
||||
},
|
||||
reload: function() {
|
||||
xhr.post("backend.php", { op: "pref-labels" }, (reply) => {
|
||||
xhr.post("backend.php", { op: "Pref_Labels" }, (reply) => {
|
||||
dijit.byId('labelsTab').attr('content', reply);
|
||||
Notify.close();
|
||||
});
|
||||
},
|
||||
editLabel: function(id) {
|
||||
xhr.json("backend.php", {op: "pref-labels", method: "edit", id: id}, (reply) => {
|
||||
xhr.json("backend.php", {op: "Pref_Labels", method: "edit", id: id}, (reply) => {
|
||||
|
||||
const fg_color = reply['fg_color'];
|
||||
const bg_color = reply['bg_color'] ? reply['bg_color'] : '#fff7d5';
|
||||
|
@ -91,7 +91,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
|
|||
}
|
||||
|
||||
const query = {
|
||||
op: "pref-labels", method: "colorset", kind: kind,
|
||||
op: "Pref_Labels", method: "colorset", kind: kind,
|
||||
ids: id, fg: fg, bg: bg, color: color
|
||||
};
|
||||
|
||||
|
@ -131,7 +131,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
|
|||
</section>
|
||||
|
||||
${App.FormFields.hidden_tag('id', id)}
|
||||
${App.FormFields.hidden_tag('op', 'pref-labels')}
|
||||
${App.FormFields.hidden_tag('op', 'Pref_Labels')}
|
||||
${App.FormFields.hidden_tag('method', 'save')}
|
||||
|
||||
${App.FormFields.hidden_tag('fg_color', fg_color, {}, 'labelEdit_fgColor')}
|
||||
|
@ -189,7 +189,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
|
|||
if (confirm(__("Reset selected labels to default colors?"))) {
|
||||
|
||||
const query = {
|
||||
op: "pref-labels", method: "colorreset",
|
||||
op: "Pref_Labels", method: "colorreset",
|
||||
ids: labels.toString()
|
||||
};
|
||||
|
||||
|
@ -210,7 +210,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
|
|||
Notify.progress("Removing selected labels...");
|
||||
|
||||
const query = {
|
||||
op: "pref-labels", method: "remove",
|
||||
op: "Pref_Labels", method: "remove",
|
||||
ids: sel_rows.toString()
|
||||
};
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const Users = {
|
|||
const user_search = App.byId("user_search");
|
||||
const search = user_search ? user_search.value : "";
|
||||
|
||||
xhr.post("backend.php", { op: "pref-users", sort: sort, search: search }, (reply) => {
|
||||
xhr.post("backend.php", { op: "Pref_Users", sort: sort, search: search }, (reply) => {
|
||||
dijit.byId('usersTab').attr('content', reply);
|
||||
Notify.close();
|
||||
resolve();
|
||||
|
@ -21,7 +21,7 @@ const Users = {
|
|||
if (login) {
|
||||
Notify.progress("Adding user...");
|
||||
|
||||
xhr.post("backend.php", {op: "pref-users", method: "add", login: login}, (reply) => {
|
||||
xhr.post("backend.php", {op: "Pref_Users", method: "add", login: login}, (reply) => {
|
||||
Users.reload().then(() => {
|
||||
Notify.info(reply);
|
||||
})
|
||||
|
@ -30,7 +30,7 @@ const Users = {
|
|||
}
|
||||
},
|
||||
edit: function(id) {
|
||||
xhr.json('backend.php', {op: 'pref-users', method: 'edit', id: id}, (reply) => {
|
||||
xhr.json('backend.php', {op: 'Pref_Users', method: 'edit', id: id}, (reply) => {
|
||||
const user = reply.user;
|
||||
const admin_disabled = (user.id == 1);
|
||||
|
||||
|
@ -53,7 +53,7 @@ const Users = {
|
|||
<form onsubmit='return false'>
|
||||
|
||||
${App.FormFields.hidden_tag('id', user.id.toString())}
|
||||
${App.FormFields.hidden_tag('op', 'pref-users')}
|
||||
${App.FormFields.hidden_tag('op', 'Pref_Users')}
|
||||
${App.FormFields.hidden_tag('method', 'editSave')}
|
||||
|
||||
<div dojoType="dijit.layout.TabContainer" style="height : 400px">
|
||||
|
@ -104,7 +104,7 @@ const Users = {
|
|||
<div dojoType="dijit.layout.ContentPane" title="${__('User details')}">
|
||||
<script type='dojo/method' event='onShow' args='evt'>
|
||||
if (this.domNode.querySelector('.loading')) {
|
||||
xhr.post("backend.php", {op: 'pref-users', method: 'userdetails', id: ${user.id}}, (reply) => {
|
||||
xhr.post("backend.php", {op: 'Pref_Users', method: 'userdetails', id: ${user.id}}, (reply) => {
|
||||
this.attr('content', reply);
|
||||
});
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ const Users = {
|
|||
|
||||
const id = rows[0];
|
||||
|
||||
xhr.post("backend.php", {op: "pref-users", method: "resetPass", id: id}, (reply) => {
|
||||
xhr.post("backend.php", {op: "Pref_Users", method: "resetPass", id: id}, (reply) => {
|
||||
Notify.close();
|
||||
Notify.info(reply, true);
|
||||
});
|
||||
|
@ -162,7 +162,7 @@ const Users = {
|
|||
Notify.progress("Removing selected users...");
|
||||
|
||||
const query = {
|
||||
op: "pref-users", method: "remove",
|
||||
op: "Pref_Users", method: "remove",
|
||||
ids: sel_rows.toString()
|
||||
};
|
||||
|
||||
|
|
22
phpstan.neon
22
phpstan.neon
|
@ -1,5 +1,6 @@
|
|||
parameters:
|
||||
level: 6
|
||||
tmpDir: .phpstan-tmp
|
||||
parallel:
|
||||
maximumNumberOfProcesses: 4
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
|
@ -13,19 +14,20 @@ parameters:
|
|||
- plugins/*/vendor/*
|
||||
- plugins.local/*/vendor/*
|
||||
excludePaths:
|
||||
- node_modules/*
|
||||
- vendor/**/tests/*
|
||||
- vendor/**/test/*
|
||||
- vendor/sebastian/*
|
||||
- lib/dojo-src/*
|
||||
- lib/**/tests/*
|
||||
- lib/**/test/*
|
||||
- plugins/**/tests/*
|
||||
- plugins/**/Test/*
|
||||
- plugins.local/**/tests/*
|
||||
- plugins/**/test/*
|
||||
- lib/**/tests/*
|
||||
- lib/dojo-src/*
|
||||
- node_modules/*
|
||||
- plugins.local/**/test/*
|
||||
- plugins.local/**/tests/*
|
||||
- plugins.local/*/vendor/intervention/*
|
||||
- plugins.local/*/vendor/psr/log/*
|
||||
- plugins.local/cache_s3/vendor/*
|
||||
- plugins/**/test/*
|
||||
- plugins/**/Test/*
|
||||
- plugins/**/tests/*
|
||||
- plugins/*/vendor/intervention/*
|
||||
- plugins/*/vendor/psr/log/*
|
||||
- vendor/**/*
|
||||
paths:
|
||||
- .
|
||||
|
|
|
@ -10,9 +10,10 @@ class Af_Comics_Cad extends Af_ComicFilter {
|
|||
if (strpos($article["title"], "News:") === false) {
|
||||
$doc = new DOMDocument();
|
||||
|
||||
$res = UrlHelper::fetch($article["link"], false, false, false,
|
||||
false, false, 0,
|
||||
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");
|
||||
$res = UrlHelper::fetch([
|
||||
'url' => $article['link'],
|
||||
'useragent' => 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0',
|
||||
]);
|
||||
|
||||
if (!$res && UrlHelper::$fetch_last_error_content)
|
||||
$res = UrlHelper::$fetch_last_error_content;
|
||||
|
|
|
@ -11,9 +11,10 @@ class Af_Comics_ComicClass extends Af_ComicFilter {
|
|||
// lol at people who block clients by user agent
|
||||
// oh noes my ad revenue Q_Q
|
||||
|
||||
$res = UrlHelper::fetch($article["link"], false, false, false,
|
||||
false, false, 0,
|
||||
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)");
|
||||
$res = UrlHelper::fetch([
|
||||
'url' => $article['link'],
|
||||
'useragent' => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)',
|
||||
]);
|
||||
|
||||
$doc = new DOMDocument();
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue