Compare commits
500 Commits
app-rootle
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a95f0a4844 | ||
|
|
2cfb58cf8c | ||
|
|
a7d5566aa9 | ||
|
|
14ac789ddc | ||
|
|
d89e825c8a | ||
|
|
7c69ee73e1 | ||
|
|
f23fff932a | ||
|
|
958d25b831 | ||
|
|
d5c3d75ff2 | ||
|
|
b49de6e92c | ||
|
|
ceab15254b | ||
|
|
a4eed151b7 | ||
|
|
18f59793e0 | ||
|
|
e0b116f904 | ||
|
|
ec367b23f4 | ||
|
|
eb05374f24 | ||
|
|
0b50b0f1f0 | ||
|
|
63464f3729 | ||
|
|
a135edcfb1 | ||
|
|
2878ae815f | ||
|
|
46ebef7ebf | ||
|
|
6e8a188e4a | ||
|
|
435358265c | ||
|
|
cb17b3b95e | ||
|
|
c3fbc81e19 | ||
|
|
0bc55e8366 | ||
|
|
b6fd27c756 | ||
|
|
fc95bae2a6 | ||
|
|
c3f3eb8387 | ||
|
|
3d8c54877f | ||
|
|
a32720615f | ||
|
|
4f2423198a | ||
|
|
664f37ae71 | ||
|
|
591ee81ad3 | ||
|
|
4583ae8dc3 | ||
|
|
d7a91de140 | ||
|
|
5e99eb41ec | ||
|
|
c67b943aa8 | ||
|
|
7c79b771e1 | ||
|
|
b369bf8107 | ||
|
|
437f8515ad | ||
|
|
35aa534c71 | ||
|
|
98779acdc5 | ||
|
|
139632c417 | ||
|
|
36644365c8 | ||
|
|
be3ee920b1 | ||
|
|
c914d0710f | ||
|
|
17bd835530 | ||
|
|
17c6d7af8d | ||
|
|
6c0bcd90ed | ||
|
|
efe6fbd3fa | ||
|
|
98dbf49733 | ||
|
|
ecef0ae951 | ||
|
|
fd5ce90efe | ||
|
|
e5c5a1bf42 | ||
|
|
9aafc7bb8d | ||
|
|
57cd48d9f7 | ||
|
|
9982871ac1 | ||
|
|
2d12ced897 | ||
|
|
618cb5bf78 | ||
|
|
f7fc00326e | ||
|
|
31d5887831 | ||
|
|
0669995c47 | ||
|
|
25f416719b | ||
|
|
e8db412dfc | ||
|
|
c60b4a6a69 | ||
|
|
a627f40477 | ||
|
|
1cd05e5b61 | ||
|
|
20843a9d15 | ||
|
|
aff4e3e840 | ||
|
|
13e1d674ee | ||
|
|
b803f85ec2 | ||
|
|
c6624d06a8 | ||
|
|
9de87345c2 | ||
|
|
4758c5f0bf | ||
|
|
d84260b59d | ||
|
|
2ec0aa7cad | ||
|
|
91ac7c3f1f | ||
|
|
6fc1ba172c | ||
|
|
75b035b93c | ||
|
|
9fd54db30d | ||
|
|
906063b1d1 | ||
|
|
19fc3bff21 | ||
|
|
0d0745da44 | ||
|
|
8eb340c3ca | ||
|
|
851ddf4bbe | ||
|
|
c7aa33fe8a | ||
|
|
aeb0d42f89 | ||
|
|
bc312b1205 | ||
|
|
8b07dc8453 | ||
|
|
524bdeb419 | ||
|
|
bb39b34d72 | ||
|
|
f62aaa307b | ||
|
|
dea3f2dcb2 | ||
|
|
d9e2cd44ce | ||
|
|
d3599707ac | ||
|
|
5b99ccf662 | ||
|
|
da4b886f08 | ||
|
|
0cd788220d | ||
|
|
46e05583a9 | ||
|
|
dea41c6a3d | ||
|
|
b989b1cd5b | ||
|
|
05a90d4ce1 | ||
|
|
775a6066f1 | ||
|
|
ec0a19c5a6 | ||
|
|
9b38a57570 | ||
|
|
b0dc82dc7e | ||
|
|
79e0d6ecc2 | ||
|
|
c1542671c1 | ||
|
|
0034cd69f8 | ||
|
|
7b4f039651 | ||
|
|
2e62d27b9f | ||
|
|
ee2f556265 | ||
|
|
629535329d | ||
|
|
50eff08fcb | ||
|
|
7c34df1946 | ||
|
|
f99ef00b0a | ||
|
|
7ab3eb8431 | ||
|
|
7ba4b40a63 | ||
|
|
2643440aba | ||
|
|
fb219848b6 | ||
|
|
f57bb8ec24 | ||
|
|
bab03ea516 | ||
|
|
2d56105c93 | ||
|
|
4088636865 | ||
|
|
18f8f55ce5 | ||
|
|
12ef981bfb | ||
|
|
664f832aac | ||
|
|
09c11df764 | ||
|
|
692c7a8949 | ||
|
|
5b0d325733 | ||
|
|
ef1f3cbcef | ||
|
|
4e47a39c2a | ||
|
|
8cfb96ed54 | ||
|
|
f549459a5c | ||
|
|
c3a0968697 | ||
|
|
b6f0724d51 | ||
|
|
1e35eb9b1b | ||
|
|
3d43eecc50 | ||
|
|
2da58d7ff0 | ||
|
|
a1dcd06b3e | ||
|
|
3047b294a6 | ||
|
|
814ab48169 | ||
|
|
446f9dcb23 | ||
|
|
8255f71c2e | ||
|
|
87fb1de91d | ||
|
|
34c7e11d84 | ||
|
|
2095052521 | ||
|
|
30bf1e9dbe | ||
|
|
ca5ef48df2 | ||
|
|
93a41aa282 | ||
|
|
18b67c837c | ||
|
|
e63fca3dec | ||
|
|
227d45687f | ||
|
|
408ab99450 | ||
|
|
516fcfee98 | ||
|
|
166495ebbf | ||
|
|
558327b3b5 | ||
|
|
0c8ec170f9 | ||
|
|
6abd7fdc4c | ||
|
|
0a6b41a3df | ||
|
|
fc6ce314d6 | ||
|
|
a26d12ba3b | ||
|
|
0688e6dadd | ||
|
|
0787af48ff | ||
|
|
c236560b70 | ||
|
|
52267d2639 | ||
|
|
9d4e945386 | ||
|
|
25d8655214 | ||
|
|
3783d987e5 | ||
|
|
a2a4e831cf | ||
|
|
3e5889394a | ||
|
|
eb2ef63c46 | ||
|
|
dba83a639c | ||
|
|
581fbd762d | ||
|
|
b25684a5a6 | ||
|
|
6695a81e3a | ||
|
|
777e60f854 | ||
|
|
9b998b3069 | ||
|
|
fa9da54d91 | ||
|
|
0a3bec7201 | ||
|
|
91ed6e6f62 | ||
|
|
a51d5ac438 | ||
|
|
b29de3eb7c | ||
|
|
819fde7318 | ||
|
|
f91c19b040 | ||
|
|
f21c0aa8f6 | ||
|
|
349d4c5931 | ||
|
|
9c66b9b326 | ||
|
|
80647fa4e8 | ||
|
|
cb6fe8f974 | ||
|
|
45fc831243 | ||
|
|
99caba74a9 | ||
|
|
33047a86ba | ||
|
|
b447cda515 | ||
|
|
2294090778 | ||
|
|
b9b5c378b0 | ||
|
|
df28c71641 | ||
|
|
9ffb096e87 | ||
|
|
5cf787064d | ||
|
|
eab69f8796 | ||
|
|
f4973264d3 | ||
|
|
a164790268 | ||
|
|
ce36b27a0d | ||
|
|
2749c75b72 | ||
|
|
adf71f09a9 | ||
|
|
cdd48bb1fa | ||
|
|
2fa54cc627 | ||
|
|
0acaac7115 | ||
|
|
d859c56636 | ||
|
|
895e29ec26 | ||
|
|
c472f00445 | ||
|
|
868c1cadad | ||
|
|
aa58ab1ce0 | ||
|
|
2e50f96901 | ||
|
|
d0d90e4ec8 | ||
|
|
888bf821d6 | ||
|
|
5f064b4477 | ||
|
|
a931b91099 | ||
|
|
8aac6f2d3d | ||
|
|
191da49ab1 | ||
|
|
ea7e8404cb | ||
|
|
0affed20d2 | ||
|
|
e68069d33e | ||
|
|
b0402aba34 | ||
|
|
b73ab44e21 | ||
|
|
8b3bd37549 | ||
|
|
77e5deb9dd | ||
|
|
e91c49b747 | ||
|
|
9735ff83cc | ||
|
|
ea6f42dc61 | ||
|
|
677cd7453f | ||
|
|
070585ac5e | ||
|
|
f1df08ba20 | ||
|
|
9d7344dfa5 | ||
|
|
2464153ffc | ||
|
|
6d11acc713 | ||
|
|
4cc40ddaa4 | ||
|
|
5263a07f61 | ||
|
|
fc059fc0fc | ||
|
|
d4faf2d369 | ||
|
|
3ee0f331cc | ||
|
|
07eb34529f | ||
|
|
8f9f06e7c0 | ||
|
|
853864794a | ||
|
|
868385442a | ||
|
|
4ce5e6e8e1 | ||
|
|
fecab891fc | ||
|
|
ec12a514a2 | ||
|
|
5eba9fd116 | ||
|
|
f9c0aacf72 | ||
|
|
bb0a136944 | ||
|
|
01159fa6f8 | ||
|
|
4cda1da5c0 | ||
|
|
997c10437e | ||
|
|
55bb464cc9 | ||
|
|
d5d15072e1 | ||
|
|
5256edd484 | ||
|
|
bc0da8edb6 | ||
|
|
3098dc0a16 | ||
|
|
b30f8c93a0 | ||
|
|
dc6ea08ca4 | ||
|
|
247efe3137 | ||
|
|
aeca30cb0c | ||
|
|
a51c1d5176 | ||
|
|
36f60b51d7 | ||
|
|
44b5b33f3d | ||
|
|
a268f52de6 | ||
|
|
6a40940ad6 | ||
|
|
f22e32a26b | ||
|
|
0520ca2226 | ||
|
|
5f70e41118 | ||
|
|
4ae17d0f1c | ||
|
|
4cb8a84df4 | ||
|
|
f097c5ed97 | ||
|
|
1c9fddd757 | ||
|
|
5c2c95a897 | ||
|
|
ae5394f6f9 | ||
|
|
7ad1efed3e | ||
|
|
0961c8bd4c | ||
|
|
f80187e05f | ||
|
|
0e4b8bd653 | ||
|
|
be82663ac9 | ||
|
|
75556e2f3d | ||
|
|
d2ccdaf400 | ||
|
|
f7199a47c2 | ||
|
|
8cf3059951 | ||
|
|
945690fffc | ||
|
|
3c138a71a1 | ||
|
|
54e8ab7e3d | ||
|
|
7e403aae92 | ||
|
|
b154bc7a10 | ||
|
|
60606aaa97 | ||
|
|
561d922e78 | ||
|
|
50e614499b | ||
|
|
f9e8911727 | ||
|
|
44e23469a0 | ||
|
|
e4f1480453 | ||
|
|
008c518d5d | ||
|
|
17b4e98249 | ||
|
|
597971f238 | ||
|
|
f00d9a18f8 | ||
|
|
eedc1460e5 | ||
|
|
25d3ce4ee8 | ||
|
|
aa552ef057 | ||
|
|
58677fc791 | ||
|
|
026d68fc2d | ||
|
|
bb2c4b3801 | ||
|
|
20ba3c67cc | ||
|
|
eaacca5792 | ||
|
|
e1256b06ea | ||
|
|
f70cd0d149 | ||
|
|
7cef3a5ac2 | ||
|
|
7bb6ebb356 | ||
|
|
c4788023a4 | ||
|
|
8df250f2eb | ||
|
|
90942d9ccf | ||
|
|
fdf9a08197 | ||
|
|
ca751c10e1 | ||
|
|
2d041f7d28 | ||
|
|
b4962b670d | ||
|
|
10c488e1d0 | ||
|
|
043162b0eb | ||
|
|
42ea2ebec0 | ||
|
|
8986a3e7ee | ||
|
|
49766ab01f | ||
|
|
c1e6a5ff63 | ||
|
|
4b677f10e4 | ||
|
|
d04fc6f26d | ||
|
|
ee81566f5e | ||
|
|
b8b475cdf6 | ||
|
|
e01dc2e0d8 | ||
|
|
5dcb8db933 | ||
|
|
5d69120056 | ||
|
|
434da183e7 | ||
|
|
6f9429f405 | ||
|
|
4053af899f | ||
|
|
28cb97ddc5 | ||
|
|
1dc0c98c51 | ||
|
|
405cae963b | ||
|
|
d373c1f978 | ||
|
|
3563a517a6 | ||
|
|
1fc4eed6cd | ||
|
|
9983954bf1 | ||
|
|
89b0332d38 | ||
|
|
7e335de7b8 | ||
|
|
532570ca17 | ||
|
|
f8198933b1 | ||
|
|
bfdfea88b9 | ||
|
|
85929b5d67 | ||
|
|
777c2b4c97 | ||
|
|
e0d9ffcbc1 | ||
|
|
b85330096e | ||
|
|
169ff6de34 | ||
|
|
708563acd4 | ||
|
|
a34927d184 | ||
|
|
59b94a9e45 | ||
|
|
d361c1c65d | ||
|
|
7618101e33 | ||
|
|
117d210c8a | ||
|
|
0eb4571c19 | ||
|
|
f1b1320438 | ||
|
|
f0687060d7 | ||
|
|
ffe47821e0 | ||
|
|
a071edaa9d | ||
|
|
5a93056c1c | ||
|
|
e546e73914 | ||
|
|
2eb3c150c2 | ||
|
|
0da9ef81bd | ||
|
|
91496a0d24 | ||
|
|
93e00d5aab | ||
|
|
ebe080dfe4 | ||
|
|
d85cfb5c56 | ||
|
|
b076eb0005 | ||
|
|
83ae3ce619 | ||
|
|
eac64efe53 | ||
|
|
fc89d2e633 | ||
|
|
a147e36a47 | ||
|
|
7b72715678 | ||
|
|
9e12c9d8e9 | ||
|
|
b8dc4794c6 | ||
|
|
52b8e0f562 | ||
|
|
4fed050215 | ||
|
|
472058e06d | ||
|
|
b172ed039b | ||
|
|
09f32efa3b | ||
|
|
eb47047351 | ||
|
|
e990a3c00f | ||
|
|
912d1d6b1b | ||
|
|
d1c0ba5944 | ||
|
|
13804c4beb | ||
|
|
f9b2291c28 | ||
|
|
119c7f13dc | ||
|
|
6f8f1b30d5 | ||
|
|
cfbbb9d714 | ||
|
|
d47e8957de | ||
|
|
662164334f | ||
|
|
62a6191f04 | ||
|
|
18b17cbc83 | ||
|
|
57dd754e07 | ||
|
|
a1bd6cea1b | ||
|
|
333bab90a7 | ||
|
|
1742fb65c5 | ||
|
|
688ab74eb6 | ||
|
|
d5b1258d29 | ||
|
|
899d7d258c | ||
|
|
e473d8ecc5 | ||
|
|
0caf502b79 | ||
|
|
13f32bb62e | ||
|
|
e858fc45fd | ||
|
|
0ef2dd7175 | ||
|
|
3860435cba | ||
|
|
65bb110770 | ||
|
|
133e8ea2a4 | ||
|
|
f6a8facfd4 | ||
|
|
cd2c10f9f7 | ||
|
|
f15db7b961 | ||
|
|
50259a1bc1 | ||
|
|
096f803f2f | ||
|
|
791c9f287c | ||
|
|
efb16289c6 | ||
|
|
30ab6a6046 | ||
|
|
d2f4e123be | ||
|
|
9340f3fd88 | ||
|
|
4dfa7a666a | ||
|
|
04c0f2a9b9 | ||
|
|
ca61521f42 | ||
|
|
3dd71e41a1 | ||
|
|
b045da0e5e | ||
|
|
8c42b3a3bf | ||
|
|
b5777b5a7c | ||
|
|
d85bdcfd78 | ||
|
|
aaeabbc961 | ||
|
|
103bafd90a | ||
|
|
667528d5b9 | ||
|
|
53fee911e6 | ||
|
|
9b0baf9b32 | ||
|
|
43e8864ead | ||
|
|
b089d67e26 | ||
|
|
7892b41234 | ||
|
|
aad69b7ca6 | ||
|
|
abcd0e8ba2 | ||
|
|
1ce1aee40f | ||
|
|
6bc9097b0f | ||
|
|
d4636716fb | ||
|
|
81ccfed4b4 | ||
|
|
4dc0e8cd29 | ||
|
|
417065b8f5 | ||
|
|
c2d0c5a5c9 | ||
|
|
648024eb2e | ||
|
|
7be6484fee | ||
|
|
1fc19d6e50 | ||
|
|
64d9a77fde | ||
|
|
c23b76eb72 | ||
|
|
ce5a96cb30 | ||
|
|
bbc28e626a | ||
|
|
31ca090c63 | ||
|
|
f28df34fec | ||
|
|
987936f57a | ||
|
|
9c1fb45d73 | ||
|
|
64a36970d6 | ||
|
|
1e14fc0fd9 | ||
|
|
486c92240a | ||
|
|
4bdd926a1c | ||
|
|
154abc61a0 | ||
|
|
394d606fe9 | ||
|
|
859ce4d7f6 | ||
|
|
2dda9f9ab5 | ||
|
|
76b9cd8274 | ||
|
|
5a200755b8 | ||
|
|
dca2ae60a1 | ||
|
|
a784305cc7 | ||
|
|
e4c57769e0 | ||
|
|
6273e26ea4 | ||
|
|
42ebdb027e | ||
|
|
d8718b7574 | ||
|
|
b12b2afcc1 | ||
|
|
eb841b761c | ||
|
|
710e42cfd8 | ||
|
|
68da94cc36 | ||
|
|
dba6a39d2a | ||
|
|
9f0eb4d7fc | ||
|
|
842e9af4cf | ||
|
|
142ca20cb0 | ||
|
|
5ea96397c0 | ||
|
|
a58a0cd888 | ||
|
|
468f464b48 | ||
|
|
7fafad2ac2 | ||
|
|
124c4e2542 | ||
|
|
946337fd5d | ||
|
|
df489df309 | ||
|
|
2ea888fdc6 | ||
|
|
df33ddaea1 | ||
|
|
7e0f5f295c | ||
|
|
e1bac5d855 | ||
|
|
884fd92f13 | ||
|
|
8fcc68baf5 | ||
|
|
b3489fa2a7 | ||
|
|
4ec871eee4 | ||
|
|
e11bc1cd1c |
@@ -1,6 +1,6 @@
|
|||||||
ARG PROXY_REGISTRY
|
ARG PROXY_REGISTRY
|
||||||
|
|
||||||
FROM ${PROXY_REGISTRY}alpine:3.20
|
FROM ${PROXY_REGISTRY}alpine:3.22
|
||||||
EXPOSE 9000/tcp
|
EXPOSE 9000/tcp
|
||||||
|
|
||||||
ARG ALPINE_MIRROR
|
ARG ALPINE_MIRROR
|
||||||
@@ -8,15 +8,22 @@ ARG ALPINE_MIRROR
|
|||||||
ENV SCRIPT_ROOT=/opt/tt-rss
|
ENV SCRIPT_ROOT=/opt/tt-rss
|
||||||
ENV SRC_DIR=/src/tt-rss/
|
ENV SRC_DIR=/src/tt-rss/
|
||||||
|
|
||||||
|
# Normally there's no need to change this, should point to a volume shared with nginx container
|
||||||
|
ENV APP_INSTALL_BASE_DIR=/var/www/html
|
||||||
|
|
||||||
|
# Used to centralize the PHP version suffix for packages and paths
|
||||||
|
ENV PHP_SUFFIX=84
|
||||||
|
|
||||||
RUN [ ! -z ${ALPINE_MIRROR} ] && \
|
RUN [ ! -z ${ALPINE_MIRROR} ] && \
|
||||||
sed -i.bak "s#dl-cdn.alpinelinux.org#${ALPINE_MIRROR}#" /etc/apk/repositories ; \
|
sed -i.bak "s#dl-cdn.alpinelinux.org#${ALPINE_MIRROR}#" /etc/apk/repositories ; \
|
||||||
apk add --no-cache dcron php83 php83-fpm php83-phar php83-sockets php83-pecl-apcu \
|
apk add --no-cache ca-certificates dcron git postgresql-client rsync sudo tzdata \
|
||||||
php83-pdo php83-gd php83-pgsql php83-pdo_pgsql php83-xmlwriter php83-opcache \
|
php${PHP_SUFFIX} \
|
||||||
php83-mbstring php83-intl php83-xml php83-curl php83-simplexml \
|
$(for p in ctype curl dom exif fileinfo fpm gd iconv intl json mbstring opcache \
|
||||||
php83-session php83-tokenizer php83-dom php83-fileinfo php83-ctype \
|
openssl pcntl pdo pdo_pgsql pecl-apcu pecl-xdebug phar posix session simplexml sockets sodium tokenizer xml xmlwriter zip; do \
|
||||||
php83-json php83-iconv php83-pcntl php83-posix php83-zip php83-exif \
|
php_pkgs="$php_pkgs php${PHP_SUFFIX}-$p"; \
|
||||||
php83-openssl git postgresql-client sudo php83-pecl-xdebug rsync tzdata && \
|
done; \
|
||||||
sed -i 's/\(memory_limit =\) 128M/\1 256M/' /etc/php83/php.ini && \
|
echo $php_pkgs) && \
|
||||||
|
sed -i 's/\(memory_limit =\) 128M/\1 256M/' /etc/php${PHP_SUFFIX}/php.ini && \
|
||||||
sed -i -e 's/^listen = 127.0.0.1:9000/listen = 9000/' \
|
sed -i -e 's/^listen = 127.0.0.1:9000/listen = 9000/' \
|
||||||
-e 's/;\(clear_env\) = .*/\1 = no/i' \
|
-e 's/;\(clear_env\) = .*/\1 = no/i' \
|
||||||
-e 's/;\(pm.status_path = \/status\)/\1/i' \
|
-e 's/;\(pm.status_path = \/status\)/\1/i' \
|
||||||
@@ -24,8 +31,8 @@ RUN [ ! -z ${ALPINE_MIRROR} ] && \
|
|||||||
-e 's/^\(user\|group\) = .*/\1 = app/i' \
|
-e 's/^\(user\|group\) = .*/\1 = app/i' \
|
||||||
-e 's/;\(php_admin_value\[error_log\]\) = .*/\1 = \/tmp\/error.log/' \
|
-e 's/;\(php_admin_value\[error_log\]\) = .*/\1 = \/tmp\/error.log/' \
|
||||||
-e 's/;\(php_admin_flag\[log_errors\]\) = .*/\1 = on/' \
|
-e 's/;\(php_admin_flag\[log_errors\]\) = .*/\1 = on/' \
|
||||||
/etc/php83/php-fpm.d/www.conf && \
|
/etc/php${PHP_SUFFIX}/php-fpm.d/www.conf && \
|
||||||
mkdir -p /var/www ${SCRIPT_ROOT}/config.d
|
mkdir -p /var/www ${SCRIPT_ROOT}/config.d ${SCRIPT_ROOT}/sql/post-init.d
|
||||||
|
|
||||||
ARG CI_COMMIT_BRANCH
|
ARG CI_COMMIT_BRANCH
|
||||||
ENV CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH}
|
ENV CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH}
|
||||||
@@ -40,6 +47,7 @@ ARG CI_COMMIT_SHA
|
|||||||
ENV CI_COMMIT_SHA=${CI_COMMIT_SHA}
|
ENV CI_COMMIT_SHA=${CI_COMMIT_SHA}
|
||||||
|
|
||||||
ADD .docker/app/startup.sh ${SCRIPT_ROOT}
|
ADD .docker/app/startup.sh ${SCRIPT_ROOT}
|
||||||
|
ADD .docker/app/update.sh ${SCRIPT_ROOT}
|
||||||
ADD .docker/app/updater.sh ${SCRIPT_ROOT}
|
ADD .docker/app/updater.sh ${SCRIPT_ROOT}
|
||||||
ADD .docker/app/dcron.sh ${SCRIPT_ROOT}
|
ADD .docker/app/dcron.sh ${SCRIPT_ROOT}
|
||||||
ADD .docker/app/backup.sh /etc/periodic/weekly/backup
|
ADD .docker/app/backup.sh /etc/periodic/weekly/backup
|
||||||
@@ -51,7 +59,7 @@ ADD .docker/app/config.docker.php ${SCRIPT_ROOT}
|
|||||||
|
|
||||||
COPY . ${SRC_DIR}
|
COPY . ${SRC_DIR}
|
||||||
|
|
||||||
ARG ORIGIN_REPO_XACCEL=https://git.tt-rss.org/fox/ttrss-nginx-xaccel.git
|
ARG ORIGIN_REPO_XACCEL=https://github.com/tt-rss/tt-rss-plugin-nginx-xaccel.git
|
||||||
|
|
||||||
RUN git clone --depth=1 ${ORIGIN_REPO_XACCEL} ${SRC_DIR}/plugins.local/nginx_xaccel
|
RUN git clone --depth=1 ${ORIGIN_REPO_XACCEL} ${SRC_DIR}/plugins.local/nginx_xaccel
|
||||||
|
|
||||||
@@ -87,12 +95,10 @@ ENV TTRSS_XDEBUG_ENABLED=""
|
|||||||
ENV TTRSS_XDEBUG_HOST=""
|
ENV TTRSS_XDEBUG_HOST=""
|
||||||
ENV TTRSS_XDEBUG_PORT="9000"
|
ENV TTRSS_XDEBUG_PORT="9000"
|
||||||
|
|
||||||
ENV TTRSS_DB_TYPE="pgsql"
|
|
||||||
ENV TTRSS_DB_HOST="db"
|
ENV TTRSS_DB_HOST="db"
|
||||||
ENV TTRSS_DB_PORT="5432"
|
ENV TTRSS_DB_PORT="5432"
|
||||||
|
|
||||||
ENV TTRSS_MYSQL_CHARSET="UTF8"
|
ENV TTRSS_PHP_EXECUTABLE="/usr/bin/php${PHP_SUFFIX}"
|
||||||
ENV TTRSS_PHP_EXECUTABLE="/usr/bin/php83"
|
|
||||||
ENV TTRSS_PLUGINS="auth_internal, note, nginx_xaccel"
|
ENV TTRSS_PLUGINS="auth_internal, note, nginx_xaccel"
|
||||||
|
|
||||||
CMD ${SCRIPT_ROOT}/startup.sh
|
CMD ${SCRIPT_ROOT}/startup.sh
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
DST_DIR=/backups
|
DST_DIR=/backups
|
||||||
KEEP_DAYS=28
|
KEEP_DAYS=28
|
||||||
APP_ROOT=/var/www/html/tt-rss
|
APP_ROOT=$APP_INSTALL_BASE_DIR/tt-rss
|
||||||
|
|
||||||
if pg_isready -h $TTRSS_DB_HOST -U $TTRSS_DB_USER; then
|
if pg_isready -h $TTRSS_DB_HOST -U $TTRSS_DB_USER -p $TTRSS_DB_PORT; then
|
||||||
DST_FILE=ttrss-backup-$(date +%Y%m%d).sql.gz
|
DST_FILE=ttrss-backup-$(date +%Y%m%d).sql.gz
|
||||||
|
|
||||||
echo backing up tt-rss database to $DST_DIR/$DST_FILE...
|
echo backing up tt-rss database to $DST_DIR/$DST_FILE...
|
||||||
|
|
||||||
export PGPASSWORD=$TTRSS_DB_PASS
|
export PGPASSWORD=$TTRSS_DB_PASS
|
||||||
|
|
||||||
pg_dump --clean -h $TTRSS_DB_HOST -U $TTRSS_DB_USER $TTRSS_DB_NAME | gzip > $DST_DIR/$DST_FILE
|
pg_dump --clean -h $TTRSS_DB_HOST -U $TTRSS_DB_USER $TTRSS_DB_NAME | gzip > $DST_DIR/$DST_FILE
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ if pg_isready -h $TTRSS_DB_HOST -U $TTRSS_DB_USER; then
|
|||||||
echo backing up tt-rss local directories to $DST_DIR/$DST_FILE...
|
echo backing up tt-rss local directories to $DST_DIR/$DST_FILE...
|
||||||
|
|
||||||
tar -cz -f $DST_DIR/$DST_FILE $APP_ROOT/*.local \
|
tar -cz -f $DST_DIR/$DST_FILE $APP_ROOT/*.local \
|
||||||
$APP_ROOT/feed-icons/ \
|
$APP_ROOT/cache/feed-icons/ \
|
||||||
$APP_ROOT/config.php
|
$APP_ROOT/config.php
|
||||||
|
|
||||||
echo cleaning up...
|
echo cleaning up...
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
#!/bin/sh -e
|
#!/bin/sh -e
|
||||||
|
#
|
||||||
|
# this script initializes the working copy on a persistent volume and starts PHP FPM
|
||||||
|
#
|
||||||
|
|
||||||
while ! pg_isready -h $TTRSS_DB_HOST -U $TTRSS_DB_USER; do
|
# TODO this should do a reasonable amount of attempts and terminate with an error
|
||||||
|
while ! pg_isready -h $TTRSS_DB_HOST -U $TTRSS_DB_USER -p $TTRSS_DB_PORT; do
|
||||||
echo waiting until $TTRSS_DB_HOST is ready...
|
echo waiting until $TTRSS_DB_HOST is ready...
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
@@ -11,18 +15,18 @@ unset HTTP_HOST
|
|||||||
|
|
||||||
if ! id app >/dev/null 2>&1; then
|
if ! id app >/dev/null 2>&1; then
|
||||||
addgroup -g $OWNER_GID app
|
addgroup -g $OWNER_GID app
|
||||||
adduser -D -h /var/www/html -G app -u $OWNER_UID app
|
adduser -D -h $APP_INSTALL_BASE_DIR -G app -u $OWNER_UID app
|
||||||
fi
|
fi
|
||||||
|
|
||||||
update-ca-certificates || true
|
update-ca-certificates || true
|
||||||
|
|
||||||
DST_DIR=/var/www/html/tt-rss
|
DST_DIR=$APP_INSTALL_BASE_DIR/tt-rss
|
||||||
|
|
||||||
[ -e $DST_DIR ] && rm -f $DST_DIR/.app_is_ready
|
[ -e $DST_DIR ] && rm -f $DST_DIR/.app_is_ready
|
||||||
|
|
||||||
export PGPASSWORD=$TTRSS_DB_PASS
|
export PGPASSWORD=$TTRSS_DB_PASS
|
||||||
|
|
||||||
[ ! -e /var/www/html/index.php ] && cp ${SCRIPT_ROOT}/index.php /var/www/html
|
[ ! -e $APP_INSTALL_BASE_DIR/index.php ] && cp ${SCRIPT_ROOT}/index.php $APP_INSTALL_BASE_DIR
|
||||||
|
|
||||||
if [ -z $SKIP_RSYNC_ON_STARTUP ]; then
|
if [ -z $SKIP_RSYNC_ON_STARTUP ]; then
|
||||||
if [ ! -d $DST_DIR ]; then
|
if [ ! -d $DST_DIR ]; then
|
||||||
@@ -56,16 +60,21 @@ for d in cache lock feed-icons plugins.local themes.local templates.local cache/
|
|||||||
sudo -u app mkdir -p $DST_DIR/$d
|
sudo -u app mkdir -p $DST_DIR/$d
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# this is some next level bullshit
|
||||||
|
# - https://stackoverflow.com/questions/65622914/why-would-i-get-a-php-pdoexception-complaining-that-it-cant-make-a-postgres-con
|
||||||
|
# - fatal error: could not open certificate file "/root/.postgresql/postgresql.crt": Permission denied
|
||||||
|
chown -R app:app /root # /.postgresql
|
||||||
|
|
||||||
for d in cache lock feed-icons; do
|
for d in cache lock feed-icons; do
|
||||||
chmod 777 $DST_DIR/$d
|
chown -R app:app $DST_DIR/$d
|
||||||
find $DST_DIR/$d -type f -exec chmod 666 {} \;
|
chmod -R u=rwX,g=rX,o=rX $DST_DIR/$d
|
||||||
done
|
done
|
||||||
|
|
||||||
sudo -u app cp ${SCRIPT_ROOT}/config.docker.php $DST_DIR/config.php
|
sudo -u app cp ${SCRIPT_ROOT}/config.docker.php $DST_DIR/config.php
|
||||||
chmod 644 $DST_DIR/config.php
|
chmod 644 $DST_DIR/config.php
|
||||||
|
|
||||||
chown -R $OWNER_UID:$OWNER_GID $DST_DIR \
|
chown -R $OWNER_UID:$OWNER_GID $DST_DIR \
|
||||||
/var/log/php83
|
/var/log/php${PHP_SUFFIX}
|
||||||
|
|
||||||
if [ -z "$TTRSS_NO_STARTUP_PLUGIN_UPDATES" ]; then
|
if [ -z "$TTRSS_NO_STARTUP_PLUGIN_UPDATES" ]; then
|
||||||
echo updating all local plugins...
|
echo updating all local plugins...
|
||||||
@@ -77,24 +86,17 @@ if [ -z "$TTRSS_NO_STARTUP_PLUGIN_UPDATES" ]; then
|
|||||||
cd $PLUGIN && \
|
cd $PLUGIN && \
|
||||||
sudo -u app git config core.filemode false && \
|
sudo -u app git config core.filemode false && \
|
||||||
sudo -u app git config pull.rebase false && \
|
sudo -u app git config pull.rebase false && \
|
||||||
sudo -u app git pull origin master || echo warning: attempt to update plugin $PLUGIN failed.
|
sudo -u app git pull origin main || sudo -u app git pull origin master || echo warning: attempt to update plugin $PLUGIN failed.
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
echo skipping local plugin updates, disabled.
|
echo skipping local plugin updates, disabled.
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PSQL="psql -q -h $TTRSS_DB_HOST -U $TTRSS_DB_USER $TTRSS_DB_NAME"
|
PSQL="psql -q -h $TTRSS_DB_HOST -p $TTRSS_DB_PORT -U $TTRSS_DB_USER $TTRSS_DB_NAME"
|
||||||
|
|
||||||
$PSQL -c "create extension if not exists pg_trgm"
|
$PSQL -c "create extension if not exists pg_trgm"
|
||||||
|
|
||||||
RESTORE_SCHEMA=${SCRIPT_ROOT}/restore-schema.sql.gz
|
|
||||||
|
|
||||||
if [ -r $RESTORE_SCHEMA ]; then
|
|
||||||
$PSQL -c "drop schema public cascade; create schema public;"
|
|
||||||
zcat $RESTORE_SCHEMA | $PSQL
|
|
||||||
fi
|
|
||||||
|
|
||||||
# this was previously generated
|
# this was previously generated
|
||||||
rm -f $DST_DIR/config.php.bak
|
rm -f $DST_DIR/config.php.bak
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ if [ ! -z "${TTRSS_XDEBUG_ENABLED}" ]; then
|
|||||||
fi
|
fi
|
||||||
echo enabling xdebug with the following parameters:
|
echo enabling xdebug with the following parameters:
|
||||||
env | grep TTRSS_XDEBUG
|
env | grep TTRSS_XDEBUG
|
||||||
cat > /etc/php83/conf.d/50_xdebug.ini <<EOF
|
cat > /etc/php${PHP_SUFFIX}/conf.d/50_xdebug.ini <<EOF
|
||||||
zend_extension=xdebug.so
|
zend_extension=xdebug.so
|
||||||
xdebug.mode=debug
|
xdebug.mode=debug
|
||||||
xdebug.start_with_request = yes
|
xdebug.start_with_request = yes
|
||||||
@@ -114,17 +116,17 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \
|
sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \
|
||||||
/etc/php83/php.ini
|
/etc/php${PHP_SUFFIX}/php.ini
|
||||||
|
|
||||||
sed -i.bak "s/^\(pm.max_children\) = \(.*\)/\1 = ${PHP_WORKER_MAX_CHILDREN}/" \
|
sed -i.bak "s/^\(pm.max_children\) = \(.*\)/\1 = ${PHP_WORKER_MAX_CHILDREN}/" \
|
||||||
/etc/php83/php-fpm.d/www.conf
|
/etc/php${PHP_SUFFIX}/php-fpm.d/www.conf
|
||||||
|
|
||||||
sudo -Eu app php83 $DST_DIR/update.php --update-schema=force-yes
|
sudo -Eu app php${PHP_SUFFIX} $DST_DIR/update.php --update-schema=force-yes
|
||||||
|
|
||||||
if [ ! -z "$ADMIN_USER_PASS" ]; then
|
if [ ! -z "$ADMIN_USER_PASS" ]; then
|
||||||
sudo -Eu app php83 $DST_DIR/update.php --user-set-password "admin:$ADMIN_USER_PASS"
|
sudo -Eu app php${PHP_SUFFIX} $DST_DIR/update.php --user-set-password "admin:$ADMIN_USER_PASS"
|
||||||
else
|
else
|
||||||
if sudo -Eu app php83 $DST_DIR/update.php --user-check-password "admin:password"; then
|
if sudo -Eu app php${PHP_SUFFIX} $DST_DIR/update.php --user-check-password "admin:password"; then
|
||||||
RANDOM_PASS=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 16 ; echo '')
|
RANDOM_PASS=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 16 ; echo '')
|
||||||
|
|
||||||
echo "*****************************************************************************"
|
echo "*****************************************************************************"
|
||||||
@@ -132,21 +134,21 @@ else
|
|||||||
echo "* If you want to set it manually, use ADMIN_USER_PASS environment variable. *"
|
echo "* If you want to set it manually, use ADMIN_USER_PASS environment variable. *"
|
||||||
echo "*****************************************************************************"
|
echo "*****************************************************************************"
|
||||||
|
|
||||||
sudo -Eu app php83 $DST_DIR/update.php --user-set-password "admin:$RANDOM_PASS"
|
sudo -Eu app php${PHP_SUFFIX} $DST_DIR/update.php --user-set-password "admin:$RANDOM_PASS"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -z "$ADMIN_USER_ACCESS_LEVEL" ]; then
|
if [ ! -z "$ADMIN_USER_ACCESS_LEVEL" ]; then
|
||||||
sudo -Eu app php83 $DST_DIR/update.php --user-set-access-level "admin:$ADMIN_USER_ACCESS_LEVEL"
|
sudo -Eu app php${PHP_SUFFIX} $DST_DIR/update.php --user-set-access-level "admin:$ADMIN_USER_ACCESS_LEVEL"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -z "$AUTO_CREATE_USER" ]; then
|
if [ ! -z "$AUTO_CREATE_USER" ]; then
|
||||||
sudo -Eu app /bin/sh -c "php83 $DST_DIR/update.php --user-exists $AUTO_CREATE_USER ||
|
sudo -Eu app /bin/sh -c "php${PHP_SUFFIX} $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\""
|
php${PHP_SUFFIX} $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
|
if [ ! -z "$AUTO_CREATE_USER_ENABLE_API" ]; then
|
||||||
# TODO: remove || true later
|
# 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
|
sudo -Eu app /bin/sh -c "php${PHP_SUFFIX} $DST_DIR/update.php --user-enable-api \"$AUTO_CREATE_USER:$AUTO_CREATE_USER_ENABLE_API\"" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fi
|
fi
|
||||||
@@ -158,6 +160,11 @@ rm -f /tmp/error.log && mkfifo /tmp/error.log && chown app:app /tmp/error.log
|
|||||||
unset ADMIN_USER_PASS
|
unset ADMIN_USER_PASS
|
||||||
unset AUTO_CREATE_USER_PASS
|
unset AUTO_CREATE_USER_PASS
|
||||||
|
|
||||||
|
find ${SCRIPT_ROOT}/sql/post-init.d/ -type f -name '*.sql' | while read F; do
|
||||||
|
echo applying SQL patch file: $F
|
||||||
|
$PSQL -f $F
|
||||||
|
done
|
||||||
|
|
||||||
touch $DST_DIR/.app_is_ready
|
touch $DST_DIR/.app_is_ready
|
||||||
|
|
||||||
exec /usr/sbin/php-fpm83 --nodaemonize --force-stderr
|
exec /usr/sbin/php-fpm${PHP_SUFFIX} --nodaemonize --force-stderr
|
||||||
|
|||||||
86
.docker/app/update.sh
Normal file
86
.docker/app/update.sh
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
#
|
||||||
|
# this script kickstarts a minimal working environment and runs update.php, could be used as an entrypoint for a cronjob
|
||||||
|
# which doesn't share a volume with FPM/updater
|
||||||
|
#
|
||||||
|
|
||||||
|
# We don't need those here (HTTP_HOST would cause false SELF_URL_PATH check failures)
|
||||||
|
unset HTTP_PORT
|
||||||
|
unset HTTP_HOST
|
||||||
|
|
||||||
|
if ! id app >/dev/null 2>&1; then
|
||||||
|
addgroup -g $OWNER_GID app
|
||||||
|
adduser -D -h $APP_INSTALL_BASE_DIR -G app -u $OWNER_UID app
|
||||||
|
fi
|
||||||
|
|
||||||
|
update-ca-certificates || true
|
||||||
|
|
||||||
|
DST_DIR=$APP_INSTALL_BASE_DIR/tt-rss
|
||||||
|
|
||||||
|
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 --no-owner \
|
||||||
|
$SRC_DIR/ $DST_DIR/
|
||||||
|
else
|
||||||
|
chown -R $OWNER_UID:$OWNER_GID $DST_DIR
|
||||||
|
|
||||||
|
sudo -u app rsync -a --no-owner --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 --no-owner --delete \
|
||||||
|
$SRC_DIR/plugins.local/nginx_xaccel \
|
||||||
|
$DST_DIR/plugins.local/nginx_xaccel
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "warning: working copy in $DST_DIR won't be updated, make sure you know what you're doing."
|
||||||
|
fi
|
||||||
|
|
||||||
|
for d in cache lock feed-icons plugins.local themes.local templates.local cache/export cache/feeds cache/images cache/upload; do
|
||||||
|
sudo -u app mkdir -p $DST_DIR/$d
|
||||||
|
done
|
||||||
|
|
||||||
|
# this is some next level bullshit
|
||||||
|
# - https://stackoverflow.com/questions/65622914/why-would-i-get-a-php-pdoexception-complaining-that-it-cant-make-a-postgres-con
|
||||||
|
# - fatal error: could not open certificate file "/root/.postgresql/postgresql.crt": Permission denied
|
||||||
|
chown -R app:app /root # /.postgresql
|
||||||
|
|
||||||
|
for d in cache lock feed-icons; do
|
||||||
|
chown -R app:app $DST_DIR/$d
|
||||||
|
chmod -R u=rwX,g=rX,o=rX $DST_DIR/$d
|
||||||
|
done
|
||||||
|
|
||||||
|
sudo -u app cp ${SCRIPT_ROOT}/config.docker.php $DST_DIR/config.php
|
||||||
|
chmod 644 $DST_DIR/config.php
|
||||||
|
|
||||||
|
if [ ! -z "${TTRSS_XDEBUG_ENABLED}" ]; then
|
||||||
|
if [ -z "${TTRSS_XDEBUG_HOST}" ]; then
|
||||||
|
export TTRSS_XDEBUG_HOST=$(ip ro sh 0/0 | cut -d " " -f 3)
|
||||||
|
fi
|
||||||
|
echo enabling xdebug with the following parameters:
|
||||||
|
env | grep TTRSS_XDEBUG
|
||||||
|
cat > /etc/php${PHP_SUFFIX}/conf.d/50_xdebug.ini <<EOF
|
||||||
|
zend_extension=xdebug.so
|
||||||
|
xdebug.mode=debug
|
||||||
|
xdebug.start_with_request = yes
|
||||||
|
xdebug.client_port = ${TTRSS_XDEBUG_PORT}
|
||||||
|
xdebug.client_host = ${TTRSS_XDEBUG_HOST}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \
|
||||||
|
/etc/php${PHP_SUFFIX}/php.ini
|
||||||
|
|
||||||
|
sed -i.bak "s/^\(pm.max_children\) = \(.*\)/\1 = ${PHP_WORKER_MAX_CHILDREN}/" \
|
||||||
|
/etc/php${PHP_SUFFIX}/php-fpm.d/www.conf
|
||||||
|
|
||||||
|
sudo -Eu app php${PHP_SUFFIX} $DST_DIR/update.php "$@"
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
#!/bin/sh -e
|
#!/bin/sh -e
|
||||||
|
#
|
||||||
|
# this scripts waits for startup.sh to finish (implying a shared volume) and runs multiprocess daemon when working copy is available
|
||||||
|
#
|
||||||
|
|
||||||
# We don't need those here (HTTP_HOST would cause false SELF_URL_PATH check failures)
|
# We don't need those here (HTTP_HOST would cause false SELF_URL_PATH check failures)
|
||||||
unset HTTP_PORT
|
unset HTTP_PORT
|
||||||
@@ -12,22 +15,30 @@ sleep 30
|
|||||||
|
|
||||||
if ! id app; then
|
if ! id app; then
|
||||||
addgroup -g $OWNER_GID app
|
addgroup -g $OWNER_GID app
|
||||||
adduser -D -h /var/www/html -G app -u $OWNER_UID app
|
adduser -D -h $APP_INSTALL_BASE_DIR -G app -u $OWNER_UID app
|
||||||
fi
|
fi
|
||||||
|
|
||||||
while ! pg_isready -h $TTRSS_DB_HOST -U $TTRSS_DB_USER; do
|
update-ca-certificates || true
|
||||||
|
|
||||||
|
# TODO this should do a reasonable amount of attempts and terminate with an error
|
||||||
|
while ! pg_isready -h $TTRSS_DB_HOST -U $TTRSS_DB_USER -p $TTRSS_DB_PORT; do
|
||||||
echo waiting until $TTRSS_DB_HOST is ready...
|
echo waiting until $TTRSS_DB_HOST is ready...
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
|
|
||||||
sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \
|
sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \
|
||||||
/etc/php83/php.ini
|
/etc/php${PHP_SUFFIX}/php.ini
|
||||||
|
|
||||||
DST_DIR=/var/www/html/tt-rss
|
DST_DIR=$APP_INSTALL_BASE_DIR/tt-rss
|
||||||
|
|
||||||
while [ ! -s $DST_DIR/config.php -a -e $DST_DIR/.app_is_ready ]; do
|
while [ ! -s $DST_DIR/config.php -a -e $DST_DIR/.app_is_ready ]; do
|
||||||
echo waiting for app container...
|
echo waiting for app container...
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
|
|
||||||
sudo -E -u app /usr/bin/php83 /var/www/html/tt-rss/update_daemon2.php "$@"
|
# this is some next level bullshit
|
||||||
|
# - https://stackoverflow.com/questions/65622914/why-would-i-get-a-php-pdoexception-complaining-that-it-cant-make-a-postgres-con
|
||||||
|
# - fatal error: could not open certificate file "/root/.postgresql/postgresql.crt": Permission denied
|
||||||
|
chown -R app:app /root # /.postgresql
|
||||||
|
|
||||||
|
sudo -E -u app "${TTRSS_PHP_EXECUTABLE}" $APP_INSTALL_BASE_DIR/tt-rss/update_daemon2.php "$@"
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ ARG PROXY_REGISTRY
|
|||||||
FROM ${PROXY_REGISTRY}nginxinc/nginx-unprivileged:1-alpine
|
FROM ${PROXY_REGISTRY}nginxinc/nginx-unprivileged:1-alpine
|
||||||
|
|
||||||
COPY ./phpdoc /usr/share/nginx/html/ttrss-docs
|
COPY ./phpdoc /usr/share/nginx/html/ttrss-docs
|
||||||
|
COPY .docker/phpdoc/redirects.conf /etc/nginx/conf.d/
|
||||||
|
|||||||
2
.docker/phpdoc/redirects.conf
Normal file
2
.docker/phpdoc/redirects.conf
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
port_in_redirect off;
|
||||||
|
absolute_redirect off;
|
||||||
@@ -8,6 +8,7 @@ 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
|
# By default, nginx will send the php requests to "app" server, but this server
|
||||||
# name can be overridden at runtime by passing an APP_UPSTREAM env var
|
# name can be overridden at runtime by passing an APP_UPSTREAM env var
|
||||||
ENV APP_UPSTREAM=${APP_UPSTREAM:-app}
|
ENV APP_UPSTREAM=${APP_UPSTREAM:-app}
|
||||||
|
ENV APP_FASTCGI_PASS="${APP_FASTCGI_PASS:-\$backend}"
|
||||||
|
|
||||||
# Webroot (defaults to /var/www/html)
|
# Webroot (defaults to /var/www/html)
|
||||||
ENV APP_WEB_ROOT=${APP_WEB_ROOT:-/var/www/html}
|
ENV APP_WEB_ROOT=${APP_WEB_ROOT:-/var/www/html}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ http {
|
|||||||
|
|
||||||
set $backend "${APP_UPSTREAM}:9000";
|
set $backend "${APP_UPSTREAM}:9000";
|
||||||
|
|
||||||
fastcgi_pass $backend;
|
fastcgi_pass ${APP_FASTCGI_PASS};
|
||||||
}
|
}
|
||||||
|
|
||||||
# Allow PATH_INFO for PHP files in plugins.local directories with an /api/ sub directory to allow plugins to leverage when desired
|
# Allow PATH_INFO for PHP files in plugins.local directories with an /api/ sub directory to allow plugins to leverage when desired
|
||||||
@@ -68,9 +68,9 @@ http {
|
|||||||
|
|
||||||
set $backend "${APP_UPSTREAM}:9000";
|
set $backend "${APP_UPSTREAM}:9000";
|
||||||
|
|
||||||
fastcgi_pass $backend;
|
fastcgi_pass ${APP_FASTCGI_PASS};
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
"extends": "eslint:recommended",
|
"extends": "eslint:recommended",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2018
|
"ecmaVersion": 2020
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"accessor-pairs": "error",
|
"accessor-pairs": "error",
|
||||||
|
|||||||
54
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
54
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report a bug
|
||||||
|
labels: [bug]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: What is the problem?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction
|
||||||
|
attributes:
|
||||||
|
label: Reproduction Steps
|
||||||
|
description: (If known) Minimal steps that can reproduce the issue.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: tt-rss Version
|
||||||
|
description: Which version (commit) of tt-rss are you using?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: environment
|
||||||
|
attributes:
|
||||||
|
label: Environment
|
||||||
|
description: |
|
||||||
|
How are you running tt-rss? Include all relevant details (e.g. Docker image, OS, PHP version, etc.).
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: other
|
||||||
|
attributes:
|
||||||
|
label: Other Information
|
||||||
|
description: |
|
||||||
|
Anything else you want to provide (e.g. related issues, suggestions on how to fix, links for context, etc.).
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgments
|
||||||
|
attributes:
|
||||||
|
label: Acknowledge
|
||||||
|
description: Please mark any checkbox that applies (otherwise leave unchecked).
|
||||||
|
options:
|
||||||
|
- label: I'm interested in implementing the fix
|
||||||
|
required: false
|
||||||
20
.github/ISSUE_TEMPLATE/documentation.yml
vendored
Normal file
20
.github/ISSUE_TEMPLATE/documentation.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Documentation Issue
|
||||||
|
description: Report an issue with documentation
|
||||||
|
labels: [documentation]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: issue
|
||||||
|
attributes:
|
||||||
|
label: What is the issue?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgments
|
||||||
|
attributes:
|
||||||
|
label: Acknowledge
|
||||||
|
description: Please mark any checkbox that apply (otherwise leave unchecked).
|
||||||
|
options:
|
||||||
|
- label: I'm interested in implementing the fix
|
||||||
|
required: false
|
||||||
57
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
Normal file
57
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: Enhancement Request
|
||||||
|
description: Request a new enhancement/feature
|
||||||
|
labels: [enhancement]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Short description of the enhancement/feature you are proposing.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: use-case
|
||||||
|
attributes:
|
||||||
|
label: Use Case
|
||||||
|
description: Why do you need this enhancement/feature?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Proposed Solution
|
||||||
|
description: |
|
||||||
|
(If known) How would you propose the enhancement/feature be implemented?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: tt-rss Version
|
||||||
|
description: Which version (commit) of tt-rss are you currently using?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: other
|
||||||
|
attributes:
|
||||||
|
label: Other Information
|
||||||
|
description: |
|
||||||
|
Anything else you want to provide (e.g. related issues, suggestions on how to implement, links for context, etc.).
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgments
|
||||||
|
attributes:
|
||||||
|
label: Acknowledge
|
||||||
|
description: Please mark any checkbox that apply (otherwise leave unchecked).
|
||||||
|
options:
|
||||||
|
- label: I'm interested in implementing the enhancement/feature
|
||||||
|
required: false
|
||||||
|
- label: This enhancement/feature might result in a breaking change
|
||||||
|
required: false
|
||||||
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: composer
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
25
.github/pull_request_template.md
vendored
Normal file
25
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
## Description
|
||||||
|
<!-- Describe your changes in detail -->
|
||||||
|
|
||||||
|
## Motivation and Context
|
||||||
|
<!-- Why is this change required? What problem does it solve? -->
|
||||||
|
<!-- If it fixes an open issue, please link to the issue here. -->
|
||||||
|
|
||||||
|
## How Has This Been Tested?
|
||||||
|
<!-- Please describe in detail how you tested your changes. -->
|
||||||
|
|
||||||
|
## Screenshots (if appropriate)
|
||||||
|
<!-- If screenshots will help in understanding the change, include them here. -->
|
||||||
|
|
||||||
|
## Types of Changes
|
||||||
|
<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Refactoring (non-breaking change)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
<!-- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||||
|
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||||
|
- [ ] My code follows the code style of this project.
|
||||||
|
- [ ] My change requires a change to the documentation.
|
||||||
67
.github/workflows/php-code-quality.yml
vendored
Normal file
67
.github/workflows/php-code-quality.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
name: PHP Code Quality
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.php'
|
||||||
|
- 'phpstan.neon'
|
||||||
|
- 'phpunit.xml'
|
||||||
|
# Allow manual triggering
|
||||||
|
workflow_dispatch:
|
||||||
|
# Allow other workflows (e.g. Publish) to invoke this one.
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
|
||||||
|
env:
|
||||||
|
fail-fast: true
|
||||||
|
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
phpstan:
|
||||||
|
name: PHPStan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.4'
|
||||||
|
coverage: none
|
||||||
|
tools: none
|
||||||
|
|
||||||
|
- name: Run PHPStan
|
||||||
|
run: vendor/bin/phpstan analyze --no-progress
|
||||||
|
|
||||||
|
phpunit:
|
||||||
|
name: PHPUnit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: ${{ matrix.experimental }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
php: ['8.2', '8.3', '8.4']
|
||||||
|
experimental: [false]
|
||||||
|
include:
|
||||||
|
- php: '8.5'
|
||||||
|
experimental: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up PHP ${{ matrix.php }}
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
coverage: none
|
||||||
|
tools: none
|
||||||
|
|
||||||
|
- name: Run PHPUnit
|
||||||
|
run: vendor/bin/phpunit --exclude integration --coverage-filter classes --coverage-filter include
|
||||||
93
.github/workflows/publish.yml
vendored
Normal file
93
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
name: Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- '.**'
|
||||||
|
- 'tests/**'
|
||||||
|
- '*.*-dist'
|
||||||
|
- '*.js'
|
||||||
|
- '*.json'
|
||||||
|
- '*.lock'
|
||||||
|
- '*.md'
|
||||||
|
- '*.neon'
|
||||||
|
- '*.xml'
|
||||||
|
# Allow manual triggering
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-php:
|
||||||
|
uses: ./.github/workflows/php-code-quality.yml
|
||||||
|
|
||||||
|
publish-dockerhub:
|
||||||
|
name: Publish ${{ matrix.image.name }} to Docker Hub
|
||||||
|
needs:
|
||||||
|
- test-php
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
image:
|
||||||
|
- name: app
|
||||||
|
dockerfile: ./.docker/app/Dockerfile
|
||||||
|
repository: supahgreg/tt-rss
|
||||||
|
- name: web-nginx
|
||||||
|
dockerfile: ./.docker/web-nginx/Dockerfile
|
||||||
|
repository: supahgreg/tt-rss-web-nginx
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Get commit timestamp
|
||||||
|
run: echo "COMMIT_TIMESTAMP=$(git show -s --format=%ci HEAD)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Get commit short SHA
|
||||||
|
run: echo "COMMIT_SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ matrix.image.repository }}
|
||||||
|
tags: |
|
||||||
|
# update 'latest'
|
||||||
|
type=raw,value=latest
|
||||||
|
# short SHA with a 'sha-' prefix (e.g. sha-abc123)
|
||||||
|
type=sha
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push to Docker Hub
|
||||||
|
id: push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ${{ matrix.image.dockerfile }}
|
||||||
|
platforms: linux/arm64,linux/amd64
|
||||||
|
# TODO: clean up build arg and environment variable naming.
|
||||||
|
build-args: |
|
||||||
|
CI_COMMIT_BRANCH=${{ github.ref_name }}
|
||||||
|
CI_COMMIT_SHA=${{ github.sha }}
|
||||||
|
CI_COMMIT_SHORT_SHA=${{ env.COMMIT_SHORT_SHA }}
|
||||||
|
CI_COMMIT_TIMESTAMP=${{ env.COMMIT_TIMESTAMP }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
provenance: true
|
||||||
|
sbom: true
|
||||||
|
push: true
|
||||||
118
.gitlab-ci.yml
118
.gitlab-ci.yml
@@ -21,9 +21,6 @@ include:
|
|||||||
- project: 'ci/ci-templates'
|
- project: 'ci/ci-templates'
|
||||||
ref: master
|
ref: master
|
||||||
file: .ci-lint-common.yml
|
file: .ci-lint-common.yml
|
||||||
- project: 'ci/ci-templates'
|
|
||||||
ref: master
|
|
||||||
file: .ci-integration-test.yml
|
|
||||||
- project: 'ci/ci-templates'
|
- project: 'ci/ci-templates'
|
||||||
ref: master
|
ref: master
|
||||||
file: .ci-update-helm-imagetag.yml
|
file: .ci-update-helm-imagetag.yml
|
||||||
@@ -45,15 +42,8 @@ ttrss-fpm-pgsql-static:build:
|
|||||||
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/app/Dockerfile
|
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/app/Dockerfile
|
||||||
IMAGE_TAR: ${IMAGE_TAR_FPM}
|
IMAGE_TAR: ${IMAGE_TAR_FPM}
|
||||||
|
|
||||||
ttrss-fpm-pgsql-static:push-master-commit-only:
|
ttrss-fpm-pgsql-static:push-commit-only-gitlab:
|
||||||
extends: .crane-image-registry-push-master-commit-only
|
extends: .crane-image-registry-push-commit-only-gitlab
|
||||||
variables:
|
|
||||||
IMAGE_TAR: ${IMAGE_TAR_FPM}
|
|
||||||
needs:
|
|
||||||
- job: ttrss-fpm-pgsql-static:build
|
|
||||||
|
|
||||||
ttrss-fpm-pgsql-static:push-branch:
|
|
||||||
extends: .crane-image-registry-push-branch
|
|
||||||
variables:
|
variables:
|
||||||
IMAGE_TAR: ${IMAGE_TAR_FPM}
|
IMAGE_TAR: ${IMAGE_TAR_FPM}
|
||||||
needs:
|
needs:
|
||||||
@@ -65,15 +55,8 @@ ttrss-web-nginx:build:
|
|||||||
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/web-nginx/Dockerfile
|
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/web-nginx/Dockerfile
|
||||||
IMAGE_TAR: ${IMAGE_TAR_WEB}
|
IMAGE_TAR: ${IMAGE_TAR_WEB}
|
||||||
|
|
||||||
ttrss-web-nginx:push-master-commit-only:
|
ttrss-web-nginx:push-commit-only-gitlab:
|
||||||
extends: .crane-image-registry-push-master-commit-only
|
extends: .crane-image-registry-push-commit-only-gitlab
|
||||||
variables:
|
|
||||||
IMAGE_TAR: ${IMAGE_TAR_WEB}
|
|
||||||
needs:
|
|
||||||
- job: ttrss-web-nginx:build
|
|
||||||
|
|
||||||
ttrss-web-nginx:push-branch:
|
|
||||||
extends: .crane-image-registry-push-branch
|
|
||||||
variables:
|
variables:
|
||||||
IMAGE_TAR: ${IMAGE_TAR_WEB}
|
IMAGE_TAR: ${IMAGE_TAR_WEB}
|
||||||
needs:
|
needs:
|
||||||
@@ -85,7 +68,7 @@ phpdoc:build:
|
|||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||||
script:
|
script:
|
||||||
- php83 /phpDocumentor.phar -d classes -d include -t phpdoc --visibility=public
|
- php84 /phpDocumentor.phar -d classes -d include -t phpdoc --visibility=public
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- phpdoc
|
- phpdoc
|
||||||
@@ -105,16 +88,51 @@ phpdoc:publish:
|
|||||||
phpunit-integration:
|
phpunit-integration:
|
||||||
image: ${PHP_IMAGE}
|
image: ${PHP_IMAGE}
|
||||||
variables:
|
variables:
|
||||||
TEST_HELM_REPO: oci://registry.fakecake.org/infra/helm-charts/tt-rss
|
POSTGRES_DB: postgres
|
||||||
extends: .integration-test
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
TTRSS_DB_HOST: db
|
||||||
|
TTRSS_DB_USER: ${POSTGRES_USER}
|
||||||
|
TTRSS_DB_NAME: ${POSTGRES_DB}
|
||||||
|
TTRSS_DB_PASS: ${POSTGRES_PASSWORD}
|
||||||
|
FF_NETWORK_PER_BUILD: "true"
|
||||||
|
APP_WEB_ROOT: /builds/shared-root
|
||||||
|
APP_INSTALL_BASE_DIR: ${APP_WEB_ROOT}
|
||||||
|
APP_BASE: "/tt-rss"
|
||||||
|
APP_FASTCGI_PASS: app:9000 # skip resolver
|
||||||
|
AUTO_CREATE_USER: test
|
||||||
|
AUTO_CREATE_USER_PASS: 'test'
|
||||||
|
AUTO_CREATE_USER_ACCESS_LEVEL: '10'
|
||||||
|
AUTO_CREATE_USER_ENABLE_API: 'true'
|
||||||
|
APP_URL: http://web-nginx/tt-rss
|
||||||
|
API_URL: ${APP_URL}/api/
|
||||||
|
HEALTHCHECK_URL: ${APP_URL}/public.php?op=healthcheck
|
||||||
|
__URLHELPER_ALLOW_LOOPBACK: 'true'
|
||||||
|
services:
|
||||||
|
- &svc_db
|
||||||
|
name: registry.fakecake.org/docker.io/postgres:15-alpine
|
||||||
|
alias: db
|
||||||
|
- &svc_app
|
||||||
|
name: ${CI_REGISTRY}/${CI_PROJECT_PATH}/ttrss-fpm-pgsql-static:${CI_COMMIT_SHORT_SHA}
|
||||||
|
alias: app
|
||||||
|
- &svc_web
|
||||||
|
name: ${CI_REGISTRY}/${CI_PROJECT_PATH}/ttrss-web-nginx:${CI_COMMIT_SHORT_SHA}
|
||||||
|
alias: web-nginx
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
|
needs:
|
||||||
|
- job: ttrss-fpm-pgsql-static:push-commit-only-gitlab
|
||||||
|
- job: ttrss-web-nginx:push-commit-only-gitlab
|
||||||
|
before_script:
|
||||||
|
# wait for everything to start
|
||||||
|
- |
|
||||||
|
for a in `seq 1 15`; do
|
||||||
|
curl -fs ${HEALTHCHECK_URL} && break
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
script:
|
script:
|
||||||
- export K8S_NAMESPACE=$(kubectl get pods -o=custom-columns=NS:.metadata.namespace | tail -1)
|
- cp tests/integration/feed.xml ${APP_WEB_ROOT}/${APP_BASE}/
|
||||||
- export API_URL="http://tt-rss-${CI_COMMIT_SHORT_SHA}-app.$K8S_NAMESPACE.svc.cluster.local/tt-rss/api/"
|
- php84 vendor/bin/phpunit --group integration --do-not-cache-result --log-junit phpunit-report.xml --coverage-cobertura phpunit-coverage.xml --coverage-text --colors=never
|
||||||
- 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:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
@@ -124,22 +142,32 @@ phpunit-integration:
|
|||||||
path: phpunit-coverage.xml
|
path: phpunit-coverage.xml
|
||||||
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
|
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
|
||||||
|
|
||||||
|
phpunit-integration:root-location:
|
||||||
|
variables:
|
||||||
|
APP_WEB_ROOT: /builds/shared-root/tt-rss
|
||||||
|
APP_INSTALL_BASE_DIR: /builds/shared-root
|
||||||
|
APP_BASE: ""
|
||||||
|
APP_URL: http://web-nginx
|
||||||
|
extends: phpunit-integration
|
||||||
|
|
||||||
selenium:
|
selenium:
|
||||||
|
extends: phpunit-integration
|
||||||
image: ${SELENIUM_IMAGE}
|
image: ${SELENIUM_IMAGE}
|
||||||
variables:
|
variables:
|
||||||
TEST_HELM_REPO: oci://registry.fakecake.org/infra/helm-charts/tt-rss
|
SELENIUM_GRID_ENDPOINT: http://selenium:4444/wd/hub
|
||||||
SELENIUM_GRID_ENDPOINT: http://selenium-hub.selenium-grid.svc.cluster.local:4444/wd/hub
|
services:
|
||||||
extends: .integration-test
|
- *svc_db
|
||||||
|
- *svc_app
|
||||||
|
- *svc_web
|
||||||
|
- name: registry.fakecake.org/docker.io/selenium/standalone-chrome:4.32.0-20250515
|
||||||
|
alias: selenium
|
||||||
script:
|
script:
|
||||||
- export K8S_NAMESPACE=$(kubectl get pods -o=custom-columns=NS:.metadata.namespace | tail -1)
|
|
||||||
- |
|
- |
|
||||||
for i in `seq 1 3`; do
|
for i in `seq 1 10`; do
|
||||||
echo attempt $i...
|
echo attempt $i...
|
||||||
python3 tests/integration/selenium_test.py && break
|
python3 tests/integration/selenium_test.py && break
|
||||||
sleep 3
|
sleep 10
|
||||||
done
|
done
|
||||||
needs:
|
|
||||||
- job: phpunit-integration
|
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
@@ -224,3 +252,15 @@ update-prod:
|
|||||||
ACCESS_TOKEN: ${PROD_HELM_TOKEN}
|
ACCESS_TOKEN: ${PROD_HELM_TOKEN}
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $PROD_HELM_TOKEN != null
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $PROD_HELM_TOKEN != null
|
||||||
|
|
||||||
|
# https://about.gitlab.com/blog/how-to-automatically-create-a-new-mr-on-gitlab-with-gitlab-ci/
|
||||||
|
weblate-integration-auto-mr:
|
||||||
|
image: ${INFRA_IMAGE}
|
||||||
|
stage: publish
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "weblate-integration" && $AUTO_MR_TOKEN != null
|
||||||
|
script:
|
||||||
|
- HOST=${CI_PROJECT_URL} CI_PROJECT_ID=${CI_PROJECT_ID}
|
||||||
|
CI_COMMIT_REF_NAME=${CI_COMMIT_REF_NAME}
|
||||||
|
GITLAB_USER_ID=${GITLAB_USER_ID}
|
||||||
|
PRIVATE_TOKEN=${AUTO_MR_TOKEN} ./utils/autoMergeRequest.sh
|
||||||
|
|||||||
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@@ -38,7 +38,7 @@
|
|||||||
"label": "gulp: default",
|
"label": "gulp: default",
|
||||||
"options": {
|
"options": {
|
||||||
"env": {
|
"env": {
|
||||||
"PATH": "${env:PATH}:/usr/lib/sdk/node16/bin/"
|
"PATH": "${workspaceRoot}/node_modules/.bin:$PATH"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,5 @@
|
|||||||
## Contributing code the right way
|
Contributions (code, translations, reporting issues, etc.) are welcome.
|
||||||
|
|
||||||
TLDR: it works *almost* like Github.
|
> [!NOTE]
|
||||||
|
> The original tt-rss project handled translations via Weblate.
|
||||||
Due to spam, new Gitlab users are set to [external](https://docs.gitlab.com/ee/user/admin_area/external_users.html). In order to do anything, you'll need to ask for your account to be promoted. Sorry for the inconvenience.
|
> It's yet to be determined how this project will handle things.
|
||||||
|
|
||||||
1. Register on the [Gitlab](https://gitlab.tt-rss.org);
|
|
||||||
2. Post on the forums asking for your account to be promoted;
|
|
||||||
3. Fork the repository you're interested in;
|
|
||||||
4. Do the needful;
|
|
||||||
6. File a PR against master branch and verify that CI pipeline (especially, PHPStan) passes;
|
|
||||||
|
|
||||||
If you have any other questions, see this [forum thread](https://discourse.tt-rss.org/t/how-to-contribute-code-via-pull-requests-on-git-tt-rss-org/1850).
|
|
||||||
|
|
||||||
Please don't inline patches in forum posts, attach files instead (``.patch`` or ``.diff`` file extensions should work).
|
|
||||||
|
|
||||||
### FAQ
|
|
||||||
|
|
||||||
#### How do I push or pull without SSH?
|
|
||||||
|
|
||||||
You can't use SSH directly because tt-rss Gitlab is behind Cloudflare. You can use HTTPS with personal access tokens instead.
|
|
||||||
|
|
||||||
Create a personal access token in [Gitlab preferences](https://gitlab.tt-rss.org/-/user_settings/personal_access_tokens);
|
|
||||||
|
|
||||||
Optionally, configure Git to transparently work with tt-rss Gitlab repositories using HTTPS:
|
|
||||||
|
|
||||||
```
|
|
||||||
git config --global \
|
|
||||||
--add url."https://gitlab-token:your-personal-access-token@gitlab.tt-rss.org/".insteadOf \
|
|
||||||
"git@gitlab.tt-rss.org:"
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, checkout over HTTPS while adding the token manually:
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://gitlab-token:your-personal-access-token@gitlab.tt-rss.org/tt-rss/tt-rss.git tt-rss
|
|
||||||
```
|
|
||||||
|
|
||||||
That's it.
|
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -1,10 +1,36 @@
|
|||||||
Tiny Tiny RSS
|
Tiny Tiny RSS (tt-rss)
|
||||||
=============
|
======================
|
||||||
|
|
||||||
Web-based news feed aggregator, designed to allow you to read news from
|
Tiny Tiny RSS (tt-rss) is a free, flexible, open-source, web-based news feed (RSS/Atom/other) reader and aggregator.
|
||||||
any location, while feeling as close to a real desktop application as possible.
|
|
||||||
|
|
||||||
http://tt-rss.org
|
## Getting started
|
||||||
|
|
||||||
|
Please refer to [the wiki](https://github.com/tt-rss/tt-rss/wiki).
|
||||||
|
|
||||||
|
## Some notes about this project
|
||||||
|
|
||||||
|
* The original tt-rss project, hosted at https://tt-rss.org/ and its various subdomains, [will be gone after 2025-11-01](https://community.tt-rss.org/t/the-end-of-tt-rss-org/7164).
|
||||||
|
* Massive thanks to fox for creating tt-rss, and maintaining it (and absolutely everything else that went along with it) for so many years.
|
||||||
|
* This project (https://github.com/tt-rss/tt-rss) is a fork of tt-rss as of 2025-10-03, created by one of its long-time contributors (`wn_`/`wn_name` on `tt-rss.org`, `supahgreg` on `github.com`).
|
||||||
|
* The goal is to continue tt-rss development, with an initial focus on replacing `tt-rss.org` references and integrations + getting things working.
|
||||||
|
* Developer note: Due to use of `invalid@email.com` on `supahgreg`'s pre-2025-10-03 commits (which were done on `tt-rss.org`) GitHub incorrectly shows `ivanivanov884`
|
||||||
|
(the GitHub user associated with that e-mail address) as the author instead of `wn_`/`supahgreg`. Apologies for any confusion. `¯\_(ツ)_/¯`
|
||||||
|
* Plugins that were under https://gitlab.tt-rss.org/tt-rss/plugins have been mirrored to `https://github.com/tt-rss/tt-rss-plugin-*`.
|
||||||
|
* Plugin repository names have changed to get a consistent `tt-rss-plugin-*` prefix.
|
||||||
|
* Documentation from https://tt-rss.org has been recreated in https://github.com/tt-rss/tt-rss/wiki .
|
||||||
|
* The repository that held the content for https://tt-rss.org was mirrored to https://github.com/tt-rss/tt-rss-web-static .
|
||||||
|
Some content tweaks were made after mirroring (prior to the wiki being set up), and the repository is now archived.
|
||||||
|
* Docker images (for `linux/amd64` and `linux/arm64`) are being built and published to Docker Hub [via GitHub Actions](https://github.com/tt-rss/tt-rss/actions/workflows/publish.yml).
|
||||||
|
* See https://hub.docker.com/r/supahgreg/tt-rss/ and https://hub.docker.com/r/supahgreg/tt-rss-web-nginx/ , and
|
||||||
|
[the installation guide](https://github.com/tt-rss/tt-rss/wiki/Installation-Guide) for how they can be used.
|
||||||
|
|
||||||
|
## Development and contributing
|
||||||
|
|
||||||
|
* Contributions (code, translations, reporting issues, etc.) are welcome.
|
||||||
|
* Development and issue tracking primarily happens in https://github.com/tt-rss/tt-rss .
|
||||||
|
* Help translate tt-rss into your own language using [Weblate](https://hosted.weblate.org/engage/tt-rss/).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
if (!empty($_SESSION["uid"])) {
|
if (!empty($_SESSION["uid"])) {
|
||||||
if (!Sessions::validate_session()) {
|
if (!Sessions::validate_session()) {
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
|
|
||||||
print json_encode([
|
print json_encode([
|
||||||
"seq" => -1,
|
"seq" => -1,
|
||||||
@@ -55,10 +55,14 @@
|
|||||||
} else /* if (method_exists($handler, 'index')) */ {
|
} else /* if (method_exists($handler, 'index')) */ {
|
||||||
$handler->index($method);
|
$handler->index($method);
|
||||||
}
|
}
|
||||||
$handler->after();
|
// API isn't currently overriding Handler#after()
|
||||||
|
// $handler->after();
|
||||||
}
|
}
|
||||||
|
|
||||||
header("Api-Content-Length: " . ob_get_length());
|
$content_length = ob_get_length();
|
||||||
|
|
||||||
|
header("Api-Content-Length: $content_length");
|
||||||
|
header("Content-Length: $content_length");
|
||||||
|
|
||||||
ob_end_flush();
|
ob_end_flush();
|
||||||
|
|
||||||
|
|||||||
38
backend.php
38
backend.php
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
/* Public calls compatibility shim */
|
/* Public calls compatibility shim */
|
||||||
|
|
||||||
$public_calls = array("globalUpdateFeeds", "rss", "getUnread", "getProfiles", "share");
|
$public_calls = array("rss", "getUnread", "getProfiles", "share");
|
||||||
|
|
||||||
if (array_search($op, $public_calls) !== false) {
|
if (array_search($op, $public_calls) !== false) {
|
||||||
header("Location: public.php?" . $_SERVER['QUERY_STRING']);
|
header("Location: public.php?" . $_SERVER['QUERY_STRING']);
|
||||||
@@ -35,9 +35,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
header("Content-Type: application/json; charset=utf-8");
|
||||||
|
|
||||||
header("Content-Type: text/json; charset=utf-8");
|
|
||||||
|
|
||||||
if (Config::get(Config::SINGLE_USER_MODE)) {
|
if (Config::get(Config::SINGLE_USER_MODE)) {
|
||||||
UserHelper::authenticate("admin", null);
|
UserHelper::authenticate("admin", null);
|
||||||
@@ -45,10 +43,9 @@
|
|||||||
|
|
||||||
if (!empty($_SESSION["uid"])) {
|
if (!empty($_SESSION["uid"])) {
|
||||||
if (!Sessions::validate_session()) {
|
if (!Sessions::validate_session()) {
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
|
|
||||||
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UserHelper::load_user_plugins($_SESSION["uid"]);
|
UserHelper::load_user_plugins($_SESSION["uid"]);
|
||||||
@@ -57,7 +54,6 @@
|
|||||||
if (Config::is_migration_needed()) {
|
if (Config::is_migration_needed()) {
|
||||||
print Errors::to_json(Errors::E_SCHEMA_MISMATCH);
|
print Errors::to_json(Errors::E_SCHEMA_MISMATCH);
|
||||||
|
|
||||||
$span->setAttribute('error', Errors::E_SCHEMA_MISMATCH);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +96,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
// shortcut syntax for plugin methods (?op=plugin--pmethod&...params)
|
// shortcut syntax for plugin methods (?op=plugin--pmethod&...params)
|
||||||
/* if (strpos($op, PluginHost::PUBLIC_METHOD_DELIMITER) !== false) {
|
/* if (str_contains($op, PluginHost::PUBLIC_METHOD_DELIMITER)) {
|
||||||
list ($plugin, $pmethod) = explode(PluginHost::PUBLIC_METHOD_DELIMITER, $op, 2);
|
list ($plugin, $pmethod) = explode(PluginHost::PUBLIC_METHOD_DELIMITER, $op, 2);
|
||||||
|
|
||||||
// TODO: better implementation that won't modify $_REQUEST
|
// TODO: better implementation that won't modify $_REQUEST
|
||||||
@@ -115,17 +111,15 @@
|
|||||||
|
|
||||||
if (class_exists($op) || $override) {
|
if (class_exists($op) || $override) {
|
||||||
|
|
||||||
if (strpos($method, "_") === 0) {
|
if (str_starts_with($method, "_")) {
|
||||||
user_error("Refusing to invoke method $method of handler $op which starts with underscore.", E_USER_WARNING);
|
user_error("Refusing to invoke method $method of handler $op which starts with underscore.", E_USER_WARNING);
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
|
|
||||||
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($override) {
|
if ($override) {
|
||||||
/** @var Plugin|IHandler|ICatchall $handler */
|
|
||||||
$handler = $override;
|
$handler = $override;
|
||||||
} else {
|
} else {
|
||||||
$reflection = new ReflectionClass($op);
|
$reflection = new ReflectionClass($op);
|
||||||
@@ -133,16 +127,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (implements_interface($handler, 'IHandler')) {
|
if (implements_interface($handler, 'IHandler')) {
|
||||||
$span->addEvent("construct/$op");
|
|
||||||
$handler->__construct($_REQUEST);
|
$handler->__construct($_REQUEST);
|
||||||
|
|
||||||
if (validate_csrf($csrf_token) || $handler->csrf_ignore($method)) {
|
if (validate_csrf($csrf_token) || $handler->csrf_ignore($method)) {
|
||||||
|
|
||||||
$span->addEvent("before/$method");
|
|
||||||
$before = $handler->before($method);
|
$before = $handler->before($method);
|
||||||
|
|
||||||
if ($before) {
|
if ($before) {
|
||||||
$span->addEvent("method/$method");
|
|
||||||
if ($method && method_exists($handler, $method)) {
|
if ($method && method_exists($handler, $method)) {
|
||||||
$reflection = new ReflectionMethod($handler, $method);
|
$reflection = new ReflectionMethod($handler, $method);
|
||||||
|
|
||||||
@@ -150,44 +142,38 @@
|
|||||||
$handler->$method();
|
$handler->$method();
|
||||||
} else {
|
} else {
|
||||||
user_error("Refusing to invoke method $method of handler $op which has required parameters.", E_USER_WARNING);
|
user_error("Refusing to invoke method $method of handler $op which has required parameters.", E_USER_WARNING);
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
|
|
||||||
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
|
|
||||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (method_exists($handler, "catchall")) {
|
if (method_exists($handler, "catchall")) {
|
||||||
$handler->catchall($method);
|
$handler->catchall($method);
|
||||||
} else {
|
} else {
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
|
|
||||||
$span->setAttribute('error', Errors::E_UNKNOWN_METHOD);
|
|
||||||
print Errors::to_json(Errors::E_UNKNOWN_METHOD, ["info" => get_class($handler) . "->$method"]);
|
print Errors::to_json(Errors::E_UNKNOWN_METHOD, ["info" => get_class($handler) . "->$method"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->addEvent("after/$method");
|
|
||||||
$handler->after();
|
$handler->after();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
|
|
||||||
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user_error("Refusing to invoke method $method of handler $op with invalid CSRF token.", E_USER_WARNING);
|
user_error("Refusing to invoke method $method of handler $op with invalid CSRF token.", E_USER_WARNING);
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
|
|
||||||
$span->setAttribute('error', Errors::E_UNAUTHORIZED);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
print Errors::to_json(Errors::E_UNKNOWN_METHOD, [ "info" => (isset($handler) ? get_class($handler) : "UNKNOWN:".$op) . "->$method"]);
|
print Errors::to_json(Errors::E_UNKNOWN_METHOD, [ "info" => (isset($handler) ? get_class($handler) : "UNKNOWN:".$op) . "->$method"]);
|
||||||
|
|
||||||
$span->setAttribute('error', Errors::E_UNKNOWN_METHOD);
|
|
||||||
|
|||||||
123
classes/API.php
123
classes/API.php
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
class API extends Handler {
|
class API extends Handler {
|
||||||
|
|
||||||
const API_LEVEL = 21;
|
const API_LEVEL = 23;
|
||||||
|
|
||||||
const STATUS_OK = 0;
|
const STATUS_OK = 0;
|
||||||
const STATUS_ERR = 1;
|
const STATUS_ERR = 1;
|
||||||
@@ -14,8 +14,7 @@ class API extends Handler {
|
|||||||
const E_OPERATION_FAILED = "E_OPERATION_FAILED";
|
const E_OPERATION_FAILED = "E_OPERATION_FAILED";
|
||||||
const E_NOT_FOUND = "E_NOT_FOUND";
|
const E_NOT_FOUND = "E_NOT_FOUND";
|
||||||
|
|
||||||
/** @var int|null */
|
private ?int $seq = null;
|
||||||
private $seq;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int|string, mixed> $reply
|
* @param array<int|string, mixed> $reply
|
||||||
@@ -31,14 +30,14 @@ class API extends Handler {
|
|||||||
|
|
||||||
function before(string $method): bool {
|
function before(string $method): bool {
|
||||||
if (parent::before($method)) {
|
if (parent::before($method)) {
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
|
|
||||||
if (empty($_SESSION["uid"]) && $method != "login" && $method != "isloggedin") {
|
if (empty($_SESSION["uid"]) && $method != "login" && $method != "isloggedin") {
|
||||||
$this->_wrap(self::STATUS_ERR, array("error" => self::E_NOT_LOGGED_IN));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_NOT_LOGGED_IN));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($_SESSION["uid"]) && $method != "logout" && !get_pref(Prefs::ENABLE_API_ACCESS)) {
|
if (!empty($_SESSION["uid"]) && $method != "logout" && !Prefs::get(Prefs::ENABLE_API_ACCESS, $_SESSION["uid"])) {
|
||||||
$this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED));
|
$this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -74,7 +73,7 @@ class API extends Handler {
|
|||||||
if (Config::get(Config::SINGLE_USER_MODE)) $login = "admin";
|
if (Config::get(Config::SINGLE_USER_MODE)) $login = "admin";
|
||||||
|
|
||||||
if ($uid = UserHelper::find_user_by_login($login)) {
|
if ($uid = UserHelper::find_user_by_login($login)) {
|
||||||
if (get_pref(Prefs::ENABLE_API_ACCESS, $uid)) {
|
if (Prefs::get(Prefs::ENABLE_API_ACCESS, $uid)) {
|
||||||
if (UserHelper::authenticate($login, $password, false, Auth_Base::AUTH_SERVICE_API)) {
|
if (UserHelper::authenticate($login, $password, false, Auth_Base::AUTH_SERVICE_API)) {
|
||||||
|
|
||||||
// needed for _get_config()
|
// needed for _get_config()
|
||||||
@@ -176,7 +175,7 @@ class API extends Handler {
|
|||||||
if ($unread || !$unread_only) {
|
if ($unread || !$unread_only) {
|
||||||
array_push($cats, [
|
array_push($cats, [
|
||||||
'id' => $cat_id,
|
'id' => $cat_id,
|
||||||
'title' => Feeds::_get_cat_title($cat_id),
|
'title' => Feeds::_get_cat_title($cat_id, $_SESSION['uid']),
|
||||||
'unread' => (int) $unread,
|
'unread' => (int) $unread,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -235,13 +234,12 @@ class API extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateArticle(): bool {
|
function updateArticle(): bool {
|
||||||
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
|
$article_ids = array_filter(explode(",", clean($_REQUEST["article_ids"] ?? "")));
|
||||||
$mode = (int) clean($_REQUEST["mode"]);
|
$mode = (int) clean($_REQUEST["mode"]);
|
||||||
$data = clean($_REQUEST["data"] ?? "");
|
$data = clean($_REQUEST["data"] ?? "");
|
||||||
$field_raw = (int)clean($_REQUEST["field"]);
|
$field_raw = (int)clean($_REQUEST["field"]);
|
||||||
|
|
||||||
$field = "";
|
$field = "";
|
||||||
$set_to = "";
|
|
||||||
$additional_fields = "";
|
$additional_fields = "";
|
||||||
|
|
||||||
switch ($field_raw) {
|
switch ($field_raw) {
|
||||||
@@ -265,20 +263,17 @@ class API extends Handler {
|
|||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
switch ($mode) {
|
$set_to = match ($mode) {
|
||||||
case 1:
|
0 => 'false',
|
||||||
$set_to = "true";
|
1 => 'true',
|
||||||
break;
|
2 => "NOT $field",
|
||||||
case 0:
|
default => null,
|
||||||
$set_to = "false";
|
};
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
$set_to = "NOT $field";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($field == "note") $set_to = $this->pdo->quote($data);
|
if ($field == 'note')
|
||||||
if ($field == "score") $set_to = (int) $data;
|
$set_to = $this->pdo->quote($data);
|
||||||
|
elseif ($field == 'score')
|
||||||
|
$set_to = (int) $data;
|
||||||
|
|
||||||
if ($field && $set_to && count($article_ids) > 0) {
|
if ($field && $set_to && count($article_ids) > 0) {
|
||||||
|
|
||||||
@@ -289,6 +284,12 @@ class API extends Handler {
|
|||||||
WHERE ref_id IN ($article_qmarks) AND owner_uid = ?");
|
WHERE ref_id IN ($article_qmarks) AND owner_uid = ?");
|
||||||
$sth->execute([...$article_ids, $_SESSION['uid']]);
|
$sth->execute([...$article_ids, $_SESSION['uid']]);
|
||||||
|
|
||||||
|
if ($field == 'marked')
|
||||||
|
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_MARK_TOGGLED, $article_ids);
|
||||||
|
|
||||||
|
if ($field == 'published')
|
||||||
|
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_PUBLISH_TOGGLED, $article_ids);
|
||||||
|
|
||||||
$num_updated = $sth->rowCount();
|
$num_updated = $sth->rowCount();
|
||||||
|
|
||||||
return $this->_wrap(self::STATUS_OK, array("status" => "OK",
|
return $this->_wrap(self::STATUS_OK, array("status" => "OK",
|
||||||
@@ -300,17 +301,16 @@ class API extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getArticle(): bool {
|
function getArticle(): bool {
|
||||||
$article_ids = explode(',', clean($_REQUEST['article_id'] ?? ''));
|
$article_ids = array_filter(explode(',', clean($_REQUEST['article_id'] ?? '')));
|
||||||
$sanitize_content = self::_param_to_bool($_REQUEST['sanitize'] ?? true);
|
$sanitize_content = self::_param_to_bool($_REQUEST['sanitize'] ?? true);
|
||||||
|
|
||||||
// @phpstan-ignore-next-line
|
|
||||||
if (count($article_ids)) {
|
if (count($article_ids)) {
|
||||||
$entries = ORM::for_table('ttrss_entries')
|
$entries = ORM::for_table('ttrss_entries')
|
||||||
->table_alias('e')
|
->table_alias('e')
|
||||||
->select_many('e.id', 'e.guid', 'e.title', 'e.link', 'e.author', 'e.content', 'e.lang', 'e.comments',
|
->select_many('e.id', 'e.guid', 'e.title', 'e.link', 'e.author', 'e.content', 'e.lang', 'e.comments',
|
||||||
'ue.feed_id', 'ue.int_id', 'ue.marked', 'ue.unread', 'ue.published', 'ue.score', 'ue.note')
|
'ue.feed_id', 'ue.int_id', 'ue.marked', 'ue.unread', 'ue.published', 'ue.score', 'ue.note')
|
||||||
->select_many_expr([
|
->select_many_expr([
|
||||||
'updated' => SUBSTRING_FOR_DATE.'(updated,1,16)',
|
'updated' => 'SUBSTRING_FOR_DATE(updated,1,16)',
|
||||||
'feed_title' => '(SELECT title FROM ttrss_feeds WHERE id = ue.feed_id)',
|
'feed_title' => '(SELECT title FROM ttrss_feeds WHERE id = ue.feed_id)',
|
||||||
'site_url' => '(SELECT site_url FROM ttrss_feeds WHERE id = ue.feed_id)',
|
'site_url' => '(SELECT site_url FROM ttrss_feeds WHERE id = ue.feed_id)',
|
||||||
'hide_images' => '(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id)',
|
'hide_images' => '(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id)',
|
||||||
@@ -372,22 +372,19 @@ class API extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @see RPC::_make_init_params()
|
||||||
|
* @see RPC::_make_runtime_info()
|
||||||
* @return array<string, array<string, string>|bool|int|string>
|
* @return array<string, array<string, string>|bool|int|string>
|
||||||
*/
|
*/
|
||||||
private function _get_config(): array {
|
private function _get_config(): array {
|
||||||
$config = [
|
return [
|
||||||
"icons_dir" => Config::get(Config::ICONS_DIR),
|
'custom_sort_types' => $this->_get_custom_sort_types(),
|
||||||
"icons_url" => Config::get(Config::ICONS_URL)
|
'daemon_is_running' => file_is_locked('update_daemon.lock'),
|
||||||
|
'icons_url' => Config::get_self_url() . '/public.php',
|
||||||
|
'num_feeds' => ORM::for_table('ttrss_feeds')
|
||||||
|
->where('owner_uid', $_SESSION['uid'])
|
||||||
|
->count(),
|
||||||
];
|
];
|
||||||
|
|
||||||
$config["daemon_is_running"] = file_is_locked("update_daemon.lock");
|
|
||||||
$config["custom_sort_types"] = $this->_get_custom_sort_types();
|
|
||||||
|
|
||||||
$config["num_feeds"] = ORM::for_table('ttrss_feeds')
|
|
||||||
->where('owner_uid', $_SESSION['uid'])
|
|
||||||
->count();
|
|
||||||
|
|
||||||
return $config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfig(): bool {
|
function getConfig(): bool {
|
||||||
@@ -410,11 +407,13 @@ class API extends Handler {
|
|||||||
$feed_id = clean($_REQUEST["feed_id"]);
|
$feed_id = clean($_REQUEST["feed_id"]);
|
||||||
$is_cat = self::_param_to_bool($_REQUEST["is_cat"] ?? false);
|
$is_cat = self::_param_to_bool($_REQUEST["is_cat"] ?? false);
|
||||||
$mode = clean($_REQUEST["mode"] ?? "");
|
$mode = clean($_REQUEST["mode"] ?? "");
|
||||||
|
$search_query = clean($_REQUEST["search_query"] ?? "");
|
||||||
|
$search_lang = clean($_REQUEST["search_lang"] ?? "");
|
||||||
|
|
||||||
if (!in_array($mode, ["all", "1day", "1week", "2week"]))
|
if (!in_array($mode, ["all", "1day", "1week", "2week"]))
|
||||||
$mode = "all";
|
$mode = "all";
|
||||||
|
|
||||||
Feeds::_catchup($feed_id, $is_cat, $_SESSION["uid"], $mode);
|
Feeds::_catchup($feed_id, $is_cat, $_SESSION["uid"], $mode, [$search_query, $search_lang]);
|
||||||
|
|
||||||
return $this->_wrap(self::STATUS_OK, array("status" => "OK"));
|
return $this->_wrap(self::STATUS_OK, array("status" => "OK"));
|
||||||
}
|
}
|
||||||
@@ -422,7 +421,7 @@ class API extends Handler {
|
|||||||
function getPref(): bool {
|
function getPref(): bool {
|
||||||
$pref_name = clean($_REQUEST["pref_name"]);
|
$pref_name = clean($_REQUEST["pref_name"]);
|
||||||
|
|
||||||
return $this->_wrap(self::STATUS_OK, array("value" => get_pref($pref_name)));
|
return $this->_wrap(self::STATUS_OK, array("value" => Prefs::get($pref_name, $_SESSION["uid"], $_SESSION["profile"] ?? null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLabels(): bool {
|
function getLabels(): bool {
|
||||||
@@ -550,26 +549,22 @@ class API extends Handler {
|
|||||||
|
|
||||||
/* Virtual feeds */
|
/* Virtual feeds */
|
||||||
|
|
||||||
$vfeeds = PluginHost::getInstance()->get_feeds(Feeds::CATEGORY_SPECIAL);
|
foreach (PluginHost::getInstance()->get_feeds(Feeds::CATEGORY_SPECIAL) as $feed) {
|
||||||
|
if (!implements_interface($feed['sender'], 'IVirtualFeed'))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (is_array($vfeeds)) {
|
/** @var IVirtualFeed $feed['sender'] */
|
||||||
foreach ($vfeeds as $feed) {
|
$unread = $feed['sender']->get_unread($feed['id']);
|
||||||
if (!implements_interface($feed['sender'], 'IVirtualFeed'))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
/** @var IVirtualFeed $feed['sender'] */
|
if ($unread || !$unread_only) {
|
||||||
$unread = $feed['sender']->get_unread($feed['id']);
|
$row = [
|
||||||
|
'id' => PluginHost::pfeed_to_feed_id($feed['id']),
|
||||||
|
'title' => $feed['title'],
|
||||||
|
'unread' => $unread,
|
||||||
|
'cat_id' => Feeds::CATEGORY_SPECIAL,
|
||||||
|
];
|
||||||
|
|
||||||
if ($unread || !$unread_only) {
|
array_push($feeds, $row);
|
||||||
$row = [
|
|
||||||
'id' => PluginHost::pfeed_to_feed_id($feed['id']),
|
|
||||||
'title' => $feed['title'],
|
|
||||||
'unread' => $unread,
|
|
||||||
'cat_id' => Feeds::CATEGORY_SPECIAL,
|
|
||||||
];
|
|
||||||
|
|
||||||
array_push($feeds, $row);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,7 +574,7 @@ class API extends Handler {
|
|||||||
$unread = Feeds::_get_counters($i, false, true);
|
$unread = Feeds::_get_counters($i, false, true);
|
||||||
|
|
||||||
if ($unread || !$unread_only) {
|
if ($unread || !$unread_only) {
|
||||||
$title = Feeds::_get_title($i);
|
$title = Feeds::_get_title($i, $_SESSION['uid']);
|
||||||
|
|
||||||
$row = [
|
$row = [
|
||||||
'id' => $i,
|
'id' => $i,
|
||||||
@@ -623,8 +618,8 @@ class API extends Handler {
|
|||||||
|
|
||||||
/* API only: -3 (Feeds::CATEGORY_ALL_EXCEPT_VIRTUAL) All feeds, excluding virtual feeds (e.g. Labels and such) */
|
/* API only: -3 (Feeds::CATEGORY_ALL_EXCEPT_VIRTUAL) All feeds, excluding virtual feeds (e.g. Labels and such) */
|
||||||
$feeds_obj = ORM::for_table('ttrss_feeds')
|
$feeds_obj = ORM::for_table('ttrss_feeds')
|
||||||
->select_many('id', 'feed_url', 'cat_id', 'title', 'order_id')
|
->select_many('id', 'feed_url', 'cat_id', 'title', 'order_id', 'last_error', 'update_interval')
|
||||||
->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
|
->select_expr('SUBSTRING_FOR_DATE(last_updated,1,19)', 'last_updated')
|
||||||
->where('owner_uid', $_SESSION['uid'])
|
->where('owner_uid', $_SESSION['uid'])
|
||||||
->order_by_asc('order_id')
|
->order_by_asc('order_id')
|
||||||
->order_by_asc('title');
|
->order_by_asc('title');
|
||||||
@@ -650,6 +645,8 @@ class API extends Handler {
|
|||||||
'cat_id' => (int) $feed->cat_id,
|
'cat_id' => (int) $feed->cat_id,
|
||||||
'last_updated' => (int) strtotime($feed->last_updated ?? ''),
|
'last_updated' => (int) strtotime($feed->last_updated ?? ''),
|
||||||
'order_id' => (int) $feed->order_id,
|
'order_id' => (int) $feed->order_id,
|
||||||
|
'last_error' => $feed->last_error,
|
||||||
|
'update_interval' => (int) $feed->update_interval,
|
||||||
];
|
];
|
||||||
|
|
||||||
array_push($feeds, $row);
|
array_push($feeds, $row);
|
||||||
@@ -660,10 +657,9 @@ class API extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string|int $feed_id
|
|
||||||
* @return array{0: array<int, array<string, mixed>>, 1: array<string, mixed>} $headlines, $headlines_header
|
* @return array{0: array<int, array<string, mixed>>, 1: array<string, mixed>} $headlines, $headlines_header
|
||||||
*/
|
*/
|
||||||
private static function _api_get_headlines($feed_id, int $limit, int $offset,
|
private static function _api_get_headlines(int|string $feed_id, int $limit, int $offset,
|
||||||
string $filter, bool $is_cat, bool $show_excerpt, bool $show_content, ?string $view_mode, string $order,
|
string $filter, bool $is_cat, bool $show_excerpt, bool $show_content, ?string $view_mode, string $order,
|
||||||
bool $include_attachments, int $since_id, string $search = "", bool $include_nested = false,
|
bool $include_attachments, int $since_id, string $search = "", bool $include_nested = false,
|
||||||
bool $sanitize_content = true, bool $force_update = false, int $excerpt_length = 100, ?int $check_first_id = null,
|
bool $sanitize_content = true, bool $force_update = false, int $excerpt_length = 100, ?int $check_first_id = null,
|
||||||
@@ -674,7 +670,7 @@ class API extends Handler {
|
|||||||
|
|
||||||
$feed = ORM::for_table('ttrss_feeds')
|
$feed = ORM::for_table('ttrss_feeds')
|
||||||
->select_many('id', 'cache_images')
|
->select_many('id', 'cache_images')
|
||||||
->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
|
->select_expr('SUBSTRING_FOR_DATE(last_updated,1,19)', 'last_updated')
|
||||||
->find_one($feed_id);
|
->find_one($feed_id);
|
||||||
|
|
||||||
if ($feed) {
|
if ($feed) {
|
||||||
@@ -696,7 +692,6 @@ class API extends Handler {
|
|||||||
if (!$is_cat && is_numeric($feed_id) && $feed_id < PLUGIN_FEED_BASE_INDEX && $feed_id > LABEL_BASE_INDEX) {
|
if (!$is_cat && is_numeric($feed_id) && $feed_id < PLUGIN_FEED_BASE_INDEX && $feed_id > LABEL_BASE_INDEX) {
|
||||||
$pfeed_id = PluginHost::feed_to_pfeed_id($feed_id);
|
$pfeed_id = PluginHost::feed_to_pfeed_id($feed_id);
|
||||||
|
|
||||||
/** @var IVirtualFeed|false $handler */
|
|
||||||
$handler = PluginHost::getInstance()->get_feed_handler($pfeed_id);
|
$handler = PluginHost::getInstance()->get_feed_handler($pfeed_id);
|
||||||
|
|
||||||
if ($handler) {
|
if ($handler) {
|
||||||
@@ -858,7 +853,7 @@ class API extends Handler {
|
|||||||
|
|
||||||
array_push($headlines, $headline_row);
|
array_push($headlines, $headline_row);
|
||||||
}
|
}
|
||||||
} else if (is_numeric($result) && $result == -1) {
|
} else if ($result == -1) {
|
||||||
$headlines_header['first_id_changed'] = true;
|
$headlines_header['first_id_changed'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,21 +85,21 @@ class Article extends Handler_Protected {
|
|||||||
content = ?, content_hash = ? WHERE id = ?");
|
content = ?, content_hash = ? WHERE id = ?");
|
||||||
$sth->execute([$content, $content_hash, $ref_id]);
|
$sth->execute([$content, $content_hash, $ref_id]);
|
||||||
|
|
||||||
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
$sth = $pdo->prepare("UPDATE ttrss_entries
|
||||||
$sth = $pdo->prepare("UPDATE ttrss_entries
|
SET tsvector_combined = to_tsvector( :ts_content)
|
||||||
SET tsvector_combined = to_tsvector( :ts_content)
|
WHERE id = :id");
|
||||||
WHERE id = :id");
|
$params = [
|
||||||
$params = [
|
":ts_content" => mb_substr(\Soundasleep\Html2Text::convert($content), 0, 900000),
|
||||||
":ts_content" => mb_substr(\Soundasleep\Html2Text::convert($content), 0, 900000),
|
":id" => $ref_id];
|
||||||
":id" => $ref_id];
|
$sth->execute($params);
|
||||||
$sth->execute($params);
|
|
||||||
}
|
|
||||||
|
|
||||||
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET published = true,
|
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET published = true,
|
||||||
last_published = NOW() WHERE
|
last_published = NOW() WHERE
|
||||||
int_id = ? AND owner_uid = ?");
|
int_id = ? AND owner_uid = ?");
|
||||||
$sth->execute([$int_id, $owner_uid]);
|
$sth->execute([$int_id, $owner_uid]);
|
||||||
|
|
||||||
|
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_PUBLISH_TOGGLED, [$ref_id]);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
$sth = $pdo->prepare("INSERT INTO ttrss_user_entries
|
$sth = $pdo->prepare("INSERT INTO ttrss_user_entries
|
||||||
@@ -108,6 +108,8 @@ class Article extends Handler_Protected {
|
|||||||
VALUES
|
VALUES
|
||||||
(?, '', NULL, NULL, ?, true, '', '', NOW(), '', false, NOW())");
|
(?, '', NULL, NULL, ?, true, '', '', NOW(), '', false, NOW())");
|
||||||
$sth->execute([$ref_id, $owner_uid]);
|
$sth->execute([$ref_id, $owner_uid]);
|
||||||
|
|
||||||
|
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_PUBLISH_TOGGLED, [$ref_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($labels) != 0) {
|
if (count($labels) != 0) {
|
||||||
@@ -122,23 +124,20 @@ class Article extends Handler_Protected {
|
|||||||
$sth = $pdo->prepare("INSERT INTO ttrss_entries
|
$sth = $pdo->prepare("INSERT INTO ttrss_entries
|
||||||
(title, guid, link, updated, content, content_hash, date_entered, date_updated)
|
(title, guid, link, updated, content, content_hash, date_entered, date_updated)
|
||||||
VALUES
|
VALUES
|
||||||
(?, ?, ?, NOW(), ?, ?, NOW(), NOW())");
|
(?, ?, ?, NOW(), ?, ?, NOW(), NOW()) RETURNING id");
|
||||||
$sth->execute([$title, $guid, $url, $content, $content_hash]);
|
$sth->execute([$title, $guid, $url, $content, $content_hash]);
|
||||||
|
|
||||||
$sth = $pdo->prepare("SELECT id FROM ttrss_entries WHERE guid = ?");
|
|
||||||
$sth->execute([$guid]);
|
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
$ref_id = $row["id"];
|
$ref_id = $row["id"];
|
||||||
if (Config::get(Config::DB_TYPE) == "pgsql"){
|
|
||||||
$sth = $pdo->prepare("UPDATE ttrss_entries
|
$sth = $pdo->prepare("UPDATE ttrss_entries
|
||||||
SET tsvector_combined = to_tsvector( :ts_content)
|
SET tsvector_combined = to_tsvector( :ts_content)
|
||||||
WHERE id = :id");
|
WHERE id = :id");
|
||||||
$params = [
|
$params = [
|
||||||
":ts_content" => mb_substr(\Soundasleep\Html2Text::convert($content), 0, 900000),
|
":ts_content" => mb_substr(\Soundasleep\Html2Text::convert($content), 0, 900000),
|
||||||
":id" => $ref_id];
|
":id" => $ref_id];
|
||||||
$sth->execute($params);
|
$sth->execute($params);
|
||||||
}
|
|
||||||
$sth = $pdo->prepare("INSERT INTO ttrss_user_entries
|
$sth = $pdo->prepare("INSERT INTO ttrss_user_entries
|
||||||
(ref_id, uuid, feed_id, orig_feed_id, owner_uid, published, tag_cache, label_cache,
|
(ref_id, uuid, feed_id, orig_feed_id, owner_uid, published, tag_cache, label_cache,
|
||||||
last_read, note, unread, last_published)
|
last_read, note, unread, last_published)
|
||||||
@@ -146,6 +145,8 @@ class Article extends Handler_Protected {
|
|||||||
(?, '', NULL, NULL, ?, true, '', '', NOW(), '', false, NOW())");
|
(?, '', NULL, NULL, ?, true, '', '', NOW(), '', false, NOW())");
|
||||||
$sth->execute([$ref_id, $owner_uid]);
|
$sth->execute([$ref_id, $owner_uid]);
|
||||||
|
|
||||||
|
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_PUBLISH_TOGGLED, [$ref_id]);
|
||||||
|
|
||||||
if (count($labels) != 0) {
|
if (count($labels) != 0) {
|
||||||
foreach ($labels as $label) {
|
foreach ($labels as $label) {
|
||||||
Labels::add_article($ref_id, trim($label), $owner_uid);
|
Labels::add_article($ref_id, trim($label), $owner_uid);
|
||||||
@@ -298,8 +299,6 @@ class Article extends Handler_Protected {
|
|||||||
* @return array{'formatted': string, 'entries': array<int, array<string, mixed>>}
|
* @return array{'formatted': string, 'entries': array<int, array<string, mixed>>}
|
||||||
*/
|
*/
|
||||||
static function _format_enclosures(int $id, bool $always_display_enclosures, string $article_content, bool $hide_images = false): array {
|
static function _format_enclosures(int $id, bool $always_display_enclosures, string $article_content, bool $hide_images = false): array {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$enclosures = self::_get_enclosures($id);
|
$enclosures = self::_get_enclosures($id);
|
||||||
$enclosures_formatted = "";
|
$enclosures_formatted = "";
|
||||||
|
|
||||||
@@ -326,7 +325,6 @@ class Article extends Handler_Protected {
|
|||||||
$enclosures_formatted, $enclosures, $id, $always_display_enclosures, $article_content, $hide_images);
|
$enclosures_formatted, $enclosures, $id, $always_display_enclosures, $article_content, $hide_images);
|
||||||
|
|
||||||
if (!empty($enclosures_formatted)) {
|
if (!empty($enclosures_formatted)) {
|
||||||
$span->end();
|
|
||||||
return [
|
return [
|
||||||
'formatted' => $enclosures_formatted,
|
'formatted' => $enclosures_formatted,
|
||||||
'entries' => []
|
'entries' => []
|
||||||
@@ -340,7 +338,7 @@ class Article extends Handler_Protected {
|
|||||||
|
|
||||||
$rv['can_inline'] = isset($_SESSION["uid"]) &&
|
$rv['can_inline'] = isset($_SESSION["uid"]) &&
|
||||||
empty($_SESSION["bw_limit"]) &&
|
empty($_SESSION["bw_limit"]) &&
|
||||||
!get_pref(Prefs::STRIP_IMAGES) &&
|
!Prefs::get(Prefs::STRIP_IMAGES, $_SESSION["uid"], $_SESSION["profile"] ?? null) &&
|
||||||
($always_display_enclosures || !preg_match("/<img/i", $article_content));
|
($always_display_enclosures || !preg_match("/<img/i", $article_content));
|
||||||
|
|
||||||
$rv['inline_text_only'] = $hide_images && $rv['can_inline'];
|
$rv['inline_text_only'] = $hide_images && $rv['can_inline'];
|
||||||
@@ -370,7 +368,6 @@ class Article extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
return $rv;
|
return $rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,8 +375,6 @@ class Article extends Handler_Protected {
|
|||||||
* @return array<int, string>
|
* @return array<int, string>
|
||||||
*/
|
*/
|
||||||
static function _get_tags(int $id, int $owner_uid = 0, ?string $tag_cache = null): array {
|
static function _get_tags(int $id, int $owner_uid = 0, ?string $tag_cache = null): array {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$a_id = $id;
|
$a_id = $id;
|
||||||
|
|
||||||
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
|
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
|
||||||
@@ -427,7 +422,6 @@ class Article extends Handler_Protected {
|
|||||||
$sth->execute([$tags_str, $id, $owner_uid]);
|
$sth->execute([$tags_str, $id, $owner_uid]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
return $tags;
|
return $tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,16 +466,9 @@ class Article extends Handler_Protected {
|
|||||||
|
|
||||||
static function _purge_orphans(): void {
|
static function _purge_orphans(): void {
|
||||||
|
|
||||||
// purge orphaned posts in main content table
|
|
||||||
|
|
||||||
if (Config::get(Config::DB_TYPE) == "mysql")
|
|
||||||
$limit_qpart = "LIMIT 5000";
|
|
||||||
else
|
|
||||||
$limit_qpart = "";
|
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
$res = $pdo->query("DELETE FROM ttrss_entries WHERE
|
$res = $pdo->query("DELETE FROM ttrss_entries WHERE
|
||||||
NOT EXISTS (SELECT ref_id FROM ttrss_user_entries WHERE ref_id = id) $limit_qpart");
|
NOT EXISTS (SELECT ref_id FROM ttrss_user_entries WHERE ref_id = id)");
|
||||||
|
|
||||||
if (Debug::enabled()) {
|
if (Debug::enabled()) {
|
||||||
$rows = $res->rowCount();
|
$rows = $res->rowCount();
|
||||||
@@ -522,8 +509,6 @@ class Article extends Handler_Protected {
|
|||||||
* @return array<int, array<int, int|string>>
|
* @return array<int, array<int, int|string>>
|
||||||
*/
|
*/
|
||||||
static function _get_labels(int $id, ?int $owner_uid = null): array {
|
static function _get_labels(int $id, ?int $owner_uid = null): array {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$rv = array();
|
$rv = array();
|
||||||
|
|
||||||
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
|
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
|
||||||
@@ -569,8 +554,6 @@ class Article extends Handler_Protected {
|
|||||||
else
|
else
|
||||||
Labels::update_cache($owner_uid, $id, array("no-labels" => 1));
|
Labels::update_cache($owner_uid, $id, array("no-labels" => 1));
|
||||||
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return $rv;
|
return $rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,9 +563,7 @@ class Article extends Handler_Protected {
|
|||||||
*
|
*
|
||||||
* @return array<int, Article::ARTICLE_KIND_*|string>
|
* @return array<int, Article::ARTICLE_KIND_*|string>
|
||||||
*/
|
*/
|
||||||
static function _get_image(array $enclosures, string $content, string $site_url, array $headline) {
|
static function _get_image(array $enclosures, string $content, string $site_url, array $headline): array {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$article_image = "";
|
$article_image = "";
|
||||||
$article_stream = "";
|
$article_stream = "";
|
||||||
$article_kind = 0;
|
$article_kind = 0;
|
||||||
@@ -603,6 +584,7 @@ class Article extends Handler_Protected {
|
|||||||
$tmpxpath = new DOMXPath($tmpdoc);
|
$tmpxpath = new DOMXPath($tmpdoc);
|
||||||
$elems = $tmpxpath->query('(//img[@src]|//video[@poster]|//iframe[contains(@src , "youtube.com/embed/")])');
|
$elems = $tmpxpath->query('(//img[@src]|//video[@poster]|//iframe[contains(@src , "youtube.com/embed/")])');
|
||||||
|
|
||||||
|
/** @var DOMElement $e */
|
||||||
foreach ($elems as $e) {
|
foreach ($elems as $e) {
|
||||||
if ($e->nodeName == "iframe") {
|
if ($e->nodeName == "iframe") {
|
||||||
$matches = [];
|
$matches = [];
|
||||||
@@ -635,7 +617,7 @@ class Article extends Handler_Protected {
|
|||||||
|
|
||||||
if (!$article_image)
|
if (!$article_image)
|
||||||
foreach ($enclosures as $enc) {
|
foreach ($enclosures as $enc) {
|
||||||
if (strpos($enc["content_type"], "image/") !== false) {
|
if (str_contains($enc["content_type"], "image/")) {
|
||||||
$article_image = $enc["content_url"];
|
$article_image = $enc["content_url"];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -660,8 +642,6 @@ class Article extends Handler_Protected {
|
|||||||
if ($article_stream && $cache->exists(sha1($article_stream)))
|
if ($article_stream && $cache->exists(sha1($article_stream)))
|
||||||
$article_stream = $cache->get_url(sha1($article_stream));
|
$article_stream = $cache->get_url(sha1($article_stream));
|
||||||
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return [$article_image, $article_stream, $article_kind];
|
return [$article_image, $article_stream, $article_kind];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,12 +655,12 @@ class Article extends Handler_Protected {
|
|||||||
if (count($article_ids) == 0)
|
if (count($article_ids) == 0)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$entries = ORM::for_table('ttrss_entries')
|
$entries = ORM::for_table('ttrss_entries')
|
||||||
->table_alias('e')
|
->table_alias('e')
|
||||||
->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue')
|
->select('ue.label_cache')
|
||||||
->where_in('id', $article_ids)
|
->join('ttrss_user_entries', ['ue.ref_id', '=', 'e.id'], 'ue')
|
||||||
|
->where_in('e.id', $article_ids)
|
||||||
|
->where('ue.owner_uid', $_SESSION['uid'])
|
||||||
->find_many();
|
->find_many();
|
||||||
|
|
||||||
$rv = [];
|
$rv = [];
|
||||||
@@ -696,8 +676,6 @@ class Article extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return array_unique($rv);
|
return array_unique($rv);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,12 +687,12 @@ class Article extends Handler_Protected {
|
|||||||
if (count($article_ids) == 0)
|
if (count($article_ids) == 0)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$entries = ORM::for_table('ttrss_entries')
|
$entries = ORM::for_table('ttrss_entries')
|
||||||
->table_alias('e')
|
->table_alias('e')
|
||||||
->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue')
|
->select('ue.feed_id')
|
||||||
->where_in('id', $article_ids)
|
->join('ttrss_user_entries', ['ue.ref_id', '=', 'e.id'], 'ue')
|
||||||
|
->where_in('e.id', $article_ids)
|
||||||
|
->where('ue.owner_uid', $_SESSION['uid'])
|
||||||
->find_many();
|
->find_many();
|
||||||
|
|
||||||
$rv = [];
|
$rv = [];
|
||||||
@@ -723,8 +701,6 @@ class Article extends Handler_Protected {
|
|||||||
array_push($rv, $entry->feed_id);
|
array_push($rv, $entry->feed_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return array_unique($rv);
|
return array_unique($rv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,11 @@ abstract class Auth_Base extends Plugin implements IAuthModule {
|
|||||||
/** Auto-creates specified user if allowed by system configuration.
|
/** Auto-creates specified user if allowed by system configuration.
|
||||||
* Can be used instead of find_user_by_login() by external auth modules
|
* Can be used instead of find_user_by_login() by external auth modules
|
||||||
* @param string $login
|
* @param string $login
|
||||||
* @param string|false $password
|
* @param null|string|false $password
|
||||||
* @return null|int
|
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
* @throws PDOException
|
* @throws PDOException
|
||||||
*/
|
*/
|
||||||
function auto_create_user(string $login, $password = false) {
|
function auto_create_user(string $login, null|false|string $password = false): ?int {
|
||||||
if ($login && Config::get(Config::AUTH_AUTO_CREATE)) {
|
if ($login && Config::get(Config::AUTH_AUTO_CREATE)) {
|
||||||
$user_id = UserHelper::find_user_by_login($login);
|
$user_id = UserHelper::find_user_by_login($login);
|
||||||
|
|
||||||
@@ -49,11 +48,9 @@ abstract class Auth_Base extends Plugin implements IAuthModule {
|
|||||||
|
|
||||||
|
|
||||||
/** replaced with UserHelper::find_user_by_login()
|
/** replaced with UserHelper::find_user_by_login()
|
||||||
* @param string $login
|
|
||||||
* @return null|int
|
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
function find_user_by_login(string $login) {
|
function find_user_by_login(string $login): ?int {
|
||||||
return UserHelper::find_user_by_login($login);
|
return UserHelper::find_user_by_login($login);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class Config {
|
|||||||
const T_STRING = 2;
|
const T_STRING = 2;
|
||||||
const T_INT = 3;
|
const T_INT = 3;
|
||||||
|
|
||||||
const SCHEMA_VERSION = 147;
|
const SCHEMA_VERSION = 151;
|
||||||
|
|
||||||
/** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX:
|
/** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX:
|
||||||
*
|
*
|
||||||
@@ -18,13 +18,16 @@ class Config {
|
|||||||
*
|
*
|
||||||
* or config.php:
|
* or config.php:
|
||||||
*
|
*
|
||||||
* putenv('TTRSS_DB_TYPE=pgsql');
|
* putenv('TTRSS_DB_HOST=my-patroni.example.com');
|
||||||
*
|
*
|
||||||
* note lack of quotes and spaces before and after "=".
|
* note lack of quotes and spaces before and after "=".
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** database type: pgsql or mysql */
|
/** this is kept for backwards/plugin compatibility, the only supported database is PostgreSQL
|
||||||
|
*
|
||||||
|
* @deprecated usages of `Config::get(Config::DB_TYPE)` should be replaced with default (and only) value: `pgsql` or removed
|
||||||
|
*/
|
||||||
const DB_TYPE = "DB_TYPE";
|
const DB_TYPE = "DB_TYPE";
|
||||||
|
|
||||||
/** database server hostname */
|
/** database server hostname */
|
||||||
@@ -42,9 +45,8 @@ class Config {
|
|||||||
/** database server port */
|
/** database server port */
|
||||||
const DB_PORT = "DB_PORT";
|
const DB_PORT = "DB_PORT";
|
||||||
|
|
||||||
/** connection charset for MySQL. if you have a legacy database and/or experience
|
/** PostgreSQL SSL mode (prefer, require, disabled) */
|
||||||
* garbage unicode characters with this option, try setting it to a blank string. */
|
const DB_SSLMODE = "DB_SSLMODE";
|
||||||
const MYSQL_CHARSET = "MYSQL_CHARSET";
|
|
||||||
|
|
||||||
/** this is a fallback falue for the CLI SAPI, it should be set to a fully-qualified tt-rss URL */
|
/** this 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";
|
const SELF_URL_PATH = "SELF_URL_PATH";
|
||||||
@@ -54,13 +56,6 @@ class Config {
|
|||||||
* your tt-rss directory protected by other means (e.g. http auth). */
|
* your tt-rss directory protected by other means (e.g. http auth). */
|
||||||
const SINGLE_USER_MODE = "SINGLE_USER_MODE";
|
const SINGLE_USER_MODE = "SINGLE_USER_MODE";
|
||||||
|
|
||||||
/** enables fallback update mode where tt-rss tries to update feeds in
|
|
||||||
* background while tt-rss is open in your browser.
|
|
||||||
* if you don't have a lot of feeds and don't want to or can't run
|
|
||||||
* background processes while not running tt-rss, this method is generally
|
|
||||||
* viable to keep your feeds up to date. */
|
|
||||||
const SIMPLE_UPDATE_MODE = "SIMPLE_UPDATE_MODE";
|
|
||||||
|
|
||||||
/** use this PHP CLI executable to start various tasks */
|
/** use this PHP CLI executable to start various tasks */
|
||||||
const PHP_EXECUTABLE = "PHP_EXECUTABLE";
|
const PHP_EXECUTABLE = "PHP_EXECUTABLE";
|
||||||
|
|
||||||
@@ -70,12 +65,6 @@ class Config {
|
|||||||
/** base directory for local cache (must be writable) */
|
/** base directory for local cache (must be writable) */
|
||||||
const CACHE_DIR = "CACHE_DIR";
|
const CACHE_DIR = "CACHE_DIR";
|
||||||
|
|
||||||
/** directory for feed favicons (directory must be writable) */
|
|
||||||
const ICONS_DIR = "ICONS_DIR";
|
|
||||||
|
|
||||||
/** URL for feed favicons */
|
|
||||||
const ICONS_URL = "ICONS_URL";
|
|
||||||
|
|
||||||
/** auto create users authenticated via external modules */
|
/** auto create users authenticated via external modules */
|
||||||
const AUTH_AUTO_CREATE = "AUTH_AUTO_CREATE";
|
const AUTH_AUTO_CREATE = "AUTH_AUTO_CREATE";
|
||||||
|
|
||||||
@@ -192,11 +181,44 @@ class Config {
|
|||||||
/** delay updates for this feed if received HTTP 429 (Too Many Requests) for this amount of seconds (base value, actual delay is base...base*2) */
|
/** delay updates for this feed if received HTTP 429 (Too Many Requests) for this amount of seconds (base value, actual delay is base...base*2) */
|
||||||
const HTTP_429_THROTTLE_INTERVAL = "HTTP_429_THROTTLE_INTERVAL";
|
const HTTP_429_THROTTLE_INTERVAL = "HTTP_429_THROTTLE_INTERVAL";
|
||||||
|
|
||||||
/** host running Jaeger collector to receive traces (disabled if empty) */
|
/** disables login form controls except HOOK_LOGINFORM_ADDITIONAL_BUTTONS (for SSO providers), also prevents logging in through auth_internal */
|
||||||
const OPENTELEMETRY_ENDPOINT = "OPENTELEMETRY_ENDPOINT";
|
const DISABLE_LOGIN_FORM = "DISABLE_LOGIN_FORM";
|
||||||
|
|
||||||
/** Jaeger service name */
|
/** optional key to transparently encrypt sensitive data (currently limited to sessions and feed passwords),
|
||||||
const OPENTELEMETRY_SERVICE = "OPENTELEMETRY_SERVICE";
|
* key is a 32 byte hex string which may be generated using `update.php --gen-encryption-key` */
|
||||||
|
const ENCRYPTION_KEY = "ENCRYPTION_KEY";
|
||||||
|
|
||||||
|
/** scheduled task to purge orphaned articles, value should be valid cron expression
|
||||||
|
* @see https://github.com/dragonmantank/cron-expression/blob/master/README.md#cron-expressions
|
||||||
|
*/
|
||||||
|
const SCHEDULE_PURGE_ORPHANS = "SCHEDULE_PURGE_ORPHANS";
|
||||||
|
|
||||||
|
/** scheduled task to expire disk cache, value should be valid cron expression */
|
||||||
|
const SCHEDULE_DISK_CACHE_EXPIRE_ALL = "SCHEDULE_DISK_CACHE_EXPIRE_ALL";
|
||||||
|
|
||||||
|
/** scheduled task, value should be valid cron expression */
|
||||||
|
const SCHEDULE_DISABLE_FAILED_FEEDS = "SCHEDULE_DISABLE_FAILED_FEEDS";
|
||||||
|
|
||||||
|
/** scheduled task to cleanup feed icons, value should be valid cron expression */
|
||||||
|
const SCHEDULE_CLEANUP_FEED_ICONS = "SCHEDULE_CLEANUP_FEED_ICONS";
|
||||||
|
|
||||||
|
/** scheduled task to disable feed updates of inactive users, value should be valid cron expression */
|
||||||
|
const SCHEDULE_LOG_DAEMON_UPDATE_LOGIN_LIMIT_USERS = "SCHEDULE_LOG_DAEMON_UPDATE_LOGIN_LIMIT_USERS";
|
||||||
|
|
||||||
|
/** scheduled task to cleanup error log, value should be valid cron expression */
|
||||||
|
const SCHEDULE_EXPIRE_ERROR_LOG = "SCHEDULE_EXPIRE_ERROR_LOG";
|
||||||
|
|
||||||
|
/** scheduled task to cleanup update daemon lock files, value should be valid cron expression */
|
||||||
|
const SCHEDULE_EXPIRE_LOCK_FILES = "SCHEDULE_EXPIRE_LOCK_FILES";
|
||||||
|
|
||||||
|
/** scheduled task to send digests, value should be valid cron expression */
|
||||||
|
const SCHEDULE_SEND_HEADLINES_DIGESTS = "SCHEDULE_SEND_HEADLINES_DIGESTS";
|
||||||
|
|
||||||
|
/** default (fallback) light theme path */
|
||||||
|
const DEFAULT_LIGHT_THEME = "DEFAULT_LIGHT_THEME";
|
||||||
|
|
||||||
|
/** default (fallback) dark (night) theme path */
|
||||||
|
const DEFAULT_DARK_THEME = "DEFAULT_DARK_THEME";
|
||||||
|
|
||||||
/** default values for all global configuration options */
|
/** default values for all global configuration options */
|
||||||
private const _DEFAULTS = [
|
private const _DEFAULTS = [
|
||||||
@@ -206,15 +228,12 @@ class Config {
|
|||||||
Config::DB_NAME => [ "", Config::T_STRING ],
|
Config::DB_NAME => [ "", Config::T_STRING ],
|
||||||
Config::DB_PASS => [ "", Config::T_STRING ],
|
Config::DB_PASS => [ "", Config::T_STRING ],
|
||||||
Config::DB_PORT => [ "5432", Config::T_STRING ],
|
Config::DB_PORT => [ "5432", Config::T_STRING ],
|
||||||
Config::MYSQL_CHARSET => [ "UTF8", Config::T_STRING ],
|
Config::DB_SSLMODE => [ "prefer", Config::T_STRING ],
|
||||||
Config::SELF_URL_PATH => [ "https://example.com/tt-rss", Config::T_STRING ],
|
Config::SELF_URL_PATH => [ "https://example.com/tt-rss", Config::T_STRING ],
|
||||||
Config::SINGLE_USER_MODE => [ "", Config::T_BOOL ],
|
Config::SINGLE_USER_MODE => [ "", Config::T_BOOL ],
|
||||||
Config::SIMPLE_UPDATE_MODE => [ "", Config::T_BOOL ],
|
|
||||||
Config::PHP_EXECUTABLE => [ "/usr/bin/php", Config::T_STRING ],
|
Config::PHP_EXECUTABLE => [ "/usr/bin/php", Config::T_STRING ],
|
||||||
Config::LOCK_DIRECTORY => [ "lock", Config::T_STRING ],
|
Config::LOCK_DIRECTORY => [ "lock", Config::T_STRING ],
|
||||||
Config::CACHE_DIR => [ "cache", Config::T_STRING ],
|
Config::CACHE_DIR => [ "cache", Config::T_STRING ],
|
||||||
Config::ICONS_DIR => [ "feed-icons", Config::T_STRING ],
|
|
||||||
Config::ICONS_URL => [ "feed-icons", Config::T_STRING ],
|
|
||||||
Config::AUTH_AUTO_CREATE => [ "true", Config::T_BOOL ],
|
Config::AUTH_AUTO_CREATE => [ "true", Config::T_BOOL ],
|
||||||
Config::AUTH_AUTO_LOGIN => [ "true", Config::T_BOOL ],
|
Config::AUTH_AUTO_LOGIN => [ "true", Config::T_BOOL ],
|
||||||
Config::FORCE_ARTICLE_PURGE => [ 0, Config::T_INT ],
|
Config::FORCE_ARTICLE_PURGE => [ 0, Config::T_INT ],
|
||||||
@@ -253,24 +272,33 @@ class Config {
|
|||||||
Config::CHECK_FOR_PLUGIN_UPDATES => [ "true", Config::T_BOOL ],
|
Config::CHECK_FOR_PLUGIN_UPDATES => [ "true", Config::T_BOOL ],
|
||||||
Config::ENABLE_PLUGIN_INSTALLER => [ "true", Config::T_BOOL ],
|
Config::ENABLE_PLUGIN_INSTALLER => [ "true", Config::T_BOOL ],
|
||||||
Config::AUTH_MIN_INTERVAL => [ 5, Config::T_INT ],
|
Config::AUTH_MIN_INTERVAL => [ 5, Config::T_INT ],
|
||||||
Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)',
|
Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://github.com/tt-rss/tt-rss)',
|
||||||
Config::T_STRING ],
|
Config::T_STRING ],
|
||||||
Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ],
|
Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ],
|
||||||
Config::OPENTELEMETRY_ENDPOINT => [ "", Config::T_STRING ],
|
Config::DISABLE_LOGIN_FORM => [ "", Config::T_BOOL ],
|
||||||
Config::OPENTELEMETRY_SERVICE => [ "tt-rss", Config::T_STRING ],
|
Config::ENCRYPTION_KEY => [ "", Config::T_STRING ],
|
||||||
|
Config::SCHEDULE_PURGE_ORPHANS => ["@daily", Config::T_STRING],
|
||||||
|
Config::SCHEDULE_DISK_CACHE_EXPIRE_ALL => ["@daily", Config::T_STRING],
|
||||||
|
Config::SCHEDULE_DISABLE_FAILED_FEEDS => ["@daily", Config::T_STRING],
|
||||||
|
Config::SCHEDULE_CLEANUP_FEED_ICONS => ["@daily", Config::T_STRING],
|
||||||
|
Config::SCHEDULE_LOG_DAEMON_UPDATE_LOGIN_LIMIT_USERS =>
|
||||||
|
["@daily", Config::T_STRING],
|
||||||
|
Config::SCHEDULE_EXPIRE_ERROR_LOG => ["@hourly", Config::T_STRING],
|
||||||
|
Config::SCHEDULE_EXPIRE_LOCK_FILES => ["@hourly", Config::T_STRING],
|
||||||
|
Config::SCHEDULE_SEND_HEADLINES_DIGESTS => ["@hourly", Config::T_STRING],
|
||||||
|
Config::DEFAULT_LIGHT_THEME => [ "light.css", Config::T_STRING],
|
||||||
|
Config::DEFAULT_DARK_THEME => [ "night.css", Config::T_STRING],
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @var Config|null */
|
private static ?Config $instance = null;
|
||||||
private static $instance;
|
|
||||||
|
|
||||||
/** @var array<string, array<bool|int|string>> */
|
/** @var array<string, array<bool|int|string>> */
|
||||||
private $params = [];
|
private array $params = [];
|
||||||
|
|
||||||
/** @var array<string, mixed> */
|
/** @var array<string, mixed> */
|
||||||
private $version = [];
|
private array $version = [];
|
||||||
|
|
||||||
/** @var Db_Migrations|null $migrations */
|
private Db_Migrations $migrations;
|
||||||
private $migrations;
|
|
||||||
|
|
||||||
public static function get_instance() : Config {
|
public static function get_instance() : Config {
|
||||||
if (self::$instance == null)
|
if (self::$instance == null)
|
||||||
@@ -304,7 +332,7 @@ class Config {
|
|||||||
* based on source git tree commit used when creating the package
|
* based on source git tree commit used when creating the package
|
||||||
* @return array<string, mixed>|string
|
* @return array<string, mixed>|string
|
||||||
*/
|
*/
|
||||||
static function get_version(bool $as_string = true) {
|
static function get_version(bool $as_string = true): array|string {
|
||||||
return self::get_instance()->_get_version($as_string);
|
return self::get_instance()->_get_version($as_string);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +350,7 @@ class Config {
|
|||||||
/**
|
/**
|
||||||
* @return array<string, mixed>|string
|
* @return array<string, mixed>|string
|
||||||
*/
|
*/
|
||||||
private function _get_version(bool $as_string = true) {
|
private function _get_version(bool $as_string = true): array|string {
|
||||||
$root_dir = self::get_self_dir();
|
$root_dir = self::get_self_dir();
|
||||||
|
|
||||||
if (empty($this->version)) {
|
if (empty($this->version)) {
|
||||||
@@ -431,24 +459,15 @@ class Config {
|
|||||||
return self::get_migrations()->get_version();
|
return self::get_migrations()->get_version();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static function cast_to(string $value, int $type_hint): bool|int|string {
|
||||||
* @return bool|int|string
|
return match ($type_hint) {
|
||||||
*/
|
self::T_BOOL => sql_bool_to_bool($value),
|
||||||
static function cast_to(string $value, int $type_hint) {
|
self::T_INT => (int) $value,
|
||||||
switch ($type_hint) {
|
default => $value,
|
||||||
case self::T_BOOL:
|
};
|
||||||
return sql_bool_to_bool($value);
|
|
||||||
case self::T_INT:
|
|
||||||
return (int) $value;
|
|
||||||
default:
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function _get(string $param): bool|int|string {
|
||||||
* @return bool|int|string
|
|
||||||
*/
|
|
||||||
private function _get(string $param) {
|
|
||||||
list ($value, $type_hint) = $this->params[$param];
|
list ($value, $type_hint) = $this->params[$param];
|
||||||
|
|
||||||
return $this->cast_to($value, $type_hint);
|
return $this->cast_to($value, $type_hint);
|
||||||
@@ -466,10 +485,7 @@ class Config {
|
|||||||
$instance->_add($param, $default, $type_hint);
|
$instance->_add($param, $default, $type_hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static function get(string $param): bool|int|string {
|
||||||
* @return bool|int|string
|
|
||||||
*/
|
|
||||||
static function get(string $param) {
|
|
||||||
$instance = self::get_instance();
|
$instance = self::get_instance();
|
||||||
|
|
||||||
return $instance->_get($param);
|
return $instance->_get($param);
|
||||||
@@ -491,32 +507,13 @@ class Config {
|
|||||||
|
|
||||||
$self_url_path = $proto . '://' . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
|
$self_url_path = $proto . '://' . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
|
||||||
$self_url_path = preg_replace("/(\/api\/{1,})?(\w+\.php)?(\?.*$)?$/", "", $self_url_path);
|
$self_url_path = preg_replace("/(\/api\/{1,})?(\w+\.php)?(\?.*$)?$/", "", $self_url_path);
|
||||||
$self_url_path = preg_replace("/(\/plugins(.local))\/.{1,}$/", "", $self_url_path);
|
$self_url_path = preg_replace("/(\/plugins(.local)?)\/.{1,}$/", "", $self_url_path);
|
||||||
|
|
||||||
return rtrim($self_url_path, "/");
|
return rtrim($self_url_path, "/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* sanity check stuff */
|
/* sanity check stuff */
|
||||||
|
|
||||||
/** checks for mysql tables not using InnoDB (tt-rss is incompatible with MyISAM)
|
|
||||||
* @return array<int, array<string, string>> A list of entries identifying tt-rss tables with bad config
|
|
||||||
*/
|
|
||||||
private static function check_mysql_tables() {
|
|
||||||
$pdo = Db::pdo();
|
|
||||||
|
|
||||||
$sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
|
|
||||||
table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
|
|
||||||
$sth->execute([self::get(Config::DB_NAME)]);
|
|
||||||
|
|
||||||
$bad_tables = [];
|
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
|
||||||
array_push($bad_tables, $line);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $bad_tables;
|
|
||||||
}
|
|
||||||
|
|
||||||
static function sanity_check(): void {
|
static function sanity_check(): void {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -530,7 +527,7 @@ class Config {
|
|||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
if (strpos(self::get(Config::PLUGINS), "auth_") === false) {
|
if (!str_contains(self::get(Config::PLUGINS), "auth_")) {
|
||||||
array_push($errors, "Please enable at least one authentication module via PLUGINS");
|
array_push($errors, "Please enable at least one authentication module via PLUGINS");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,8 +539,8 @@ class Config {
|
|||||||
array_push($errors, "Please don't run this script as root.");
|
array_push($errors, "Please don't run this script as root.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version_compare(PHP_VERSION, '7.4.0', '<')) {
|
if (version_compare(PHP_VERSION, '8.2.0', '<')) {
|
||||||
array_push($errors, "PHP version 7.4.0 or newer required. You're using " . PHP_VERSION . ".");
|
array_push($errors, "PHP version 8.2.0 or newer required. You're using " . PHP_VERSION . ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!class_exists("UConverter")) {
|
if (!class_exists("UConverter")) {
|
||||||
@@ -599,10 +596,6 @@ class Config {
|
|||||||
array_push($errors, "Data export cache is not writable (chmod -R 777 ".self::get(Config::CACHE_DIR)."/export)");
|
array_push($errors, "Data export cache is not writable (chmod -R 777 ".self::get(Config::CACHE_DIR)."/export)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_writable(self::get(Config::ICONS_DIR))) {
|
|
||||||
array_push($errors, "ICONS_DIR defined in config.php is not writable (chmod -R 777 ".self::get(Config::ICONS_DIR).").\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!is_writable(self::get(Config::LOCK_DIRECTORY))) {
|
if (!is_writable(self::get(Config::LOCK_DIRECTORY))) {
|
||||||
array_push($errors, "LOCK_DIRECTORY is not writable (chmod -R 777 ".self::get(Config::LOCK_DIRECTORY).").\n");
|
array_push($errors, "LOCK_DIRECTORY is not writable (chmod -R 777 ".self::get(Config::LOCK_DIRECTORY).").\n");
|
||||||
}
|
}
|
||||||
@@ -621,29 +614,6 @@ class Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self::get(Config::DB_TYPE) == "mysql") {
|
|
||||||
$bad_tables = self::check_mysql_tables();
|
|
||||||
|
|
||||||
if (count($bad_tables) > 0) {
|
|
||||||
$bad_tables_fmt = [];
|
|
||||||
|
|
||||||
foreach ($bad_tables as $bt) {
|
|
||||||
array_push($bad_tables_fmt, sprintf("%s (%s)", $bt['table_name'], $bt['engine']));
|
|
||||||
}
|
|
||||||
|
|
||||||
$msg = "<p>The following tables use an unsupported MySQL engine: <b>" .
|
|
||||||
implode(", ", $bad_tables_fmt) . "</b>.</p>";
|
|
||||||
|
|
||||||
$msg .= "<p>The only supported engine on MySQL is InnoDB. MyISAM lacks functionality to run
|
|
||||||
tt-rss.
|
|
||||||
Please backup your data (via OPML) and re-import the schema before continuing.</p>
|
|
||||||
<p><b>WARNING: importing the schema would mean LOSS OF ALL YOUR DATA.</b></p>";
|
|
||||||
|
|
||||||
|
|
||||||
array_push($errors, $msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count($errors) > 0 && php_sapi_name() != "cli") {
|
if (count($errors) > 0 && php_sapi_name() != "cli") {
|
||||||
http_response_code(503); ?>
|
http_response_code(503); ?>
|
||||||
|
|
||||||
@@ -662,9 +632,9 @@ class Config {
|
|||||||
|
|
||||||
<?php foreach ($errors as $error) { echo self::format_error($error); } ?>
|
<?php foreach ($errors as $error) { echo self::format_error($error); } ?>
|
||||||
|
|
||||||
<p>You might want to check the tt-rss <a target="_blank" href="https://tt-rss.org/wiki/InstallationNotes/">wiki</a> or
|
<p>You might want to check the tt-rss <a target="_blank" href="https://github.com/tt-rss/tt-rss/wiki">wiki</a> or
|
||||||
<a target="_blank" href="https://community.tt-rss.org/">forums</a> for more information. Please search the forums before creating a new topic
|
<a target="_blank" href="https://github.com/tt-rss/tt-rss/discussions">discussions</a> for more information.
|
||||||
for your question.</p>
|
Please search before creating a new topic for your question.</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class Counters {
|
|||||||
static private function get_cat_children(int $cat_id, int $owner_uid): array {
|
static private function get_cat_children(int $cat_id, int $owner_uid): array {
|
||||||
$unread = 0;
|
$unread = 0;
|
||||||
$marked = 0;
|
$marked = 0;
|
||||||
|
$published = 0;
|
||||||
|
|
||||||
$cats = ORM::for_table('ttrss_feed_categories')
|
$cats = ORM::for_table('ttrss_feed_categories')
|
||||||
->where('owner_uid', $owner_uid)
|
->where('owner_uid', $owner_uid)
|
||||||
@@ -42,13 +43,14 @@ class Counters {
|
|||||||
->find_many();
|
->find_many();
|
||||||
|
|
||||||
foreach ($cats as $cat) {
|
foreach ($cats as $cat) {
|
||||||
list ($tmp_unread, $tmp_marked) = self::get_cat_children($cat->id, $owner_uid);
|
list ($tmp_unread, $tmp_marked, $tmp_published) = self::get_cat_children($cat->id, $owner_uid);
|
||||||
|
|
||||||
$unread += $tmp_unread + Feeds::_get_cat_unread($cat->id, $owner_uid);
|
$unread += $tmp_unread + Feeds::_get_cat_unread($cat->id, $owner_uid);
|
||||||
$marked += $tmp_marked + Feeds::_get_cat_marked($cat->id, $owner_uid);
|
$marked += $tmp_marked + Feeds::_get_cat_marked($cat->id, $owner_uid);
|
||||||
|
$published += $tmp_published + Feeds::_get_cat_published($cat->id, $owner_uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [$unread, $marked];
|
return [$unread, $marked, $published];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,8 +77,9 @@ class Counters {
|
|||||||
|
|
||||||
$sth = $pdo->prepare("SELECT fc.id,
|
$sth = $pdo->prepare("SELECT fc.id,
|
||||||
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
||||||
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
||||||
(SELECT COUNT(id) FROM ttrss_feed_categories fcc
|
SUM(CASE WHEN published THEN 1 ELSE 0 END) AS count_published,
|
||||||
|
(SELECT COUNT(id) FROM ttrss_feed_categories fcc
|
||||||
WHERE fcc.parent_cat = fc.id) AS num_children
|
WHERE fcc.parent_cat = fc.id) AS num_children
|
||||||
FROM ttrss_feed_categories fc
|
FROM ttrss_feed_categories fc
|
||||||
LEFT JOIN ttrss_feeds f ON (f.cat_id = fc.id)
|
LEFT JOIN ttrss_feeds f ON (f.cat_id = fc.id)
|
||||||
@@ -86,8 +89,9 @@ class Counters {
|
|||||||
UNION
|
UNION
|
||||||
SELECT 0,
|
SELECT 0,
|
||||||
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
||||||
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
||||||
0
|
SUM(CASE WHEN published THEN 1 ELSE 0 END) AS count_published,
|
||||||
|
0
|
||||||
FROM ttrss_feeds f, ttrss_user_entries ue
|
FROM ttrss_feeds f, ttrss_user_entries ue
|
||||||
WHERE f.cat_id IS NULL AND
|
WHERE f.cat_id IS NULL AND
|
||||||
ue.feed_id = f.id AND
|
ue.feed_id = f.id AND
|
||||||
@@ -98,8 +102,9 @@ class Counters {
|
|||||||
} else {
|
} else {
|
||||||
$sth = $pdo->prepare("SELECT fc.id,
|
$sth = $pdo->prepare("SELECT fc.id,
|
||||||
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
||||||
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
||||||
(SELECT COUNT(id) FROM ttrss_feed_categories fcc
|
SUM(CASE WHEN published THEN 1 ELSE 0 END) AS count_published,
|
||||||
|
(SELECT COUNT(id) FROM ttrss_feed_categories fcc
|
||||||
WHERE fcc.parent_cat = fc.id) AS num_children
|
WHERE fcc.parent_cat = fc.id) AS num_children
|
||||||
FROM ttrss_feed_categories fc
|
FROM ttrss_feed_categories fc
|
||||||
LEFT JOIN ttrss_feeds f ON (f.cat_id = fc.id)
|
LEFT JOIN ttrss_feeds f ON (f.cat_id = fc.id)
|
||||||
@@ -109,8 +114,9 @@ class Counters {
|
|||||||
UNION
|
UNION
|
||||||
SELECT 0,
|
SELECT 0,
|
||||||
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
||||||
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked,
|
||||||
0
|
SUM(CASE WHEN published THEN 1 ELSE 0 END) AS count_published,
|
||||||
|
0
|
||||||
FROM ttrss_feeds f, ttrss_user_entries ue
|
FROM ttrss_feeds f, ttrss_user_entries ue
|
||||||
WHERE f.cat_id IS NULL AND
|
WHERE f.cat_id IS NULL AND
|
||||||
ue.feed_id = f.id AND
|
ue.feed_id = f.id AND
|
||||||
@@ -121,16 +127,18 @@ class Counters {
|
|||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch()) {
|
||||||
if ($line["num_children"] > 0) {
|
if ($line["num_children"] > 0) {
|
||||||
list ($child_counter, $child_marked_counter) = self::get_cat_children($line["id"], $_SESSION["uid"]);
|
list ($child_counter, $child_marked_counter, $child_published_counter) = self::get_cat_children($line["id"], $_SESSION["uid"]);
|
||||||
} else {
|
} else {
|
||||||
$child_counter = 0;
|
$child_counter = 0;
|
||||||
$child_marked_counter = 0;
|
$child_marked_counter = 0;
|
||||||
|
$child_published_counter = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cv = [
|
$cv = [
|
||||||
"id" => (int)$line["id"],
|
"id" => (int)$line["id"],
|
||||||
"kind" => "cat",
|
"kind" => "cat",
|
||||||
"markedcounter" => (int) $line["count_marked"] + $child_marked_counter,
|
"markedcounter" => (int) $line["count_marked"] + $child_marked_counter,
|
||||||
|
"publishedcounter" => (int) $line["count_published"] + $child_published_counter,
|
||||||
"counter" => (int) $line["count"] + $child_counter
|
"counter" => (int) $line["count"] + $child_counter
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -145,75 +153,40 @@ class Counters {
|
|||||||
* @return array<int, array<string, int|string>>
|
* @return array<int, array<string, int|string>>
|
||||||
*/
|
*/
|
||||||
private static function get_feeds(?array $feed_ids = null): array {
|
private static function get_feeds(?array $feed_ids = null): array {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$ret = [];
|
$ret = [];
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
if (is_array($feed_ids) && count($feed_ids) === 0)
|
||||||
|
return $ret;
|
||||||
|
|
||||||
if (is_array($feed_ids)) {
|
$feeds = ORM::for_table('ttrss_feeds')
|
||||||
if (count($feed_ids) == 0)
|
->table_alias('f')
|
||||||
return [];
|
->select_many('f.id', 'f.title', 'f.last_error')
|
||||||
|
->select_many_expr([
|
||||||
|
'count' => 'SUM(CASE WHEN ue.unread THEN 1 ELSE 0 END)',
|
||||||
|
'count_marked' => 'SUM(CASE WHEN ue.marked THEN 1 ELSE 0 END)',
|
||||||
|
'count_published' => 'SUM(CASE WHEN ue.published THEN 1 ELSE 0 END)',
|
||||||
|
'last_updated' => 'SUBSTRING_FOR_DATE(f.last_updated,1,19)',
|
||||||
|
])
|
||||||
|
->join('ttrss_user_entries', [ 'ue.feed_id', '=', 'f.id'], 'ue')
|
||||||
|
->where('ue.owner_uid', $_SESSION['uid'])
|
||||||
|
->group_by('f.id');
|
||||||
|
|
||||||
$feed_ids_qmarks = arr_qmarks($feed_ids);
|
if (is_array($feed_ids))
|
||||||
|
$feeds->where_in('f.id', $feed_ids);
|
||||||
|
|
||||||
$sth = $pdo->prepare("SELECT f.id,
|
foreach ($feeds->find_many() as $feed) {
|
||||||
f.title,
|
$ret[] = [
|
||||||
".SUBSTRING_FOR_DATE."(f.last_updated,1,19) AS last_updated,
|
'id' => $feed->id,
|
||||||
f.last_error,
|
'title' => truncate_string($feed->title, 30),
|
||||||
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
'error' => $feed->last_error,
|
||||||
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked
|
'updated' => TimeHelper::make_local_datetime($feed->last_updated),
|
||||||
FROM ttrss_feeds f, ttrss_user_entries ue
|
'counter' => (int) $feed->count,
|
||||||
WHERE f.id = ue.feed_id AND ue.owner_uid = ? AND f.id IN ($feed_ids_qmarks)
|
'markedcounter' => (int) $feed->count_marked,
|
||||||
GROUP BY f.id");
|
'publishedcounter' => (int) $feed->count_published,
|
||||||
|
'ts' => Feeds::_has_icon($feed->id) ? (int) filemtime(Feeds::_get_icon_file($feed->id)) : 0,
|
||||||
$sth->execute([$_SESSION['uid'], ...$feed_ids]);
|
|
||||||
} else {
|
|
||||||
$sth = $pdo->prepare("SELECT f.id,
|
|
||||||
f.title,
|
|
||||||
".SUBSTRING_FOR_DATE."(f.last_updated,1,19) AS last_updated,
|
|
||||||
f.last_error,
|
|
||||||
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
|
|
||||||
SUM(CASE WHEN marked THEN 1 ELSE 0 END) AS count_marked
|
|
||||||
FROM ttrss_feeds f, ttrss_user_entries ue
|
|
||||||
WHERE f.id = ue.feed_id AND ue.owner_uid = :uid
|
|
||||||
GROUP BY f.id");
|
|
||||||
|
|
||||||
$sth->execute(["uid" => $_SESSION['uid']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
|
||||||
|
|
||||||
$id = $line["id"];
|
|
||||||
$last_updated = TimeHelper::make_local_datetime($line['last_updated'], false);
|
|
||||||
|
|
||||||
if (Feeds::_has_icon($id)) {
|
|
||||||
$ts = filemtime(Feeds::_get_icon_file($id));
|
|
||||||
} else {
|
|
||||||
$ts = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide default un-updated timestamp i.e. 1970-01-01 (?) -fox
|
|
||||||
if ((int)date('Y') - (int)date('Y', strtotime($line['last_updated'] ?? '')) > 2)
|
|
||||||
$last_updated = '';
|
|
||||||
|
|
||||||
$cv = [
|
|
||||||
"id" => $id,
|
|
||||||
"updated" => $last_updated,
|
|
||||||
"counter" => (int) $line["count"],
|
|
||||||
"markedcounter" => (int) $line["count_marked"],
|
|
||||||
"ts" => (int) $ts
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$cv["error"] = $line["last_error"];
|
|
||||||
$cv["title"] = truncate_string($line["title"], 30);
|
|
||||||
|
|
||||||
array_push($ret, $cv);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,8 +194,6 @@ class Counters {
|
|||||||
* @return array<int, array<string, int|string>>
|
* @return array<int, array<string, int|string>>
|
||||||
*/
|
*/
|
||||||
private static function get_global(): array {
|
private static function get_global(): array {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$ret = [
|
$ret = [
|
||||||
[
|
[
|
||||||
"id" => "global-unread",
|
"id" => "global-unread",
|
||||||
@@ -239,8 +210,6 @@ class Counters {
|
|||||||
"counter" => $subcribed_feeds
|
"counter" => $subcribed_feeds
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,8 +217,6 @@ class Counters {
|
|||||||
* @return array<int, array<string, int|string>>
|
* @return array<int, array<string, int|string>>
|
||||||
*/
|
*/
|
||||||
private static function get_virt(): array {
|
private static function get_virt(): array {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$ret = [];
|
$ret = [];
|
||||||
|
|
||||||
foreach ([Feeds::FEED_ARCHIVED, Feeds::FEED_STARRED, Feeds::FEED_PUBLISHED,
|
foreach ([Feeds::FEED_ARCHIVED, Feeds::FEED_STARRED, Feeds::FEED_PUBLISHED,
|
||||||
@@ -271,31 +238,29 @@ class Counters {
|
|||||||
if ($feed_id == Feeds::FEED_STARRED)
|
if ($feed_id == Feeds::FEED_STARRED)
|
||||||
$cv["markedcounter"] = $auxctr;
|
$cv["markedcounter"] = $auxctr;
|
||||||
|
|
||||||
|
if ($feed_id == Feeds::FEED_PUBLISHED)
|
||||||
|
$cv["publishedcounter"] = $auxctr;
|
||||||
|
|
||||||
array_push($ret, $cv);
|
array_push($ret, $cv);
|
||||||
}
|
}
|
||||||
|
|
||||||
$feeds = PluginHost::getInstance()->get_feeds(Feeds::CATEGORY_SPECIAL);
|
foreach (PluginHost::getInstance()->get_feeds(Feeds::CATEGORY_SPECIAL) as $feed) {
|
||||||
|
if (!implements_interface($feed['sender'], 'IVirtualFeed'))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (is_array($feeds)) {
|
/** @var Plugin&IVirtualFeed $feed['sender'] */
|
||||||
foreach ($feeds as $feed) {
|
|
||||||
/** @var IVirtualFeed $feed['sender'] */
|
|
||||||
|
|
||||||
if (!implements_interface($feed['sender'], 'IVirtualFeed'))
|
$cv = [
|
||||||
continue;
|
"id" => PluginHost::pfeed_to_feed_id($feed['id']),
|
||||||
|
"counter" => $feed['sender']->get_unread($feed['id'])
|
||||||
|
];
|
||||||
|
|
||||||
$cv = [
|
if (method_exists($feed['sender'], 'get_total'))
|
||||||
"id" => PluginHost::pfeed_to_feed_id($feed['id']),
|
$cv["auxcounter"] = $feed['sender']->get_total($feed['id']);
|
||||||
"counter" => $feed['sender']->get_unread($feed['id'])
|
|
||||||
];
|
|
||||||
|
|
||||||
if (method_exists($feed['sender'], 'get_total'))
|
array_push($ret, $cv);
|
||||||
$cv["auxcounter"] = $feed['sender']->get_total($feed['id']);
|
|
||||||
|
|
||||||
array_push($ret, $cv);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,8 +269,6 @@ class Counters {
|
|||||||
* @return array<int, array<string, int|string>>
|
* @return array<int, array<string, int|string>>
|
||||||
*/
|
*/
|
||||||
static function get_labels(?array $label_ids = null): array {
|
static function get_labels(?array $label_ids = null): array {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$ret = [];
|
$ret = [];
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
@@ -320,6 +283,7 @@ class Counters {
|
|||||||
caption,
|
caption,
|
||||||
SUM(CASE WHEN u1.unread = true THEN 1 ELSE 0 END) AS count_unread,
|
SUM(CASE WHEN u1.unread = true THEN 1 ELSE 0 END) AS count_unread,
|
||||||
SUM(CASE WHEN u1.marked = true THEN 1 ELSE 0 END) AS count_marked,
|
SUM(CASE WHEN u1.marked = true THEN 1 ELSE 0 END) AS count_marked,
|
||||||
|
SUM(CASE WHEN u1.published = true THEN 1 ELSE 0 END) AS count_published,
|
||||||
COUNT(u1.unread) AS total
|
COUNT(u1.unread) AS total
|
||||||
FROM ttrss_labels2 LEFT JOIN ttrss_user_labels2 ON
|
FROM ttrss_labels2 LEFT JOIN ttrss_user_labels2 ON
|
||||||
(ttrss_labels2.id = label_id)
|
(ttrss_labels2.id = label_id)
|
||||||
@@ -332,6 +296,7 @@ class Counters {
|
|||||||
caption,
|
caption,
|
||||||
SUM(CASE WHEN u1.unread = true THEN 1 ELSE 0 END) AS count_unread,
|
SUM(CASE WHEN u1.unread = true THEN 1 ELSE 0 END) AS count_unread,
|
||||||
SUM(CASE WHEN u1.marked = true THEN 1 ELSE 0 END) AS count_marked,
|
SUM(CASE WHEN u1.marked = true THEN 1 ELSE 0 END) AS count_marked,
|
||||||
|
SUM(CASE WHEN u1.published = true THEN 1 ELSE 0 END) AS count_published,
|
||||||
COUNT(u1.unread) AS total
|
COUNT(u1.unread) AS total
|
||||||
FROM ttrss_labels2 LEFT JOIN ttrss_user_labels2 ON
|
FROM ttrss_labels2 LEFT JOIN ttrss_user_labels2 ON
|
||||||
(ttrss_labels2.id = label_id)
|
(ttrss_labels2.id = label_id)
|
||||||
@@ -350,13 +315,13 @@ class Counters {
|
|||||||
"counter" => (int) $line["count_unread"],
|
"counter" => (int) $line["count_unread"],
|
||||||
"auxcounter" => (int) $line["total"],
|
"auxcounter" => (int) $line["total"],
|
||||||
"markedcounter" => (int) $line["count_marked"],
|
"markedcounter" => (int) $line["count_marked"],
|
||||||
|
"publishedcounter" => (int) $line["count_published"],
|
||||||
"description" => $line["caption"]
|
"description" => $line["caption"]
|
||||||
];
|
];
|
||||||
|
|
||||||
array_push($ret, $cv);
|
array_push($ret, $cv);
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
classes/Crypt.php
Normal file
62
classes/Crypt.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
class Crypt {
|
||||||
|
|
||||||
|
/** the only algo supported at the moment */
|
||||||
|
private const ENCRYPT_ALGO = 'xchacha20poly1305_ietf';
|
||||||
|
|
||||||
|
/** currently only generates keys using sodium_crypto_aead_chacha20poly1305_keygen() i.e. one supported Crypt::ENCRYPT_ALGO
|
||||||
|
* @return string random 256-bit (for ChaCha20-Poly1305) binary string
|
||||||
|
*/
|
||||||
|
static function generate_key() : string {
|
||||||
|
return sodium_crypto_aead_chacha20poly1305_keygen();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** encrypts provided ciphertext using Config::ENCRYPTION_KEY into an encrypted object
|
||||||
|
*
|
||||||
|
* @return array{'algo': string, 'nonce': string, 'payload': string} encrypted data object containing algo, nonce, and encrypted data
|
||||||
|
*/
|
||||||
|
static function encrypt_string(string $ciphertext) : array {
|
||||||
|
$key = Config::get(Config::ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
if (!$key)
|
||||||
|
throw new Exception("Crypt::encrypt_string() failed to encrypt - key is not available");
|
||||||
|
|
||||||
|
$nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
|
||||||
|
|
||||||
|
$payload = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($ciphertext, '', $nonce, hex2bin($key));
|
||||||
|
|
||||||
|
if ($payload) {
|
||||||
|
$encrypted_data = [
|
||||||
|
'algo' => self::ENCRYPT_ALGO,
|
||||||
|
'nonce' => $nonce,
|
||||||
|
'payload' => $payload,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $encrypted_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Crypt::encrypt_string() failed to encrypt ciphertext");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** decrypts payload of a valid encrypted object using Config::ENCRYPTION_KEY
|
||||||
|
*
|
||||||
|
* @param array{'algo': string, 'nonce': string, 'payload': string} $encrypted_data
|
||||||
|
*
|
||||||
|
* @return string decrypted string payload
|
||||||
|
*/
|
||||||
|
static function decrypt_string(array $encrypted_data) : string {
|
||||||
|
$key = Config::get(Config::ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
if (!$key)
|
||||||
|
throw new Exception("Crypt::decrypt_string() failed to decrypt - key is not available");
|
||||||
|
|
||||||
|
// only one is supported for the time being
|
||||||
|
switch ($encrypted_data['algo']) {
|
||||||
|
case self::ENCRYPT_ALGO:
|
||||||
|
return sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($encrypted_data['payload'], '', $encrypted_data['nonce'], hex2bin($key));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception('Crypt::decrypt_string() failed to decrypt passed encrypted data object, unsupported algo: ' . $encrypted_data['algo']);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,20 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
class Db
|
class Db {
|
||||||
{
|
private static ?Db $instance = null;
|
||||||
/** @var Db $instance */
|
|
||||||
private static $instance;
|
|
||||||
|
|
||||||
/** @var PDO|null $pdo */
|
private ?PDO $pdo = null;
|
||||||
private $pdo;
|
|
||||||
|
|
||||||
function __construct() {
|
function __construct() {
|
||||||
ORM::configure(self::get_dsn());
|
ORM::configure(self::get_dsn());
|
||||||
ORM::configure('username', Config::get(Config::DB_USER));
|
ORM::configure('username', Config::get(Config::DB_USER));
|
||||||
ORM::configure('password', Config::get(Config::DB_PASS));
|
ORM::configure('password', Config::get(Config::DB_PASS));
|
||||||
ORM::configure('return_result_sets', true);
|
ORM::configure('return_result_sets', true);
|
||||||
if (Config::get(Config::DB_TYPE) == "mysql" && Config::get(Config::MYSQL_CHARSET)) {
|
|
||||||
ORM::configure('driver_options', array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES ' . Config::get(Config::MYSQL_CHARSET)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,13 +26,10 @@ class Db
|
|||||||
public static function get_dsn(): string {
|
public static function get_dsn(): string {
|
||||||
$db_port = Config::get(Config::DB_PORT) ? ';port=' . Config::get(Config::DB_PORT) : '';
|
$db_port = Config::get(Config::DB_PORT) ? ';port=' . Config::get(Config::DB_PORT) : '';
|
||||||
$db_host = Config::get(Config::DB_HOST) ? ';host=' . Config::get(Config::DB_HOST) : '';
|
$db_host = Config::get(Config::DB_HOST) ? ';host=' . Config::get(Config::DB_HOST) : '';
|
||||||
if (Config::get(Config::DB_TYPE) == "mysql" && Config::get(Config::MYSQL_CHARSET)) {
|
$db_sslmode = Config::get(Config::DB_SSLMODE);
|
||||||
$db_charset = ';charset=' . Config::get(Config::MYSQL_CHARSET);
|
|
||||||
} else {
|
|
||||||
$db_charset = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return Config::get(Config::DB_TYPE) . ':dbname=' . Config::get(Config::DB_NAME) . $db_host . $db_port . $db_charset;
|
return 'pgsql:dbname=' . Config::get(Config::DB_NAME) . $db_host . $db_port .
|
||||||
|
";sslmode=$db_sslmode";
|
||||||
}
|
}
|
||||||
|
|
||||||
// this really shouldn't be used unless a separate PDO connection is needed
|
// this really shouldn't be used unless a separate PDO connection is needed
|
||||||
@@ -56,20 +47,10 @@ class Db
|
|||||||
|
|
||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
$pdo->query("set client_encoding = 'UTF-8'");
|
||||||
|
$pdo->query("set datestyle = 'ISO, european'");
|
||||||
$pdo->query("set client_encoding = 'UTF-8'");
|
$pdo->query("set TIME ZONE 0");
|
||||||
$pdo->query("set datestyle = 'ISO, european'");
|
$pdo->query("set cpu_tuple_cost = 0.5");
|
||||||
$pdo->query("set TIME ZONE 0");
|
|
||||||
$pdo->query("set cpu_tuple_cost = 0.5");
|
|
||||||
|
|
||||||
} else if (Config::get(Config::DB_TYPE) == "mysql") {
|
|
||||||
$pdo->query("SET time_zone = '+0:0'");
|
|
||||||
|
|
||||||
if (Config::get(Config::MYSQL_CHARSET)) {
|
|
||||||
$pdo->query("SET NAMES " . Config::get(Config::MYSQL_CHARSET));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
@@ -92,11 +73,8 @@ class Db
|
|||||||
return self::$instance->pdo;
|
return self::$instance->pdo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated usages should be replaced with `RANDOM()` */
|
||||||
public static function sql_random_function(): string {
|
public static function sql_random_function(): string {
|
||||||
if (Config::get(Config::DB_TYPE) == "mysql") {
|
|
||||||
return "RAND()";
|
|
||||||
}
|
|
||||||
return "RANDOM()";
|
return "RANDOM()";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Db_Migrations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initialize(string $root_path, string $migrations_table, bool $base_is_latest = true, int $max_version_override = 0): void {
|
function initialize(string $root_path, string $migrations_table, bool $base_is_latest = true, int $max_version_override = 0): void {
|
||||||
$this->base_path = "$root_path/" . Config::get(Config::DB_TYPE);
|
$this->base_path = "$root_path/pgsql";
|
||||||
$this->migrations_path = $this->base_path . "/migrations";
|
$this->migrations_path = $this->base_path . "/migrations";
|
||||||
$this->migrations_table = $migrations_table;
|
$this->migrations_table = $migrations_table;
|
||||||
$this->base_is_latest = $base_is_latest;
|
$this->base_is_latest = $base_is_latest;
|
||||||
@@ -88,9 +88,7 @@ class Db_Migrations {
|
|||||||
$lines = $this->get_lines($version);
|
$lines = $this->get_lines($version);
|
||||||
|
|
||||||
if (count($lines) > 0) {
|
if (count($lines) > 0) {
|
||||||
// mysql doesn't support transactions for DDL statements
|
$this->pdo->beginTransaction();
|
||||||
if (Config::get(Config::DB_TYPE) != "mysql")
|
|
||||||
$this->pdo->beginTransaction();
|
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
Debug::log($line, Debug::LOG_EXTENDED);
|
Debug::log($line, Debug::LOG_EXTENDED);
|
||||||
@@ -107,8 +105,7 @@ class Db_Migrations {
|
|||||||
else
|
else
|
||||||
$this->set_version($version);
|
$this->set_version($version);
|
||||||
|
|
||||||
if (Config::get(Config::DB_TYPE) != "mysql")
|
$this->pdo->commit();
|
||||||
$this->pdo->commit();
|
|
||||||
|
|
||||||
Debug::log("Migration finished, current version: " . $this->get_version(), Debug::LOG_VERBOSE);
|
Debug::log("Migration finished, current version: " . $this->get_version(), Debug::LOG_VERBOSE);
|
||||||
|
|
||||||
@@ -190,7 +187,7 @@ class Db_Migrations {
|
|||||||
|
|
||||||
if (file_exists($filename)) {
|
if (file_exists($filename)) {
|
||||||
$lines = array_filter(preg_split("/[\r\n]/", file_get_contents($filename)),
|
$lines = array_filter(preg_split("/[\r\n]/", file_get_contents($filename)),
|
||||||
fn($line) => strlen(trim($line)) > 0 && strpos($line, "--") !== 0);
|
fn($line) => strlen(trim($line)) > 0 && !str_starts_with($line, "--"));
|
||||||
|
|
||||||
return array_filter(explode(";", implode("", $lines)),
|
return array_filter(explode(";", implode("", $lines)),
|
||||||
fn($line) => strlen(trim($line)) > 0 && !in_array(strtolower($line), ["begin", "commit"]));
|
fn($line) => strlen(trim($line)) > 0 && !in_array(strtolower($line), ["begin", "commit"]));
|
||||||
|
|||||||
@@ -2,17 +2,11 @@
|
|||||||
class Db_Prefs {
|
class Db_Prefs {
|
||||||
// this class is a stub for the time being (to be removed)
|
// this class is a stub for the time being (to be removed)
|
||||||
|
|
||||||
/**
|
function read(string $pref_name, ?int $user_id = null, bool $die_on_error = false): bool|int|null|string {
|
||||||
* @return bool|int|null|string
|
return Prefs::get($pref_name, $user_id ?: $_SESSION['uid'], $_SESSION['profile'] ?? null);
|
||||||
*/
|
|
||||||
function read(string $pref_name, ?int $user_id = null, bool $die_on_error = false) {
|
|
||||||
return get_pref($pref_name, $user_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function write(string $pref_name, mixed $value, ?int $user_id = null, bool $strip_tags = true): bool {
|
||||||
* @param mixed $value
|
return Prefs::set($pref_name, $value, $user_id ?: $_SESSION['uid'], $_SESSION['profile'] ?? null, $strip_tags);
|
||||||
*/
|
|
||||||
function write(string $pref_name, $value, ?int $user_id = null, bool $strip_tags = true): bool {
|
|
||||||
return set_pref($pref_name, $value, $user_id, $strip_tags);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class Debug {
|
|||||||
*/
|
*/
|
||||||
public static function map_loglevel(int $level) : int {
|
public static function map_loglevel(int $level) : int {
|
||||||
if (in_array($level, self::ALL_LOG_LEVELS)) {
|
if (in_array($level, self::ALL_LOG_LEVELS)) {
|
||||||
/** @phpstan-ignore-next-line */
|
/** @phpstan-ignore return.type (yes it is a Debug::LOG_* value) */
|
||||||
return $level;
|
return $level;
|
||||||
} else {
|
} else {
|
||||||
user_error("Passed invalid debug log level: $level", E_USER_WARNING);
|
user_error("Passed invalid debug log level: $level", E_USER_WARNING);
|
||||||
|
|||||||
@@ -2,28 +2,20 @@
|
|||||||
class Digest
|
class Digest
|
||||||
{
|
{
|
||||||
static function send_headlines_digests(): void {
|
static function send_headlines_digests(): void {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$user_limit = 15; // amount of users to process (e.g. emails to send out)
|
$user_limit = 15; // amount of users to process (e.g. emails to send out)
|
||||||
$limit = 1000; // maximum amount of headlines to include
|
$limit = 1000; // maximum amount of headlines to include
|
||||||
|
|
||||||
Debug::log("Sending digests, batch of max $user_limit users, headline limit = $limit");
|
Debug::log("Sending digests, batch of max $user_limit users, headline limit = $limit");
|
||||||
|
|
||||||
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
|
||||||
$interval_qpart = "last_digest_sent < NOW() - INTERVAL '1 days'";
|
|
||||||
} else /* if (Config::get(Config::DB_TYPE) == "mysql") */ {
|
|
||||||
$interval_qpart = "last_digest_sent < DATE_SUB(NOW(), INTERVAL 1 DAY)";
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
$res = $pdo->query("SELECT id, login, 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)");
|
WHERE email != '' AND (last_digest_sent IS NULL OR last_digest_sent < NOW() - INTERVAL '1 day')");
|
||||||
|
|
||||||
while ($line = $res->fetch()) {
|
while ($line = $res->fetch()) {
|
||||||
|
|
||||||
if (get_pref(Prefs::DIGEST_ENABLE, $line['id'])) {
|
if (Prefs::get(Prefs::DIGEST_ENABLE, $line['id'])) {
|
||||||
$preferred_ts = strtotime(get_pref(Prefs::DIGEST_PREFERRED_TIME, $line['id']) ?? '');
|
$preferred_ts = strtotime(Prefs::get(Prefs::DIGEST_PREFERRED_TIME, $line['id']) ?? '');
|
||||||
|
|
||||||
// try to send digests within 2 hours of preferred time
|
// try to send digests within 2 hours of preferred time
|
||||||
if ($preferred_ts && time() >= $preferred_ts &&
|
if ($preferred_ts && time() >= $preferred_ts &&
|
||||||
@@ -32,7 +24,7 @@ class Digest
|
|||||||
|
|
||||||
Debug::log("Sending digest for UID:" . $line['id'] . " - " . $line["email"]);
|
Debug::log("Sending digest for UID:" . $line['id'] . " - " . $line["email"]);
|
||||||
|
|
||||||
$do_catchup = get_pref(Prefs::DIGEST_CATCHUP, $line['id']);
|
$do_catchup = Prefs::get(Prefs::DIGEST_CATCHUP, $line['id']);
|
||||||
|
|
||||||
global $tz_offset;
|
global $tz_offset;
|
||||||
|
|
||||||
@@ -77,7 +69,6 @@ class Digest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
Debug::log("All done.");
|
Debug::log("All done.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,13 +100,6 @@ class Digest
|
|||||||
$tpl_t->setVariable('TTRSS_HOST', Config::get_self_url());
|
$tpl_t->setVariable('TTRSS_HOST', Config::get_self_url());
|
||||||
|
|
||||||
$affected_ids = array();
|
$affected_ids = array();
|
||||||
|
|
||||||
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
|
||||||
$interval_qpart = "ttrss_entries.date_updated > NOW() - INTERVAL '$days days'";
|
|
||||||
} else /* if (Config::get(Config::DB_TYPE) == "mysql") */ {
|
|
||||||
$interval_qpart = "ttrss_entries.date_updated > DATE_SUB(NOW(), INTERVAL $days DAY)";
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
|
|
||||||
$sth = $pdo->prepare("SELECT ttrss_entries.title,
|
$sth = $pdo->prepare("SELECT ttrss_entries.title,
|
||||||
@@ -126,7 +110,7 @@ class Digest
|
|||||||
link,
|
link,
|
||||||
score,
|
score,
|
||||||
content,
|
content,
|
||||||
".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
|
SUBSTRING_FOR_DATE(last_updated,1,19) AS last_updated
|
||||||
FROM
|
FROM
|
||||||
ttrss_user_entries,ttrss_entries,ttrss_feeds
|
ttrss_user_entries,ttrss_entries,ttrss_feeds
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -134,7 +118,7 @@ class Digest
|
|||||||
WHERE
|
WHERE
|
||||||
ref_id = ttrss_entries.id AND feed_id = ttrss_feeds.id
|
ref_id = ttrss_entries.id AND feed_id = ttrss_feeds.id
|
||||||
AND include_in_digest = true
|
AND include_in_digest = true
|
||||||
AND $interval_qpart
|
AND ttrss_entries.date_updated > NOW() - INTERVAL '$days day'
|
||||||
AND ttrss_user_entries.owner_uid = :user_id
|
AND ttrss_user_entries.owner_uid = :user_id
|
||||||
AND unread = true
|
AND unread = true
|
||||||
AND score >= :min_score
|
AND score >= :min_score
|
||||||
@@ -156,17 +140,16 @@ class Digest
|
|||||||
|
|
||||||
array_push($affected_ids, $line["ref_id"]);
|
array_push($affected_ids, $line["ref_id"]);
|
||||||
|
|
||||||
$updated = TimeHelper::make_local_datetime($line['last_updated'], false,
|
$updated = TimeHelper::make_local_datetime($line['last_updated'], owner_uid: $user_id);
|
||||||
$user_id);
|
|
||||||
|
|
||||||
if (get_pref(Prefs::ENABLE_FEED_CATS, $user_id)) {
|
if (Prefs::get(Prefs::ENABLE_FEED_CATS, $user_id)) {
|
||||||
$line['feed_title'] = $line['cat_title'] . " / " . $line['feed_title'];
|
$line['feed_title'] = $line['cat_title'] . " / " . $line['feed_title'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$article_labels = Article::_get_labels($line["ref_id"], $user_id);
|
$article_labels = Article::_get_labels($line["ref_id"], $user_id);
|
||||||
$article_labels_formatted = "";
|
$article_labels_formatted = "";
|
||||||
|
|
||||||
if (is_array($article_labels) && count($article_labels) > 0) {
|
if (count($article_labels) > 0) {
|
||||||
$article_labels_formatted = implode(", ", array_map(fn($a) => $a[1], $article_labels));
|
$article_labels_formatted = implode(", ", array_map(fn($a) => $a[1], $article_labels));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
class DiskCache implements Cache_Adapter {
|
class DiskCache implements Cache_Adapter {
|
||||||
/** @var Cache_Adapter $adapter */
|
private Cache_Adapter $adapter;
|
||||||
private $adapter;
|
|
||||||
|
|
||||||
/** @var array<string, DiskCache> $instances */
|
/** @var array<string, DiskCache> $instances */
|
||||||
private static $instances = [];
|
private static array $instances = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://stackoverflow.com/a/53662733
|
* https://stackoverflow.com/a/53662733
|
||||||
@@ -221,11 +220,7 @@ class DiskCache implements Cache_Adapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function remove(string $filename): bool {
|
public function remove(string $filename): bool {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
$span->setAttribute('file.name', $filename);
|
|
||||||
|
|
||||||
$rc = $this->adapter->remove($filename);
|
$rc = $this->adapter->remove($filename);
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return $rc;
|
return $rc;
|
||||||
}
|
}
|
||||||
@@ -251,9 +246,6 @@ class DiskCache implements Cache_Adapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function exists(string $filename): bool {
|
public function exists(string $filename): bool {
|
||||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
|
||||||
$span->addEvent("DiskCache::exists: $filename");
|
|
||||||
|
|
||||||
$rc = $this->adapter->exists(basename($filename));
|
$rc = $this->adapter->exists(basename($filename));
|
||||||
|
|
||||||
return $rc;
|
return $rc;
|
||||||
@@ -263,11 +255,7 @@ class DiskCache implements Cache_Adapter {
|
|||||||
* @return int|false -1 if the file doesn't exist, false if an error occurred, size in bytes otherwise
|
* @return int|false -1 if the file doesn't exist, false if an error occurred, size in bytes otherwise
|
||||||
*/
|
*/
|
||||||
public function get_size(string $filename) {
|
public function get_size(string $filename) {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
$span->setAttribute('file.name', $filename);
|
|
||||||
|
|
||||||
$rc = $this->adapter->get_size(basename($filename));
|
$rc = $this->adapter->get_size(basename($filename));
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return $rc;
|
return $rc;
|
||||||
}
|
}
|
||||||
@@ -278,11 +266,7 @@ class DiskCache implements Cache_Adapter {
|
|||||||
* @return int|false Bytes written or false if an error occurred.
|
* @return int|false Bytes written or false if an error occurred.
|
||||||
*/
|
*/
|
||||||
public function put(string $filename, $data) {
|
public function put(string $filename, $data) {
|
||||||
$span = Tracer::start(__METHOD__);
|
return $this->adapter->put(basename($filename), $data);
|
||||||
$rc = $this->adapter->put(basename($filename), $data);
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return $rc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated we can't assume cached files are local, and other storages
|
/** @deprecated we can't assume cached files are local, and other storages
|
||||||
@@ -316,8 +300,11 @@ class DiskCache implements Cache_Adapter {
|
|||||||
if ($this->exists($local_filename) && !$force)
|
if ($this->exists($local_filename) && !$force)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
$data = UrlHelper::fetch(array_merge(["url" => $url,
|
$data = UrlHelper::fetch([
|
||||||
"max_size" => Config::get(Config::MAX_CACHE_FILE_SIZE)], $options));
|
'url' => $url,
|
||||||
|
'max_size' => Config::get(Config::MAX_CACHE_FILE_SIZE),
|
||||||
|
...$options,
|
||||||
|
]);
|
||||||
|
|
||||||
if ($data)
|
if ($data)
|
||||||
return $this->put($local_filename, $data) > 0;
|
return $this->put($local_filename, $data) > 0;
|
||||||
@@ -326,17 +313,12 @@ class DiskCache implements Cache_Adapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function send(string $filename) {
|
public function send(string $filename) {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
$span->setAttribute('file.name', $filename);
|
|
||||||
|
|
||||||
$filename = basename($filename);
|
$filename = basename($filename);
|
||||||
|
|
||||||
if (!$this->exists($filename)) {
|
if (!$this->exists($filename)) {
|
||||||
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
|
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
|
||||||
echo "File not found.";
|
echo "File not found.";
|
||||||
|
|
||||||
$span->setAttribute('error', '404 not found');
|
|
||||||
$span->end();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,8 +328,6 @@ class DiskCache implements Cache_Adapter {
|
|||||||
if (($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '') == $gmt_modified || ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') == $file_mtime) {
|
if (($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '') == $gmt_modified || ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') == $file_mtime) {
|
||||||
header('HTTP/1.1 304 Not Modified');
|
header('HTTP/1.1 304 Not Modified');
|
||||||
|
|
||||||
$span->setAttribute('error', '304 not modified');
|
|
||||||
$span->end();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,9 +345,6 @@ class DiskCache implements Cache_Adapter {
|
|||||||
header("Content-type: text/plain");
|
header("Content-type: text/plain");
|
||||||
|
|
||||||
print "Stored file has disallowed content type ($mimetype)";
|
print "Stored file has disallowed content type ($mimetype)";
|
||||||
|
|
||||||
$span->setAttribute('error', '400 disallowed content type');
|
|
||||||
$span->end();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,13 +366,7 @@ class DiskCache implements Cache_Adapter {
|
|||||||
|
|
||||||
header_remove("Pragma");
|
header_remove("Pragma");
|
||||||
|
|
||||||
$span->setAttribute('mimetype', $mimetype);
|
return $this->adapter->send($filename);
|
||||||
|
|
||||||
$rc = $this->adapter->send($filename);
|
|
||||||
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return $rc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_full_path(string $filename): string {
|
public function get_full_path(string $filename): string {
|
||||||
@@ -424,13 +395,9 @@ class DiskCache implements Cache_Adapter {
|
|||||||
// plugins work on original source URLs used before caching
|
// plugins work on original source URLs used before caching
|
||||||
// NOTE: URLs should be already absolutized because this is called after sanitize()
|
// NOTE: URLs should be already absolutized because this is called after sanitize()
|
||||||
static public function rewrite_urls(string $str): string {
|
static public function rewrite_urls(string $str): string {
|
||||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
|
||||||
$span->addEvent("DiskCache::rewrite_urls");
|
|
||||||
|
|
||||||
$res = trim($str);
|
$res = trim($str);
|
||||||
|
|
||||||
if (!$res) {
|
if (!$res) {
|
||||||
$span->end();
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,13 +406,12 @@ class DiskCache implements Cache_Adapter {
|
|||||||
$xpath = new DOMXPath($doc);
|
$xpath = new DOMXPath($doc);
|
||||||
$cache = DiskCache::instance("images");
|
$cache = DiskCache::instance("images");
|
||||||
|
|
||||||
$entries = $xpath->query('(//img[@src]|//source[@src|@srcset]|//video[@poster|@src])');
|
|
||||||
|
|
||||||
$need_saving = false;
|
$need_saving = false;
|
||||||
|
|
||||||
foreach ($entries as $entry) {
|
$entries = $xpath->query('(//img[@src]|//source[@src|@srcset]|//video[@poster|@src])');
|
||||||
$span->addEvent("entry: " . $entry->tagName);
|
|
||||||
|
|
||||||
|
/** @var DOMElement $entry */
|
||||||
|
foreach ($entries as $entry) {
|
||||||
foreach (array('src', 'poster') as $attr) {
|
foreach (array('src', 'poster') as $attr) {
|
||||||
if ($entry->hasAttribute($attr)) {
|
if ($entry->hasAttribute($attr)) {
|
||||||
$url = $entry->getAttribute($attr);
|
$url = $entry->getAttribute($attr);
|
||||||
|
|||||||
@@ -1,21 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
class FeedEnclosure {
|
class FeedEnclosure {
|
||||||
/** @var string */
|
public string $link = '';
|
||||||
public $link;
|
public string $type = '';
|
||||||
|
public string $length = '';
|
||||||
/** @var string */
|
public string $title = '';
|
||||||
public $type;
|
public string $height = '';
|
||||||
|
public string $width = '';
|
||||||
/** @var string */
|
|
||||||
public $length;
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
public $title;
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
public $height;
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
public $width;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ abstract class FeedItem {
|
|||||||
abstract function get_id(): string;
|
abstract function get_id(): string;
|
||||||
|
|
||||||
/** @return int|false a timestamp on success, false otherwise */
|
/** @return int|false a timestamp on success, false otherwise */
|
||||||
abstract function get_date();
|
abstract function get_date(): false|int;
|
||||||
|
|
||||||
abstract function get_link(): string;
|
abstract function get_link(): string;
|
||||||
abstract function get_title(): string;
|
abstract function get_title(): string;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class FeedItem_Atom extends FeedItem_Common {
|
|||||||
/**
|
/**
|
||||||
* @return int|false a timestamp on success, false otherwise
|
* @return int|false a timestamp on success, false otherwise
|
||||||
*/
|
*/
|
||||||
function get_date() {
|
function get_date(): false|int {
|
||||||
$updated = $this->elem->getElementsByTagName("updated")->item(0);
|
$updated = $this->elem->getElementsByTagName("updated")->item(0);
|
||||||
|
|
||||||
if ($updated) {
|
if ($updated) {
|
||||||
@@ -43,7 +43,6 @@ class FeedItem_Atom extends FeedItem_Common {
|
|||||||
$links = $this->elem->getElementsByTagName("link");
|
$links = $this->elem->getElementsByTagName("link");
|
||||||
|
|
||||||
foreach ($links as $link) {
|
foreach ($links as $link) {
|
||||||
/** @phpstan-ignore-next-line */
|
|
||||||
if ($link->hasAttribute("href") &&
|
if ($link->hasAttribute("href") &&
|
||||||
(!$link->hasAttribute("rel")
|
(!$link->hasAttribute("rel")
|
||||||
|| $link->getAttribute("rel") == "alternate"
|
|| $link->getAttribute("rel") == "alternate"
|
||||||
@@ -81,6 +80,7 @@ class FeedItem_Atom extends FeedItem_Common {
|
|||||||
|
|
||||||
$elems = $tmpxpath->query("(//*[@href]|//*[@src])");
|
$elems = $tmpxpath->query("(//*[@href]|//*[@src])");
|
||||||
|
|
||||||
|
/** @var DOMElement $elem */
|
||||||
foreach ($elems as $elem) {
|
foreach ($elems as $elem) {
|
||||||
if ($elem->hasAttribute("href")) {
|
if ($elem->hasAttribute("href")) {
|
||||||
$elem->setAttribute("href",
|
$elem->setAttribute("href",
|
||||||
@@ -181,16 +181,14 @@ class FeedItem_Atom extends FeedItem_Common {
|
|||||||
$encs = [];
|
$encs = [];
|
||||||
|
|
||||||
foreach ($links as $link) {
|
foreach ($links as $link) {
|
||||||
/** @phpstan-ignore-next-line */
|
|
||||||
if ($link->hasAttribute("href") && $link->hasAttribute("rel")) {
|
if ($link->hasAttribute("href") && $link->hasAttribute("rel")) {
|
||||||
$base = $this->xpath->evaluate("string(ancestor-or-self::*[@xml:base][1]/@xml:base)", $link);
|
$base = $this->xpath->evaluate("string(ancestor-or-self::*[@xml:base][1]/@xml:base)", $link);
|
||||||
|
|
||||||
if ($link->getAttribute("rel") == "enclosure") {
|
if ($link->getAttribute("rel") == "enclosure") {
|
||||||
$enc = new FeedEnclosure();
|
$enc = new FeedEnclosure();
|
||||||
|
$enc->type = clean($link->getAttribute('type'));
|
||||||
$enc->type = clean($link->getAttribute("type"));
|
$enc->length = clean($link->getAttribute('length'));
|
||||||
$enc->length = clean($link->getAttribute("length"));
|
$enc->link = clean($link->getAttribute('href'));
|
||||||
$enc->link = clean($link->getAttribute("href"));
|
|
||||||
|
|
||||||
if (!empty($base)) {
|
if (!empty($base)) {
|
||||||
$enc->link = UrlHelper::rewrite_relative($base, $enc->link);
|
$enc->link = UrlHelper::rewrite_relative($base, $enc->link);
|
||||||
@@ -213,6 +211,7 @@ class FeedItem_Atom extends FeedItem_Common {
|
|||||||
return clean($lang);
|
return clean($lang);
|
||||||
} else {
|
} else {
|
||||||
// Fall back to the language declared on the feed, if any.
|
// Fall back to the language declared on the feed, if any.
|
||||||
|
/** @var DOMElement|DOMNode $child */
|
||||||
foreach ($this->doc->childNodes as $child) {
|
foreach ($this->doc->childNodes as $child) {
|
||||||
if (method_exists($child, "getAttributeNS")) {
|
if (method_exists($child, "getAttributeNS")) {
|
||||||
return clean($child->getAttributeNS(self::NS_XML, "lang"));
|
return clean($child->getAttributeNS(self::NS_XML, "lang"));
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
abstract class FeedItem_Common extends FeedItem {
|
abstract class FeedItem_Common extends FeedItem {
|
||||||
/** @var DOMElement */
|
protected readonly DOMElement $elem;
|
||||||
protected $elem;
|
protected readonly DOMDocument $doc;
|
||||||
|
protected readonly DOMXPath $xpath;
|
||||||
/** @var DOMDocument */
|
|
||||||
protected $doc;
|
|
||||||
|
|
||||||
/** @var DOMXPath */
|
|
||||||
protected $xpath;
|
|
||||||
|
|
||||||
function __construct(DOMElement $elem, DOMDocument $doc, DOMXPath $xpath) {
|
function __construct(DOMElement $elem, DOMDocument $doc, DOMXPath $xpath) {
|
||||||
$this->elem = $elem;
|
$this->elem = $elem;
|
||||||
$this->xpath = $xpath;
|
|
||||||
$this->doc = $doc;
|
$this->doc = $doc;
|
||||||
|
$this->xpath = $xpath;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$source = $elem->getElementsByTagName("source")->item(0);
|
$source = $elem->getElementsByTagName("source")->item(0);
|
||||||
@@ -89,6 +84,7 @@ abstract class FeedItem_Common extends FeedItem {
|
|||||||
/**
|
/**
|
||||||
* this is common for both Atom and RSS types and deals with various 'media:' elements
|
* this is common for both Atom and RSS types and deals with various 'media:' elements
|
||||||
*
|
*
|
||||||
|
* @see https://www.rssboard.org/media-rss
|
||||||
* @return array<int, FeedEnclosure>
|
* @return array<int, FeedEnclosure>
|
||||||
*/
|
*/
|
||||||
function get_enclosures(): array {
|
function get_enclosures(): array {
|
||||||
@@ -96,14 +92,14 @@ abstract class FeedItem_Common extends FeedItem {
|
|||||||
|
|
||||||
$enclosures = $this->xpath->query("media:content", $this->elem);
|
$enclosures = $this->xpath->query("media:content", $this->elem);
|
||||||
|
|
||||||
|
/** @var DOMElement $enclosure */
|
||||||
foreach ($enclosures as $enclosure) {
|
foreach ($enclosures as $enclosure) {
|
||||||
$enc = new FeedEnclosure();
|
$enc = new FeedEnclosure();
|
||||||
|
$enc->type = clean($enclosure->getAttribute('type'));
|
||||||
$enc->type = clean($enclosure->getAttribute("type"));
|
$enc->link = clean($enclosure->getAttribute('url'));
|
||||||
$enc->link = clean($enclosure->getAttribute("url"));
|
$enc->length = clean($enclosure->getAttribute('length'));
|
||||||
$enc->length = clean($enclosure->getAttribute("length"));
|
$enc->height = clean($enclosure->getAttribute('height'));
|
||||||
$enc->height = clean($enclosure->getAttribute("height"));
|
$enc->width = clean($enclosure->getAttribute('width'));
|
||||||
$enc->width = clean($enclosure->getAttribute("width"));
|
|
||||||
|
|
||||||
$medium = clean($enclosure->getAttribute("medium"));
|
$medium = clean($enclosure->getAttribute("medium"));
|
||||||
if (!$enc->type && $medium) {
|
if (!$enc->type && $medium) {
|
||||||
@@ -119,17 +115,16 @@ abstract class FeedItem_Common extends FeedItem {
|
|||||||
$enclosures = $this->xpath->query("media:group", $this->elem);
|
$enclosures = $this->xpath->query("media:group", $this->elem);
|
||||||
|
|
||||||
foreach ($enclosures as $enclosure) {
|
foreach ($enclosures as $enclosure) {
|
||||||
$enc = new FeedEnclosure();
|
|
||||||
|
|
||||||
/** @var DOMElement|null */
|
/** @var DOMElement|null */
|
||||||
$content = $this->xpath->query("media:content", $enclosure)->item(0);
|
$content = $this->xpath->query("media:content", $enclosure)->item(0);
|
||||||
|
|
||||||
if ($content) {
|
if ($content) {
|
||||||
$enc->type = clean($content->getAttribute("type"));
|
$enc = new FeedEnclosure();
|
||||||
$enc->link = clean($content->getAttribute("url"));
|
$enc->type = clean($content->getAttribute('type'));
|
||||||
$enc->length = clean($content->getAttribute("length"));
|
$enc->link = clean($content->getAttribute('url'));
|
||||||
$enc->height = clean($content->getAttribute("height"));
|
$enc->length = clean($content->getAttribute('length'));
|
||||||
$enc->width = clean($content->getAttribute("width"));
|
$enc->height = clean($content->getAttribute('height'));
|
||||||
|
$enc->width = clean($content->getAttribute('width'));
|
||||||
|
|
||||||
$medium = clean($content->getAttribute("medium"));
|
$medium = clean($content->getAttribute("medium"));
|
||||||
if (!$enc->type && $medium) {
|
if (!$enc->type && $medium) {
|
||||||
@@ -148,15 +143,15 @@ abstract class FeedItem_Common extends FeedItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$enclosures = $this->xpath->query("media:thumbnail", $this->elem);
|
$enclosures = $this->xpath->query("(.|media:content|media:group|media:group/media:content)/media:thumbnail", $this->elem);
|
||||||
|
|
||||||
|
/** @var DOMElement $enclosure */
|
||||||
foreach ($enclosures as $enclosure) {
|
foreach ($enclosures as $enclosure) {
|
||||||
$enc = new FeedEnclosure();
|
$enc = new FeedEnclosure();
|
||||||
|
$enc->type = 'image/generic';
|
||||||
$enc->type = "image/generic";
|
$enc->link = clean($enclosure->getAttribute('url'));
|
||||||
$enc->link = clean($enclosure->getAttribute("url"));
|
$enc->height = clean($enclosure->getAttribute('height'));
|
||||||
$enc->height = clean($enclosure->getAttribute("height"));
|
$enc->width = clean($enclosure->getAttribute('width'));
|
||||||
$enc->width = clean($enclosure->getAttribute("width"));
|
|
||||||
|
|
||||||
array_push($encs, $enc);
|
array_push($encs, $enc);
|
||||||
}
|
}
|
||||||
@@ -171,7 +166,7 @@ abstract class FeedItem_Common extends FeedItem {
|
|||||||
/**
|
/**
|
||||||
* @return false|string false on failure, otherwise string contents
|
* @return false|string false on failure, otherwise string contents
|
||||||
*/
|
*/
|
||||||
function subtree_or_text(DOMElement $node) {
|
function subtree_or_text(DOMElement $node): false|string {
|
||||||
if ($this->count_children($node) == 0) {
|
if ($this->count_children($node) == 0) {
|
||||||
return $node->nodeValue;
|
return $node->nodeValue;
|
||||||
} else {
|
} else {
|
||||||
@@ -201,10 +196,6 @@ abstract class FeedItem_Common extends FeedItem {
|
|||||||
|
|
||||||
$cat = preg_replace('/[,\'\"]/', "", $cat);
|
$cat = preg_replace('/[,\'\"]/', "", $cat);
|
||||||
|
|
||||||
if (Config::get(Config::DB_TYPE) == "mysql") {
|
|
||||||
$cat = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $cat);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mb_strlen($cat) > 250)
|
if (mb_strlen($cat) > 250)
|
||||||
$cat = mb_substr($cat, 0, 250);
|
$cat = mb_substr($cat, 0, 250);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class FeedItem_RSS extends FeedItem_Common {
|
|||||||
/**
|
/**
|
||||||
* @return int|false a timestamp on success, false otherwise
|
* @return int|false a timestamp on success, false otherwise
|
||||||
*/
|
*/
|
||||||
function get_date() {
|
function get_date(): false|int {
|
||||||
$pubDate = $this->elem->getElementsByTagName("pubDate")->item(0);
|
$pubDate = $this->elem->getElementsByTagName("pubDate")->item(0);
|
||||||
|
|
||||||
if ($pubDate) {
|
if ($pubDate) {
|
||||||
@@ -33,8 +33,9 @@ class FeedItem_RSS extends FeedItem_Common {
|
|||||||
function get_link(): string {
|
function get_link(): string {
|
||||||
$links = $this->xpath->query("atom:link", $this->elem);
|
$links = $this->xpath->query("atom:link", $this->elem);
|
||||||
|
|
||||||
|
/** @var DOMElement $link */
|
||||||
foreach ($links as $link) {
|
foreach ($links as $link) {
|
||||||
if ($link && $link->hasAttribute("href") &&
|
if ($link->hasAttribute("href") &&
|
||||||
(!$link->hasAttribute("rel")
|
(!$link->hasAttribute("rel")
|
||||||
|| $link->getAttribute("rel") == "alternate"
|
|| $link->getAttribute("rel") == "alternate"
|
||||||
|| $link->getAttribute("rel") == "standout")) {
|
|| $link->getAttribute("rel") == "standout")) {
|
||||||
@@ -142,12 +143,11 @@ class FeedItem_RSS extends FeedItem_Common {
|
|||||||
|
|
||||||
foreach ($enclosures as $enclosure) {
|
foreach ($enclosures as $enclosure) {
|
||||||
$enc = new FeedEnclosure();
|
$enc = new FeedEnclosure();
|
||||||
|
$enc->type = clean($enclosure->getAttribute('type'));
|
||||||
$enc->type = clean($enclosure->getAttribute("type"));
|
$enc->link = clean($enclosure->getAttribute('url'));
|
||||||
$enc->link = clean($enclosure->getAttribute("url"));
|
$enc->length = clean($enclosure->getAttribute('length'));
|
||||||
$enc->length = clean($enclosure->getAttribute("length"));
|
$enc->height = clean($enclosure->getAttribute('height'));
|
||||||
$enc->height = clean($enclosure->getAttribute("height"));
|
$enc->width = clean($enclosure->getAttribute('width'));
|
||||||
$enc->width = clean($enclosure->getAttribute("width"));
|
|
||||||
|
|
||||||
array_push($encs, $enc);
|
array_push($encs, $enc);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,25 @@
|
|||||||
<?php
|
<?php
|
||||||
class FeedParser {
|
class FeedParser {
|
||||||
|
private DOMDocument $doc;
|
||||||
|
|
||||||
/** @var DOMDocument */
|
private ?string $error = null;
|
||||||
private $doc;
|
|
||||||
|
|
||||||
/** @var string|null */
|
|
||||||
private $error = null;
|
|
||||||
|
|
||||||
/** @var array<string> */
|
/** @var array<string> */
|
||||||
private $libxml_errors = [];
|
private array $libxml_errors = [];
|
||||||
|
|
||||||
/** @var array<FeedItem> */
|
/** @var array<FeedItem> */
|
||||||
private $items = [];
|
private array $items = [];
|
||||||
|
|
||||||
/** @var string|null */
|
private ?string $link = null;
|
||||||
private $link;
|
|
||||||
|
|
||||||
/** @var string|null */
|
private ?string $title = null;
|
||||||
private $title;
|
|
||||||
|
|
||||||
/** @var FeedParser::FEED_*|null */
|
/** @var FeedParser::FEED_* */
|
||||||
private $type;
|
private int $type;
|
||||||
|
|
||||||
/** @var DOMXPath|null */
|
private ?DOMXPath $xpath = null;
|
||||||
private $xpath;
|
|
||||||
|
|
||||||
|
const FEED_UNKNOWN = -1;
|
||||||
const FEED_RDF = 0;
|
const FEED_RDF = 0;
|
||||||
const FEED_RSS = 1;
|
const FEED_RSS = 1;
|
||||||
const FEED_ATOM = 2;
|
const FEED_ATOM = 2;
|
||||||
@@ -32,6 +27,9 @@ class FeedParser {
|
|||||||
function __construct(string $data) {
|
function __construct(string $data) {
|
||||||
libxml_use_internal_errors(true);
|
libxml_use_internal_errors(true);
|
||||||
libxml_clear_errors();
|
libxml_clear_errors();
|
||||||
|
|
||||||
|
$this->type = $this::FEED_UNKNOWN;
|
||||||
|
|
||||||
$this->doc = new DOMDocument();
|
$this->doc = new DOMDocument();
|
||||||
$this->doc->loadXML($data);
|
$this->doc->loadXML($data);
|
||||||
|
|
||||||
@@ -48,75 +46,54 @@ class FeedParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
libxml_clear_errors();
|
libxml_clear_errors();
|
||||||
|
|
||||||
|
if ($this->error)
|
||||||
|
return;
|
||||||
|
|
||||||
|
$this->xpath = new DOMXPath($this->doc);
|
||||||
|
$this->xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
|
||||||
|
$this->xpath->registerNamespace('atom03', 'http://purl.org/atom/ns#');
|
||||||
|
$this->xpath->registerNamespace('media', 'http://search.yahoo.com/mrss/');
|
||||||
|
$this->xpath->registerNamespace('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
|
||||||
|
$this->xpath->registerNamespace('slash', 'http://purl.org/rss/1.0/modules/slash/');
|
||||||
|
$this->xpath->registerNamespace('dc', 'http://purl.org/dc/elements/1.1/');
|
||||||
|
$this->xpath->registerNamespace('content', 'http://purl.org/rss/1.0/modules/content/');
|
||||||
|
$this->xpath->registerNamespace('thread', 'http://purl.org/syndication/thread/1.0');
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() : void {
|
/**
|
||||||
$xpath = new DOMXPath($this->doc);
|
* @return bool false if initialization couldn't occur (e.g. parsing error or unrecognized feed type), otherwise true
|
||||||
$xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
|
*/
|
||||||
$xpath->registerNamespace('atom03', 'http://purl.org/atom/ns#');
|
function init(): bool {
|
||||||
$xpath->registerNamespace('media', 'http://search.yahoo.com/mrss/');
|
if ($this->error)
|
||||||
$xpath->registerNamespace('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
|
return false;
|
||||||
$xpath->registerNamespace('slash', 'http://purl.org/rss/1.0/modules/slash/');
|
|
||||||
$xpath->registerNamespace('dc', 'http://purl.org/dc/elements/1.1/');
|
|
||||||
$xpath->registerNamespace('content', 'http://purl.org/rss/1.0/modules/content/');
|
|
||||||
$xpath->registerNamespace('thread', 'http://purl.org/syndication/thread/1.0');
|
|
||||||
|
|
||||||
$this->xpath = $xpath;
|
$type = $this->get_type();
|
||||||
|
|
||||||
$root_list = $xpath->query("(//atom03:feed|//atom:feed|//channel|//rdf:rdf|//rdf:RDF)");
|
if ($type === self::FEED_UNKNOWN)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (!empty($root_list) && $root_list->length > 0) {
|
$xpath = $this->xpath;
|
||||||
|
|
||||||
/** @var DOMElement|null $root */
|
switch ($type) {
|
||||||
$root = $root_list->item(0);
|
|
||||||
|
|
||||||
if ($root) {
|
|
||||||
switch (mb_strtolower($root->tagName)) {
|
|
||||||
case "rdf:rdf":
|
|
||||||
$this->type = $this::FEED_RDF;
|
|
||||||
break;
|
|
||||||
case "channel":
|
|
||||||
$this->type = $this::FEED_RSS;
|
|
||||||
break;
|
|
||||||
case "feed":
|
|
||||||
case "atom:feed":
|
|
||||||
$this->type = $this::FEED_ATOM;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$this->error ??= "Unknown/unsupported feed type";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($this->type) {
|
|
||||||
case $this::FEED_ATOM:
|
case $this::FEED_ATOM:
|
||||||
|
$title = $xpath->query('//atom:feed/atom:title')->item(0)
|
||||||
$title = $xpath->query("//atom:feed/atom:title")->item(0);
|
?? $xpath->query('//atom03:feed/atom03:title')->item(0);
|
||||||
|
|
||||||
if (!$title)
|
|
||||||
$title = $xpath->query("//atom03:feed/atom03:title")->item(0);
|
|
||||||
|
|
||||||
|
|
||||||
if ($title) {
|
if ($title) {
|
||||||
$this->title = $title->nodeValue;
|
$this->title = $title->nodeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$link = $xpath->query("//atom:feed/atom:link[not(@rel)]")->item(0);
|
|
||||||
|
|
||||||
if (!$link)
|
|
||||||
$link = $xpath->query("//atom:feed/atom:link[@rel='alternate']")->item(0);
|
|
||||||
|
|
||||||
if (!$link)
|
|
||||||
$link = $xpath->query("//atom03:feed/atom03:link[not(@rel)]")->item(0);
|
|
||||||
|
|
||||||
if (!$link)
|
|
||||||
$link = $xpath->query("//atom03:feed/atom03:link[@rel='alternate']")->item(0);
|
|
||||||
|
|
||||||
/** @var DOMElement|null $link */
|
/** @var DOMElement|null $link */
|
||||||
if ($link && $link->hasAttributes()) {
|
$link = $xpath->query('//atom:feed/atom:link[not(@rel)]')->item(0)
|
||||||
$this->link = $link->getAttribute("href");
|
?? $xpath->query("//atom:feed/atom:link[@rel='alternate']")->item(0)
|
||||||
}
|
?? $xpath->query('//atom03:feed/atom03:link[not(@rel)]')->item(0)
|
||||||
|
?? $xpath->query("//atom03:feed/atom03:link[@rel='alternate']")->item(0);
|
||||||
|
|
||||||
|
if ($link?->getAttribute('href'))
|
||||||
|
$this->link = $link->getAttribute('href');
|
||||||
|
|
||||||
$articles = $xpath->query("//atom:entry");
|
$articles = $xpath->query("//atom:entry");
|
||||||
|
|
||||||
@@ -174,16 +151,15 @@ class FeedParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->title) $this->title = trim($this->title);
|
|
||||||
if ($this->link) $this->link = trim($this->link);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
$this->error ??= "Unknown/unsupported feed type";
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->title)
|
||||||
|
$this->title = trim($this->title);
|
||||||
|
|
||||||
|
if ($this->link)
|
||||||
|
$this->link = trim($this->link);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated use Errors::format_libxml_error() instead */
|
/** @deprecated use Errors::format_libxml_error() instead */
|
||||||
@@ -201,6 +177,33 @@ class FeedParser {
|
|||||||
return $this->libxml_errors;
|
return $this->libxml_errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return FeedParser::FEED_*
|
||||||
|
*/
|
||||||
|
function get_type(): int {
|
||||||
|
if ($this->type !== self::FEED_UNKNOWN || $this->error)
|
||||||
|
return $this->type;
|
||||||
|
|
||||||
|
$root_list = $this->xpath->query('(//atom03:feed|//atom:feed|//channel|//rdf:rdf|//rdf:RDF)');
|
||||||
|
|
||||||
|
if ($root_list && $root_list->length > 0) {
|
||||||
|
/** @var DOMElement $root */
|
||||||
|
$root = $root_list->item(0);
|
||||||
|
|
||||||
|
$this->type = match (mb_strtolower($root->tagName)) {
|
||||||
|
'rdf:rdf' => self::FEED_RDF,
|
||||||
|
'channel' => self::FEED_RSS,
|
||||||
|
'feed', 'atom:feed' => self::FEED_ATOM,
|
||||||
|
default => self::FEED_UNKNOWN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->type === self::FEED_UNKNOWN)
|
||||||
|
$this->error ??= 'Unknown/unsupported feed type';
|
||||||
|
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
function get_link() : string {
|
function get_link() : string {
|
||||||
return clean($this->link ?? '');
|
return clean($this->link ?? '');
|
||||||
}
|
}
|
||||||
@@ -222,6 +225,7 @@ class FeedParser {
|
|||||||
case $this::FEED_ATOM:
|
case $this::FEED_ATOM:
|
||||||
$links = $this->xpath->query("//atom:feed/atom:link");
|
$links = $this->xpath->query("//atom:feed/atom:link");
|
||||||
|
|
||||||
|
/** @var DOMElement $link */
|
||||||
foreach ($links as $link) {
|
foreach ($links as $link) {
|
||||||
if (!$rel || $link->hasAttribute('rel') && $link->getAttribute('rel') == $rel) {
|
if (!$rel || $link->hasAttribute('rel') && $link->getAttribute('rel') == $rel) {
|
||||||
array_push($rv, clean(trim($link->getAttribute('href'))));
|
array_push($rv, clean(trim($link->getAttribute('href'))));
|
||||||
@@ -231,6 +235,7 @@ class FeedParser {
|
|||||||
case $this::FEED_RSS:
|
case $this::FEED_RSS:
|
||||||
$links = $this->xpath->query("//atom:link");
|
$links = $this->xpath->query("//atom:link");
|
||||||
|
|
||||||
|
/** @var DOMElement $link */
|
||||||
foreach ($links as $link) {
|
foreach ($links as $link) {
|
||||||
if (!$rel || $link->hasAttribute('rel') && $link->getAttribute('rel') == $rel) {
|
if (!$rel || $link->hasAttribute('rel') && $link->getAttribute('rel') == $rel) {
|
||||||
array_push($rv, clean(trim($link->getAttribute('href'))));
|
array_push($rv, clean(trim($link->getAttribute('href'))));
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,23 +8,27 @@ class Handler_Public extends Handler {
|
|||||||
int $limit, int $offset, string $search, string $view_mode = "",
|
int $limit, int $offset, string $search, string $view_mode = "",
|
||||||
string $format = 'atom', string $order = "", string $orig_guid = "", string $start_ts = ""): void {
|
string $format = 'atom', string $order = "", string $orig_guid = "", string $start_ts = ""): void {
|
||||||
|
|
||||||
$note_style = "background-color : #fff7d5;
|
// fail early if the requested format isn't recognized
|
||||||
border-width : 1px; ".
|
if (!in_array($format, ['atom', 'json'])) {
|
||||||
"padding : 5px; border-style : dashed; border-color : #e7d796;".
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
"margin-bottom : 1em; color : #9a8c59;";
|
print "Unknown format: $format.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$limit) $limit = 60;
|
$note_style = 'color: #9a8c59; background-color: #fff7d5; '
|
||||||
|
. 'border: 1px dashed #e7d796; padding: 5px; margin-bottom: 1em;';
|
||||||
|
|
||||||
|
if (!$limit)
|
||||||
|
$limit = 60;
|
||||||
|
|
||||||
list($override_order, $skip_first_id_check) = Feeds::_order_to_override_query($order);
|
list($override_order, $skip_first_id_check) = Feeds::_order_to_override_query($order);
|
||||||
|
|
||||||
if (!$override_order) {
|
if (!$override_order) {
|
||||||
$override_order = "date_entered DESC, updated DESC";
|
$override_order = match (true) {
|
||||||
|
$feed == Feeds::FEED_PUBLISHED && !$is_cat => 'last_published DESC',
|
||||||
if ($feed == Feeds::FEED_PUBLISHED && !$is_cat) {
|
$feed == Feeds::FEED_STARRED && !$is_cat => 'last_marked DESC',
|
||||||
$override_order = "last_published DESC";
|
default => 'date_entered DESC, updated DESC',
|
||||||
} else if ($feed == Feeds::FEED_STARRED && !$is_cat) {
|
};
|
||||||
$override_order = "last_marked DESC";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$params = array(
|
$params = array(
|
||||||
@@ -42,8 +46,9 @@ class Handler_Public extends Handler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!$is_cat && is_numeric($feed) && $feed < PLUGIN_FEED_BASE_INDEX && $feed > LABEL_BASE_INDEX) {
|
if (!$is_cat && is_numeric($feed) && $feed < PLUGIN_FEED_BASE_INDEX && $feed > LABEL_BASE_INDEX) {
|
||||||
|
// TODO: _ENABLED_PLUGINS is profile-specific, so use of the default profile's plugins here should
|
||||||
$user_plugins = get_pref(Prefs::_ENABLED_PLUGINS, $owner_uid);
|
// be called out in the docs, and/or access key stuff (see 'rss()') should also consider the profile
|
||||||
|
$user_plugins = Prefs::get(Prefs::_ENABLED_PLUGINS, $owner_uid);
|
||||||
|
|
||||||
$tmppluginhost = new PluginHost();
|
$tmppluginhost = new PluginHost();
|
||||||
$tmppluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_ALL);
|
$tmppluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_ALL);
|
||||||
@@ -54,8 +59,6 @@ class Handler_Public extends Handler {
|
|||||||
PluginHost::feed_to_pfeed_id((int)$feed));
|
PluginHost::feed_to_pfeed_id((int)$feed));
|
||||||
|
|
||||||
if ($handler) {
|
if ($handler) {
|
||||||
// 'get_headlines' is implemented by the plugin.
|
|
||||||
// @phpstan-ignore-next-line
|
|
||||||
$qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id((int)$feed), $params);
|
$qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id((int)$feed), $params);
|
||||||
} else {
|
} else {
|
||||||
user_error("Failed to find handler for plugin feed ID: $feed", E_USER_ERROR);
|
user_error("Failed to find handler for plugin feed ID: $feed", E_USER_ERROR);
|
||||||
@@ -76,7 +79,8 @@ class Handler_Public extends Handler {
|
|||||||
"/public.php?op=rss&id=$feed&key=" .
|
"/public.php?op=rss&id=$feed&key=" .
|
||||||
Feeds::_get_access_key($feed, false, $owner_uid);
|
Feeds::_get_access_key($feed, false, $owner_uid);
|
||||||
|
|
||||||
if (!$feed_site_url) $feed_site_url = Config::get_self_url();
|
if (!$feed_site_url)
|
||||||
|
$feed_site_url = Config::get_self_url();
|
||||||
|
|
||||||
if ($format == 'atom') {
|
if ($format == 'atom') {
|
||||||
$tpl = new Templator();
|
$tpl = new Templator();
|
||||||
@@ -84,10 +88,12 @@ class Handler_Public extends Handler {
|
|||||||
$tpl->readTemplateFromFile("generated_feed.txt");
|
$tpl->readTemplateFromFile("generated_feed.txt");
|
||||||
|
|
||||||
$tpl->setVariable('FEED_TITLE', $feed_title, true);
|
$tpl->setVariable('FEED_TITLE', $feed_title, true);
|
||||||
|
$tpl->setVariable('FEED_UPDATED', date('c'), true);
|
||||||
$tpl->setVariable('VERSION', Config::get_version(), true);
|
$tpl->setVariable('VERSION', Config::get_version(), true);
|
||||||
$tpl->setVariable('FEED_URL', htmlspecialchars($feed_self_url), true);
|
$tpl->setVariable('FEED_URL', htmlspecialchars($feed_self_url), true);
|
||||||
|
|
||||||
$tpl->setVariable('SELF_URL', htmlspecialchars(Config::get_self_url()), true);
|
$tpl->setVariable('SELF_URL', htmlspecialchars(Config::get_self_url()), true);
|
||||||
|
|
||||||
while ($line = $result->fetch()) {
|
while ($line = $result->fetch()) {
|
||||||
|
|
||||||
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
|
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
|
||||||
@@ -120,8 +126,7 @@ class Handler_Public extends Handler {
|
|||||||
$content = DiskCache::rewrite_urls($content);
|
$content = DiskCache::rewrite_urls($content);
|
||||||
|
|
||||||
if ($line['note']) {
|
if ($line['note']) {
|
||||||
$content = "<div style=\"$note_style\">Article note: " . $line['note'] . "</div>" .
|
$content = "<div style=\"$note_style\">Article note: " . $line['note'] . "</div>" . $content;
|
||||||
$content;
|
|
||||||
$tpl->setVariable('ARTICLE_NOTE', htmlspecialchars($line['note']), true);
|
$tpl->setVariable('ARTICLE_NOTE', htmlspecialchars($line['note']), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +153,7 @@ class Handler_Public extends Handler {
|
|||||||
foreach ($enclosures as $e) {
|
foreach ($enclosures as $e) {
|
||||||
$type = htmlspecialchars($e['content_type']);
|
$type = htmlspecialchars($e['content_type']);
|
||||||
$url = htmlspecialchars($e['content_url']);
|
$url = htmlspecialchars($e['content_url']);
|
||||||
$length = $e['duration'] ? $e['duration'] : 1;
|
$length = $e['duration'] ?: 1;
|
||||||
|
|
||||||
$tpl->setVariable('ARTICLE_ENCLOSURE_URL', $url, true);
|
$tpl->setVariable('ARTICLE_ENCLOSURE_URL', $url, true);
|
||||||
$tpl->setVariable('ARTICLE_ENCLOSURE_TYPE', $type, true);
|
$tpl->setVariable('ARTICLE_ENCLOSURE_TYPE', $type, true);
|
||||||
@@ -181,14 +186,14 @@ class Handler_Public extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print $tmp;
|
print $tmp;
|
||||||
} else if ($format == 'json') {
|
} else { // $format == 'json'
|
||||||
|
|
||||||
$feed = array();
|
$feed = [
|
||||||
|
'title' => $feed_title,
|
||||||
$feed['title'] = $feed_title;
|
'feed_url' => $feed_self_url,
|
||||||
$feed['feed_url'] = $feed_self_url;
|
'self_url' => Config::get_self_url(),
|
||||||
$feed['self_url'] = Config::get_self_url();
|
'articles' => [],
|
||||||
$feed['articles'] = [];
|
];
|
||||||
|
|
||||||
while ($line = $result->fetch()) {
|
while ($line = $result->fetch()) {
|
||||||
|
|
||||||
@@ -207,30 +212,26 @@ class Handler_Public extends Handler {
|
|||||||
},
|
},
|
||||||
$line, $feed, $is_cat, $owner_uid);
|
$line, $feed, $is_cat, $owner_uid);
|
||||||
|
|
||||||
$article = array();
|
$article = [
|
||||||
|
'id' => $line['link'],
|
||||||
$article['id'] = $line['link'];
|
'link' => $line['link'],
|
||||||
$article['link'] = $line['link'];
|
'title' => $line['title'],
|
||||||
$article['title'] = $line['title'];
|
'content' => Sanitizer::sanitize($line['content'], false, $owner_uid, $feed_site_url, null, $line['id']),
|
||||||
$article['excerpt'] = $line["content_preview"];
|
'updated' => date('c', strtotime($line['updated'] ?? '')),
|
||||||
$article['content'] = Sanitizer::sanitize($line["content"], false, $owner_uid, $feed_site_url, null, $line["id"]);
|
'source' => [
|
||||||
$article['updated'] = date('c', strtotime($line["updated"] ?? ''));
|
'link' => $line['site_url'] ?: Config::get_self_url(),
|
||||||
|
'title' => $line['feed_title'] ?? $feed_title,
|
||||||
if (!empty($line['note'])) $article['note'] = $line['note'];
|
],
|
||||||
if (!empty($line['author'])) $article['author'] = $line['author'];
|
|
||||||
|
|
||||||
$article['source'] = [
|
|
||||||
'link' => $line['site_url'] ? $line["site_url"] : Config::get_self_url(),
|
|
||||||
'title' => $line['feed_title'] ?? $feed_title
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (count($line["tags"]) > 0) {
|
if (!empty($line['note']))
|
||||||
$article['tags'] = array();
|
$article['note'] = $line['note'];
|
||||||
|
|
||||||
foreach ($line["tags"] as $tag) {
|
if (!empty($line['author']))
|
||||||
array_push($article['tags'], $tag);
|
$article['author'] = $line['author'];
|
||||||
}
|
|
||||||
}
|
if (count($line['tags']) > 0)
|
||||||
|
$article['tags'] = $line['tags'];
|
||||||
|
|
||||||
$enclosures = Article::_get_enclosures($line["id"]);
|
$enclosures = Article::_get_enclosures($line["id"]);
|
||||||
|
|
||||||
@@ -238,23 +239,20 @@ class Handler_Public extends Handler {
|
|||||||
$article['enclosures'] = array();
|
$article['enclosures'] = array();
|
||||||
|
|
||||||
foreach ($enclosures as $e) {
|
foreach ($enclosures as $e) {
|
||||||
$type = $e['content_type'];
|
$article['enclosures'][] = [
|
||||||
$url = $e['content_url'];
|
'url' => $e['content_url'],
|
||||||
$length = $e['duration'];
|
'type' => $e['content_type'],
|
||||||
|
'length' => $e['duration'],
|
||||||
array_push($article['enclosures'], array("url" => $url, "type" => $type, "length" => $length));
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
array_push($feed['articles'], $article);
|
array_push($feed['articles'], $article);
|
||||||
}
|
}
|
||||||
|
|
||||||
header("Content-Type: text/json; charset=utf-8");
|
header("Content-Type: application/json; charset=utf-8");
|
||||||
print json_encode($feed);
|
print json_encode($feed);
|
||||||
|
|
||||||
} else {
|
|
||||||
header("Content-Type: text/plain; charset=utf-8");
|
|
||||||
print "Unknown format: $format.";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +319,7 @@ class Handler_Public extends Handler {
|
|||||||
|
|
||||||
header("Location: " . $redirect_url);
|
header("Location: " . $redirect_url);
|
||||||
} else {
|
} else {
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,20 +359,6 @@ class Handler_Public extends Handler {
|
|||||||
header('HTTP/1.1 403 Forbidden');
|
header('HTTP/1.1 403 Forbidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTask(): void {
|
|
||||||
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
function housekeepingTask(): void {
|
|
||||||
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
|
|
||||||
}
|
|
||||||
|
|
||||||
function globalUpdateFeeds(): void {
|
|
||||||
RPC::updaterandomfeed_real();
|
|
||||||
|
|
||||||
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
function login(): void {
|
function login(): void {
|
||||||
if (!Config::get(Config::SINGLE_USER_MODE)) {
|
if (!Config::get(Config::SINGLE_USER_MODE)) {
|
||||||
|
|
||||||
@@ -432,6 +416,13 @@ class Handler_Public extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function forgotpass(): void {
|
function forgotpass(): void {
|
||||||
|
if (Config::get(Config::DISABLE_LOGIN_FORM) || !str_contains(Config::get(Config::PLUGINS), "auth_internal")) {
|
||||||
|
header($_SERVER["SERVER_PROTOCOL"]." 403 Forbidden");
|
||||||
|
echo "Forbidden.";
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
startup_gettext();
|
startup_gettext();
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
@@ -447,15 +438,23 @@ class Handler_Public extends Handler {
|
|||||||
<link rel="icon" type="image/png" sizes="72x72" href="images/favicon-72px.png">
|
<link rel="icon" type="image/png" sizes="72x72" href="images/favicon-72px.png">
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
<?php
|
<?php
|
||||||
echo stylesheet_tag("themes/light.css");
|
|
||||||
echo javascript_tag("lib/dojo/dojo.js");
|
echo javascript_tag("lib/dojo/dojo.js");
|
||||||
echo javascript_tag("lib/dojo/tt-rss-layer.js");
|
echo javascript_tag("lib/dojo/tt-rss-layer.js");
|
||||||
|
echo javascript_tag("js/common.js");
|
||||||
|
echo javascript_tag("js/utility.js");
|
||||||
?>
|
?>
|
||||||
<?= Config::get_override_links() ?>
|
<?= Config::get_override_links() ?>
|
||||||
</head>
|
</head>
|
||||||
<body class='flat ttrss_utility'>
|
<body class='flat ttrss_utility css_loading'>
|
||||||
<div class='container'>
|
<div class='container'>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
const __csrf_token = "<?= $_SESSION["csrf_token"]; ?>";
|
||||||
|
|
||||||
|
const __default_light_theme = "<?= get_theme_path(Config::get(Config::DEFAULT_LIGHT_THEME), 'themes/light.css') ?>";
|
||||||
|
const __default_dark_theme = "<?= get_theme_path(Config::get(Config::DEFAULT_DARK_THEME), 'themes/night.css') ?>";
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
require(['dojo/parser', "dojo/ready", 'dijit/form/Button','dijit/form/CheckBox', 'dijit/form/Form',
|
require(['dojo/parser', "dojo/ready", 'dijit/form/Button','dijit/form/CheckBox', 'dijit/form/Form',
|
||||||
'dijit/form/Select','dijit/form/TextBox','dijit/form/ValidationTextBox'],function(parser, ready){
|
'dijit/form/Select','dijit/form/TextBox','dijit/form/ValidationTextBox'],function(parser, ready){
|
||||||
@@ -464,6 +463,19 @@ class Handler_Public extends Handler {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background : #303030;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.css_loading * {
|
||||||
|
display : none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
print "<h1>".__("Password recovery")."</h1>";
|
print "<h1>".__("Password recovery")."</h1>";
|
||||||
@@ -763,6 +775,11 @@ class Handler_Public extends Handler {
|
|||||||
$cache = DiskCache::instance($cache_dir);
|
$cache = DiskCache::instance($cache_dir);
|
||||||
|
|
||||||
if ($cache->exists($filename)) {
|
if ($cache->exists($filename)) {
|
||||||
|
$size = $cache->get_size($filename);
|
||||||
|
|
||||||
|
if ($size && $size > 0)
|
||||||
|
header("Content-Length: $size");
|
||||||
|
|
||||||
$cache->send($filename);
|
$cache->send($filename);
|
||||||
} else {
|
} else {
|
||||||
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
|
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
|
||||||
@@ -809,17 +826,17 @@ class Handler_Public extends Handler {
|
|||||||
$plugin->$method();
|
$plugin->$method();
|
||||||
} else {
|
} else {
|
||||||
user_error("PluginHandler[PUBLIC]: Requested private method '$method' of plugin '$plugin_name'.", E_USER_WARNING);
|
user_error("PluginHandler[PUBLIC]: Requested private method '$method' of plugin '$plugin_name'.", E_USER_WARNING);
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
print Errors::to_json(Errors::E_UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user_error("PluginHandler[PUBLIC]: Requested unknown method '$method' of plugin '$plugin_name'.", E_USER_WARNING);
|
user_error("PluginHandler[PUBLIC]: Requested unknown method '$method' of plugin '$plugin_name'.", E_USER_WARNING);
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
|
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user_error("PluginHandler[PUBLIC]: Requested method '$method' of unknown plugin '$plugin_name'.", E_USER_WARNING);
|
user_error("PluginHandler[PUBLIC]: Requested method '$method' of unknown plugin '$plugin_name'.", E_USER_WARNING);
|
||||||
header("Content-Type: text/json");
|
header("Content-Type: application/json");
|
||||||
print Errors::to_json(Errors::E_UNKNOWN_PLUGIN, ['plugin' => $plugin_name]);
|
print Errors::to_json(Errors::E_UNKNOWN_PLUGIN, ['plugin' => $plugin_name]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ class Labels
|
|||||||
/**
|
/**
|
||||||
* @return false|int false if the check for an existing label failed, otherwise the number of rows inserted (1 on success)
|
* @return false|int false if the check for an existing label failed, otherwise the number of rows inserted (1 on success)
|
||||||
*/
|
*/
|
||||||
static function create(string $caption, ?string $fg_color = '', ?string $bg_color = '', ?int $owner_uid = null) {
|
static function create(string $caption, ?string $fg_color = '', ?string $bg_color = '', ?int $owner_uid = null): false|int {
|
||||||
|
|
||||||
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
|
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
class Logger {
|
class Logger {
|
||||||
/** @var Logger|null */
|
private static ?Logger $instance = null;
|
||||||
private static $instance;
|
|
||||||
|
|
||||||
/** @var Logger_Adapter|null */
|
private ?Logger_Adapter $adapter = null;
|
||||||
private $adapter;
|
|
||||||
|
|
||||||
const LOG_DEST_SQL = "sql";
|
const LOG_DEST_SQL = "sql";
|
||||||
const LOG_DEST_STDOUT = "stdout";
|
const LOG_DEST_STDOUT = "stdout";
|
||||||
@@ -57,19 +55,12 @@ class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function __construct() {
|
function __construct() {
|
||||||
switch (Config::get(Config::LOG_DESTINATION)) {
|
$this->adapter = match (Config::get(Config::LOG_DESTINATION)) {
|
||||||
case self::LOG_DEST_SQL:
|
self::LOG_DEST_SQL => new Logger_SQL(),
|
||||||
$this->adapter = new Logger_SQL();
|
self::LOG_DEST_SYSLOG => new Logger_Syslog(),
|
||||||
break;
|
self::LOG_DEST_STDOUT => new Logger_Stdout(),
|
||||||
case self::LOG_DEST_SYSLOG:
|
default => null,
|
||||||
$this->adapter = new Logger_Syslog();
|
};
|
||||||
break;
|
|
||||||
case self::LOG_DEST_STDOUT:
|
|
||||||
$this->adapter = new Logger_Stdout();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$this->adapter = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->adapter && !implements_interface($this->adapter, "Logger_Adapter"))
|
if ($this->adapter && !implements_interface($this->adapter, "Logger_Adapter"))
|
||||||
user_error("Adapter for LOG_DESTINATION: " . Config::LOG_DESTINATION . " does not implement required interface.", E_USER_ERROR);
|
user_error("Adapter for LOG_DESTINATION: " . Config::LOG_DESTINATION . " does not implement required interface.", E_USER_ERROR);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class Mailer {
|
|||||||
* @param array<string, mixed> $params
|
* @param array<string, mixed> $params
|
||||||
* @return bool|int bool if the default mail function handled the request, otherwise an int as described in Mailer#mail()
|
* @return bool|int bool if the default mail function handled the request, otherwise an int as described in Mailer#mail()
|
||||||
*/
|
*/
|
||||||
function mail(array $params) {
|
function mail(array $params): bool|int {
|
||||||
|
|
||||||
$to_name = $params["to_name"] ?? "";
|
$to_name = $params["to_name"] ?? "";
|
||||||
$to_address = $params["to_address"];
|
$to_address = $params["to_address"];
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ class OPML extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool|int|void false if writing the file failed, true if printing succeeded, int if bytes were written to a file, or void if $owner_uid is missing
|
* @return bool|int|null false if writing the file failed, true if printing succeeded, int if bytes were written to a file, or null if $owner_uid is missing
|
||||||
*/
|
*/
|
||||||
function export() {
|
function export(): bool|int|null {
|
||||||
$output_name = sprintf("tt-rss_%s_%s.opml", $_SESSION["name"], date("Y-m-d"));
|
$output_name = sprintf("tt-rss_%s_%s.opml", $_SESSION["name"], date("Y-m-d"));
|
||||||
$include_settings = $_REQUEST["include_settings"] == "1";
|
$include_settings = $_REQUEST["include_settings"] == "1";
|
||||||
$owner_uid = $_SESSION["uid"];
|
$owner_uid = $_SESSION["uid"];
|
||||||
@@ -20,33 +20,6 @@ class OPML extends Handler_Protected {
|
|||||||
return $rc;
|
return $rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
function import(): void {
|
|
||||||
$owner_uid = $_SESSION["uid"];
|
|
||||||
|
|
||||||
header('Content-Type: text/html; charset=utf-8');
|
|
||||||
|
|
||||||
print "<html>
|
|
||||||
<head>
|
|
||||||
".stylesheet_tag("themes/light.css")."
|
|
||||||
<title>".__("OPML Utility")."</title>
|
|
||||||
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>
|
|
||||||
</head>
|
|
||||||
<body class='claro ttrss_utility'>
|
|
||||||
<h1>".__('OPML Utility')."</h1><div class='content'>";
|
|
||||||
|
|
||||||
Feeds::_add_cat("Imported feeds", $owner_uid);
|
|
||||||
|
|
||||||
$this->opml_notice(__("Importing OPML..."));
|
|
||||||
|
|
||||||
$this->opml_import($owner_uid);
|
|
||||||
|
|
||||||
print "<br><form method=\"GET\" action=\"prefs.php\">
|
|
||||||
<input type=\"submit\" value=\"".__("Return to preferences")."\">
|
|
||||||
</form>";
|
|
||||||
|
|
||||||
print "</div></body></html>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export
|
// Export
|
||||||
|
|
||||||
private function opml_export_category(int $owner_uid, int $cat_id, bool $hide_private_feeds = false, bool $include_settings = true): string {
|
private function opml_export_category(int $owner_uid, int $cat_id, bool $hide_private_feeds = false, bool $include_settings = true): string {
|
||||||
@@ -126,10 +99,10 @@ class OPML extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool|int|void false if writing the file failed, true if printing succeeded, int if bytes were written to a file, or void if $owner_uid is missing
|
* @return bool|int|null false if writing the file failed, true if printing succeeded, int if bytes were written to a file, or null if $owner_uid is missing
|
||||||
*/
|
*/
|
||||||
function opml_export(string $filename, int $owner_uid, bool $hide_private_feeds = false, bool $include_settings = true, bool $file_output = false) {
|
function opml_export(string $filename, int $owner_uid, bool $hide_private_feeds = false, bool $include_settings = true, bool $file_output = false): bool|int|null {
|
||||||
if (!$owner_uid) return;
|
if (!$owner_uid) return null;
|
||||||
|
|
||||||
if (!$file_output)
|
if (!$file_output)
|
||||||
if (!isset($_REQUEST["debug"])) {
|
if (!isset($_REQUEST["debug"])) {
|
||||||
@@ -209,6 +182,7 @@ class OPML extends Handler_Protected {
|
|||||||
if ($cat_filter && $tmp_line["cat_id"] || $tmp_line["feed_id"]) {
|
if ($cat_filter && $tmp_line["cat_id"] || $tmp_line["feed_id"]) {
|
||||||
$tmp_line["feed"] = Feeds::_get_title(
|
$tmp_line["feed"] = Feeds::_get_title(
|
||||||
$cat_filter ? $tmp_line["cat_id"] : $tmp_line["feed_id"],
|
$cat_filter ? $tmp_line["cat_id"] : $tmp_line["feed_id"],
|
||||||
|
$owner_uid,
|
||||||
$cat_filter);
|
$cat_filter);
|
||||||
} else {
|
} else {
|
||||||
$tmp_line["feed"] = "";
|
$tmp_line["feed"] = "";
|
||||||
@@ -217,16 +191,16 @@ class OPML extends Handler_Protected {
|
|||||||
$match = [];
|
$match = [];
|
||||||
foreach (json_decode($tmp_line["match_on"], true) as $feed_id) {
|
foreach (json_decode($tmp_line["match_on"], true) as $feed_id) {
|
||||||
|
|
||||||
if (strpos($feed_id, "CAT:") === 0) {
|
if (str_starts_with($feed_id, "CAT:")) {
|
||||||
$feed_id = (int)substr($feed_id, 4);
|
$feed_id = (int)substr($feed_id, 4);
|
||||||
if ($feed_id) {
|
if ($feed_id) {
|
||||||
array_push($match, [Feeds::_get_cat_title($feed_id), true, false]);
|
array_push($match, [Feeds::_get_cat_title($feed_id, $owner_uid), true, false]);
|
||||||
} else {
|
} else {
|
||||||
array_push($match, [0, true, true]);
|
array_push($match, [0, true, true]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ($feed_id) {
|
if ($feed_id) {
|
||||||
array_push($match, [Feeds::_get_title((int)$feed_id), false, false]);
|
array_push($match, [Feeds::_get_title((int)$feed_id, $owner_uid), false, false]);
|
||||||
} else {
|
} else {
|
||||||
array_push($match, [0, false, true]);
|
array_push($match, [0, false, true]);
|
||||||
}
|
}
|
||||||
@@ -363,6 +337,9 @@ class OPML extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo support passing in $profile so 'update.php --opml-import' can import prefs to a user profile
|
||||||
|
*/
|
||||||
private function opml_import_preference(DOMNode $node, int $owner_uid, int $nest): void {
|
private function opml_import_preference(DOMNode $node, int $owner_uid, int $nest): void {
|
||||||
$attrs = $node->attributes;
|
$attrs = $node->attributes;
|
||||||
$pref_name = $attrs->getNamedItem('pref-name')->nodeValue;
|
$pref_name = $attrs->getNamedItem('pref-name')->nodeValue;
|
||||||
@@ -373,7 +350,7 @@ class OPML extends Handler_Protected {
|
|||||||
$this->opml_notice(T_sprintf("Setting preference key %s to %s",
|
$this->opml_notice(T_sprintf("Setting preference key %s to %s",
|
||||||
$pref_name, $pref_value), $nest);
|
$pref_name, $pref_value), $nest);
|
||||||
|
|
||||||
set_pref($pref_name, $pref_value, $owner_uid);
|
Prefs::set($pref_name, $pref_value, $owner_uid, $_SESSION['profile'] ?? null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,14 +371,10 @@ class OPML extends Handler_Protected {
|
|||||||
//print "F: $title, $inverse, $enabled, $match_any_rule";
|
//print "F: $title, $inverse, $enabled, $match_any_rule";
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("INSERT INTO ttrss_filters2 (match_any_rule,enabled,inverse,title,owner_uid)
|
$sth = $this->pdo->prepare("INSERT INTO ttrss_filters2 (match_any_rule,enabled,inverse,title,owner_uid)
|
||||||
VALUES (?, ?, ?, ?, ?)");
|
VALUES (?, ?, ?, ?, ?) RETURNING id");
|
||||||
|
|
||||||
$sth->execute([$match_any_rule, $enabled, $inverse, $title, $owner_uid]);
|
$sth->execute([$match_any_rule, $enabled, $inverse, $title, $owner_uid]);
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT MAX(id) AS id FROM ttrss_filters2 WHERE
|
|
||||||
owner_uid = ?");
|
|
||||||
$sth->execute([$owner_uid]);
|
|
||||||
|
|
||||||
$row = $sth->fetch();
|
$row = $sth->fetch();
|
||||||
$filter_id = $row['id'];
|
$filter_id = $row['id'];
|
||||||
|
|
||||||
@@ -587,19 +560,12 @@ class OPML extends Handler_Protected {
|
|||||||
$dst_cat_id = $cat_id;
|
$dst_cat_id = $cat_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ($cat_title) {
|
match ($cat_title) {
|
||||||
case "tt-rss-prefs":
|
'tt-rss-prefs' => $this->opml_import_preference($node, $owner_uid, $nest+1),
|
||||||
$this->opml_import_preference($node, $owner_uid, $nest+1);
|
'tt-rss-labels' => $this->opml_import_label($node, $owner_uid, $nest+1),
|
||||||
break;
|
'tt-rss-filters' => $this->opml_import_filter($node, $owner_uid, $nest+1),
|
||||||
case "tt-rss-labels":
|
default => $this->opml_import_feed($node, $dst_cat_id, $owner_uid, $nest+1),
|
||||||
$this->opml_import_label($node, $owner_uid, $nest+1);
|
};
|
||||||
break;
|
|
||||||
case "tt-rss-filters":
|
|
||||||
$this->opml_import_filter($node, $owner_uid, $nest+1);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$this->opml_import_feed($node, $dst_cat_id, $owner_uid, $nest+1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -607,10 +573,10 @@ class OPML extends Handler_Protected {
|
|||||||
|
|
||||||
/** $filename is optional; assumes HTTP upload with $_FILES otherwise */
|
/** $filename is optional; assumes HTTP upload with $_FILES otherwise */
|
||||||
/**
|
/**
|
||||||
* @return bool|void false on failure, true if successful, void if $owner_uid is missing
|
* @return bool|null false on failure, true if successful, null if $owner_uid is missing
|
||||||
*/
|
*/
|
||||||
function opml_import(int $owner_uid, string $filename = "") {
|
function opml_import(int $owner_uid, string $filename = ""): ?bool {
|
||||||
if (!$owner_uid) return;
|
if (!$owner_uid) return null;
|
||||||
|
|
||||||
if (!$filename) {
|
if (!$filename) {
|
||||||
if ($_FILES['opml_file']['error'] != 0) {
|
if ($_FILES['opml_file']['error'] != 0) {
|
||||||
@@ -644,16 +610,8 @@ class OPML extends Handler_Protected {
|
|||||||
|
|
||||||
$doc = new DOMDocument();
|
$doc = new DOMDocument();
|
||||||
|
|
||||||
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
|
|
||||||
libxml_disable_entity_loader(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
$loaded = $doc->load($tmp_file);
|
$loaded = $doc->load($tmp_file);
|
||||||
|
|
||||||
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
|
|
||||||
libxml_disable_entity_loader(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// only remove temporary i.e. HTTP uploaded files
|
// only remove temporary i.e. HTTP uploaded files
|
||||||
if (!$filename)
|
if (!$filename)
|
||||||
unlink($tmp_file);
|
unlink($tmp_file);
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ abstract class Plugin {
|
|||||||
*
|
*
|
||||||
* @return string */
|
* @return string */
|
||||||
function __($msgid) {
|
function __($msgid) {
|
||||||
/** @var Plugin $this -- this is a strictly template-related hack */
|
// this is a strictly template-related hack
|
||||||
return _dgettext(PluginHost::object_to_domain($this), $msgid);
|
return _dgettext(PluginHost::object_to_domain($this), $msgid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ abstract class Plugin {
|
|||||||
*
|
*
|
||||||
* @return string */
|
* @return string */
|
||||||
function _ngettext($singular, $plural, $number) {
|
function _ngettext($singular, $plural, $number) {
|
||||||
/** @var Plugin $this -- this is a strictly template-related hack */
|
// this is a strictly template-related hack
|
||||||
return _dngettext(PluginHost::object_to_domain($this), $singular, $plural, $number);
|
return _dngettext(PluginHost::object_to_domain($this), $singular, $plural, $number);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,18 +557,18 @@ abstract class Plugin {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Invoked when filter is triggered on an article, may be used to implement logging for filters
|
/**
|
||||||
* NOTE: $article_filters should be renamed $filter_actions because that's what this is
|
* Invoked when filtering is triggered on an article. May be used to implement logging for filters, etc.
|
||||||
* @param int $feed_id
|
* @param int $feed_id
|
||||||
* @param int $owner_uid
|
* @param int $owner_uid
|
||||||
* @param array<string,mixed> $article
|
* @param array<string,mixed> $article
|
||||||
* @param array<string,mixed> $matched_filters
|
* @param array<string,mixed> $matched_filters
|
||||||
* @param array<string,string|bool|int> $matched_rules
|
* @param array<string,string|bool|int> $matched_rules
|
||||||
* @param array<string,string> $article_filters
|
* @param array<int, array{'type': string, 'param': string}> $article_filter_actions An array of filter actions from matched filters
|
||||||
* @return void
|
* @return void
|
||||||
* @see PluginHost::HOOK_FILTER_TRIGGERED
|
* @see PluginHost::HOOK_FILTER_TRIGGERED
|
||||||
*/
|
*/
|
||||||
function hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters) {
|
function hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filter_actions) {
|
||||||
user_error("Dummy method invoked.", E_USER_ERROR);
|
user_error("Dummy method invoked.", E_USER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,4 +713,26 @@ abstract class Plugin {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Invoked after passed article IDs were either marked (i.e. starred) or unmarked.
|
||||||
|
*
|
||||||
|
* **Note** resulting state of the articles is not passed to this function (because
|
||||||
|
* tt-rss may do invert operation on ID range), you will need to get this from the database.
|
||||||
|
* @param array<int> $article_ids ref_ids
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function hook_articles_mark_toggled(array $article_ids) {
|
||||||
|
user_error("Dummy method invoked.", E_USER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Invoked after passed article IDs were either published or unpublished.
|
||||||
|
*
|
||||||
|
* **Note** resulting state of the articles is not passed to this function (because
|
||||||
|
* tt-rss may do invert operation on ID range), you will need to get this from the database.
|
||||||
|
*
|
||||||
|
* @param array<int> $article_ids ref_ids
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function hook_articles_publish_toggled(array $article_ids) {
|
||||||
|
user_error("Dummy method invoked.", E_USER_ERROR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ class PluginHost {
|
|||||||
/** @var array<string, array<string, mixed>> plugin name -> (potential profile array) -> key -> value */
|
/** @var array<string, array<string, mixed>> plugin name -> (potential profile array) -> key -> value */
|
||||||
private array $storage = [];
|
private array $storage = [];
|
||||||
|
|
||||||
/** @var array<int, array<int, array{'id': int, 'title': string, 'sender': Plugin, 'icon': string}>> */
|
/** @var array<int, array{'id': int, 'title': string, 'sender': Plugin, 'icon': string}> */
|
||||||
private array $feeds = [];
|
private array $special_feeds = [];
|
||||||
|
|
||||||
/** @var array<string, Plugin> API method name, Plugin sender */
|
/** @var array<string, Plugin> API method name, Plugin sender */
|
||||||
private array $api_methods = [];
|
private array $api_methods = [];
|
||||||
@@ -38,6 +38,8 @@ class PluginHost {
|
|||||||
|
|
||||||
private static ?PluginHost $instance = null;
|
private static ?PluginHost $instance = null;
|
||||||
|
|
||||||
|
private ?Scheduler $scheduler = null;
|
||||||
|
|
||||||
const API_VERSION = 2;
|
const API_VERSION = 2;
|
||||||
const PUBLIC_METHOD_DELIMITER = "--";
|
const PUBLIC_METHOD_DELIMITER = "--";
|
||||||
|
|
||||||
@@ -143,9 +145,6 @@ class PluginHost {
|
|||||||
/** @see Plugin::hook_format_article() */
|
/** @see Plugin::hook_format_article() */
|
||||||
const HOOK_FORMAT_ARTICLE = "hook_format_article";
|
const HOOK_FORMAT_ARTICLE = "hook_format_article";
|
||||||
|
|
||||||
/** @see Plugin::hook_format_article_cdm() */
|
|
||||||
const HOOK_FORMAT_ARTICLE_CDM = "hook_format_article_cdm";
|
|
||||||
|
|
||||||
/** @see Plugin::hook_feed_basic_info() */
|
/** @see Plugin::hook_feed_basic_info() */
|
||||||
const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info";
|
const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info";
|
||||||
|
|
||||||
@@ -202,6 +201,12 @@ class PluginHost {
|
|||||||
/** @see Plugin::hook_validate_session() */
|
/** @see Plugin::hook_validate_session() */
|
||||||
const HOOK_VALIDATE_SESSION = "hook_validate_session";
|
const HOOK_VALIDATE_SESSION = "hook_validate_session";
|
||||||
|
|
||||||
|
/** @see Plugin::hook_articles_mark_toggled() */
|
||||||
|
const HOOK_ARTICLES_MARK_TOGGLED = "hook_articles_mark_toggled";
|
||||||
|
|
||||||
|
/** @see Plugin::hook_articles_publish_toggled() */
|
||||||
|
const HOOK_ARTICLES_PUBLISH_TOGGLED = "hook_articles_publish_toggled";
|
||||||
|
|
||||||
const KIND_ALL = 1;
|
const KIND_ALL = 1;
|
||||||
const KIND_SYSTEM = 2;
|
const KIND_SYSTEM = 2;
|
||||||
const KIND_USER = 3;
|
const KIND_USER = 3;
|
||||||
@@ -212,6 +217,7 @@ class PluginHost {
|
|||||||
|
|
||||||
function __construct() {
|
function __construct() {
|
||||||
$this->pdo = Db::pdo();
|
$this->pdo = Db::pdo();
|
||||||
|
$this->scheduler = new Scheduler('PluginHost Scheduler');
|
||||||
$this->storage = [];
|
$this->storage = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,16 +348,8 @@ class PluginHost {
|
|||||||
*/
|
*/
|
||||||
function chain_hooks_callback(string $hook, Closure $callback, &...$args): void {
|
function chain_hooks_callback(string $hook, Closure $callback, &...$args): void {
|
||||||
$method = strtolower((string)$hook);
|
$method = strtolower((string)$hook);
|
||||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
|
||||||
$span->addEvent("chain_hooks_callback: $hook");
|
|
||||||
|
|
||||||
foreach ($this->get_hooks((string)$hook) as $plugin) {
|
foreach ($this->get_hooks((string)$hook) as $plugin) {
|
||||||
//Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
|
|
||||||
|
|
||||||
//$p_span = Tracer::start("$hook - " . get_class($plugin));
|
|
||||||
|
|
||||||
$span->addEvent("$hook - " . get_class($plugin));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($callback($plugin->$method(...$args), $plugin))
|
if ($callback($plugin->$method(...$args), $plugin))
|
||||||
break;
|
break;
|
||||||
@@ -360,11 +358,7 @@ class PluginHost {
|
|||||||
} catch (Error $err) {
|
} catch (Error $err) {
|
||||||
user_error($err, E_USER_WARNING);
|
user_error($err, E_USER_WARNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
//$p_span->end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//$span->end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -398,7 +392,7 @@ class PluginHost {
|
|||||||
* @param PluginHost::HOOK_* $type
|
* @param PluginHost::HOOK_* $type
|
||||||
*/
|
*/
|
||||||
function del_hook(string $type, Plugin $sender): void {
|
function del_hook(string $type, Plugin $sender): void {
|
||||||
if (is_array($this->hooks[$type])) {
|
if (array_key_exists($type, $this->hooks)) {
|
||||||
foreach (array_keys($this->hooks[$type]) as $prio) {
|
foreach (array_keys($this->hooks[$type]) as $prio) {
|
||||||
$key = array_search($sender, $this->hooks[$type][$prio]);
|
$key = array_search($sender, $this->hooks[$type][$prio]);
|
||||||
|
|
||||||
@@ -430,9 +424,6 @@ class PluginHost {
|
|||||||
* @param PluginHost::KIND_* $kind
|
* @param PluginHost::KIND_* $kind
|
||||||
*/
|
*/
|
||||||
function load_all(int $kind, ?int $owner_uid = null, bool $skip_init = false): void {
|
function load_all(int $kind, ?int $owner_uid = null, bool $skip_init = false): void {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
$span->setAttribute('func.args', json_encode(func_get_args()));
|
|
||||||
|
|
||||||
$plugins = [...(glob("plugins/*") ?: []), ...(glob("plugins.local/*") ?: [])];
|
$plugins = [...(glob("plugins/*") ?: []), ...(glob("plugins.local/*") ?: [])];
|
||||||
$plugins = array_filter($plugins, "is_dir");
|
$plugins = array_filter($plugins, "is_dir");
|
||||||
$plugins = array_map("basename", $plugins);
|
$plugins = array_map("basename", $plugins);
|
||||||
@@ -440,27 +431,24 @@ class PluginHost {
|
|||||||
asort($plugins);
|
asort($plugins);
|
||||||
|
|
||||||
$this->load(join(",", $plugins), (int)$kind, $owner_uid, $skip_init);
|
$this->load(join(",", $plugins), (int)$kind, $owner_uid, $skip_init);
|
||||||
|
|
||||||
$span->end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param PluginHost::KIND_* $kind
|
* @param PluginHost::KIND_* $kind
|
||||||
*/
|
*/
|
||||||
function load(string $classlist, int $kind, ?int $owner_uid = null, bool $skip_init = false): void {
|
function load(string $classlist, int $kind, ?int $owner_uid = null, bool $skip_init = false): void {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
$span->setAttribute('func.args', json_encode(func_get_args()));
|
|
||||||
|
|
||||||
$plugins = explode(",", $classlist);
|
$plugins = explode(",", $classlist);
|
||||||
|
|
||||||
$this->owner_uid = (int) $owner_uid;
|
$this->owner_uid = (int) $owner_uid;
|
||||||
|
|
||||||
|
if ($this->owner_uid) {
|
||||||
|
$this->set_scheduler_name("PluginHost Scheduler for UID $owner_uid");
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($plugins as $class) {
|
foreach ($plugins as $class) {
|
||||||
$class = trim($class);
|
$class = trim($class);
|
||||||
$class_file = strtolower(basename(clean($class)));
|
$class_file = strtolower(basename(clean($class)));
|
||||||
|
|
||||||
$span->addEvent("$class_file: load");
|
|
||||||
|
|
||||||
// try system plugin directory first
|
// try system plugin directory first
|
||||||
$file = Config::get_self_dir() . "/plugins/$class_file/init.php";
|
$file = Config::get_self_dir() . "/plugins/$class_file/init.php";
|
||||||
|
|
||||||
@@ -485,8 +473,6 @@ class PluginHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$_SESSION["safe_mode"] = 1;
|
$_SESSION["safe_mode"] = 1;
|
||||||
|
|
||||||
$span->setAttribute('error', 'plugin is blacklisted');
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,8 +483,6 @@ class PluginHost {
|
|||||||
|
|
||||||
} catch (Error $err) {
|
} catch (Error $err) {
|
||||||
user_error($err, E_USER_WARNING);
|
user_error($err, E_USER_WARNING);
|
||||||
|
|
||||||
$span->setAttribute('error', $err);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,8 +492,6 @@ class PluginHost {
|
|||||||
|
|
||||||
if ($plugin_api < self::API_VERSION) {
|
if ($plugin_api < self::API_VERSION) {
|
||||||
user_error("Plugin $class is not compatible with current API version (need: " . self::API_VERSION . ", got: $plugin_api)", E_USER_WARNING);
|
user_error("Plugin $class is not compatible with current API version (need: " . self::API_VERSION . ", got: $plugin_api)", E_USER_WARNING);
|
||||||
|
|
||||||
$span->setAttribute('error', 'plugin is not compatible with API version');
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,8 +500,6 @@ class PluginHost {
|
|||||||
_bind_textdomain_codeset($class, "UTF-8");
|
_bind_textdomain_codeset($class, "UTF-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->addEvent("$class_file: initialize");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch ($kind) {
|
switch ($kind) {
|
||||||
case $this::KIND_SYSTEM:
|
case $this::KIND_SYSTEM:
|
||||||
@@ -549,7 +529,6 @@ class PluginHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->load_data();
|
$this->load_data();
|
||||||
$span->end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function is_system(Plugin $plugin): bool {
|
function is_system(Plugin $plugin): bool {
|
||||||
@@ -581,7 +560,7 @@ class PluginHost {
|
|||||||
/**
|
/**
|
||||||
* @return false|Plugin false if the handler couldn't be found, otherwise the Plugin/handler
|
* @return false|Plugin false if the handler couldn't be found, otherwise the Plugin/handler
|
||||||
*/
|
*/
|
||||||
function lookup_handler(string $handler, string $method) {
|
function lookup_handler(string $handler, string $method): false|Plugin {
|
||||||
$handler = str_replace("-", "_", strtolower($handler));
|
$handler = str_replace("-", "_", strtolower($handler));
|
||||||
$method = strtolower($method);
|
$method = strtolower($method);
|
||||||
|
|
||||||
@@ -610,7 +589,7 @@ class PluginHost {
|
|||||||
/**
|
/**
|
||||||
* @return false|Plugin false if the command couldn't be found, otherwise the registered Plugin
|
* @return false|Plugin false if the command couldn't be found, otherwise the registered Plugin
|
||||||
*/
|
*/
|
||||||
function lookup_command(string $command) {
|
function lookup_command(string $command): false|Plugin {
|
||||||
$command = "-" . strtolower($command);
|
$command = "-" . strtolower($command);
|
||||||
|
|
||||||
if (array_key_exists($command, $this->commands)) {
|
if (array_key_exists($command, $this->commands)) {
|
||||||
@@ -620,7 +599,7 @@ class PluginHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, array{'description': string, 'suffix': string, 'arghelp': string, 'class': Plugin}>> command type -> details array */
|
/** @return array<string, array{'description': string, 'suffix': string, 'arghelp': string, 'class': Plugin}> command type -> details array */
|
||||||
function get_commands() {
|
function get_commands() {
|
||||||
return $this->commands;
|
return $this->commands;
|
||||||
}
|
}
|
||||||
@@ -638,17 +617,12 @@ class PluginHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function load_data(): void {
|
private function load_data(): void {
|
||||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
|
||||||
$span->addEvent('load plugin data');
|
|
||||||
|
|
||||||
if ($this->owner_uid && !$this->data_loaded && Config::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
|
$sth = $this->pdo->prepare("SELECT name, content FROM ttrss_plugin_storage
|
||||||
WHERE owner_uid = ?");
|
WHERE owner_uid = ?");
|
||||||
$sth->execute([$this->owner_uid]);
|
$sth->execute([$this->owner_uid]);
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
while ($line = $sth->fetch()) {
|
||||||
$span->addEvent($line["name"] . ': unserialize');
|
|
||||||
|
|
||||||
$this->storage[$line["name"]] = unserialize($line["content"]);
|
$this->storage[$line["name"]] = unserialize($line["content"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,9 +632,6 @@ class PluginHost {
|
|||||||
|
|
||||||
private function save_data(string $plugin): void {
|
private function save_data(string $plugin): void {
|
||||||
if ($this->owner_uid) {
|
if ($this->owner_uid) {
|
||||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
|
||||||
$span->addEvent(__METHOD__ . ": $plugin");
|
|
||||||
|
|
||||||
if (!$this->pdo_data)
|
if (!$this->pdo_data)
|
||||||
$this->pdo_data = Db::instance()->pdo_connect();
|
$this->pdo_data = Db::instance()->pdo_connect();
|
||||||
|
|
||||||
@@ -769,7 +740,7 @@ class PluginHost {
|
|||||||
* @param array<int|string, mixed> $default_value
|
* @param array<int|string, mixed> $default_value
|
||||||
* @return array<int|string, mixed>
|
* @return array<int|string, mixed>
|
||||||
*/
|
*/
|
||||||
function get_array(Plugin $sender, string $name, array $default_value = []) {
|
function get_array(Plugin $sender, string $name, array $default_value = []): array {
|
||||||
$tmp = $this->get($sender, $name);
|
$tmp = $this->get($sender, $name);
|
||||||
|
|
||||||
if (!is_array($tmp)) $tmp = $default_value;
|
if (!is_array($tmp)) $tmp = $default_value;
|
||||||
@@ -798,36 +769,51 @@ class PluginHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin feed functions are *EXPERIMENTAL*!
|
/**
|
||||||
|
* Add a special (plugin-provided) feed
|
||||||
|
*
|
||||||
|
* @param int $cat_id only -1 (Feeds::CATEGORY_SPECIAL) is supported
|
||||||
|
* @return false|int false if the feed wasn't added (e.g. $cat_id wasn't Feeds::CATEGORY_SPECIAL),
|
||||||
|
* otherwise an integer "feed ID" that might change between executions
|
||||||
|
*/
|
||||||
|
function add_feed(int $cat_id, string $title, string $icon, Plugin $sender): false|int {
|
||||||
|
if ($cat_id !== Feeds::CATEGORY_SPECIAL)
|
||||||
|
return false;
|
||||||
|
|
||||||
// cat_id: only -1 (Feeds::CATEGORY_SPECIAL) is supported (Special)
|
$id = count($this->special_feeds);
|
||||||
function add_feed(int $cat_id, string $title, string $icon, Plugin $sender): int {
|
|
||||||
|
|
||||||
if (empty($this->feeds[$cat_id]))
|
$this->special_feeds[] = [
|
||||||
$this->feeds[$cat_id] = [];
|
'id' => $id,
|
||||||
|
'title' => $title,
|
||||||
$id = count($this->feeds[$cat_id]);
|
'sender' => $sender,
|
||||||
|
'icon' => $icon,
|
||||||
array_push($this->feeds[$cat_id],
|
];
|
||||||
['id' => $id, 'title' => $title, 'sender' => $sender, 'icon' => $icon]);
|
|
||||||
|
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Get special (plugin-provided) feeds
|
||||||
|
*
|
||||||
|
* @param int $cat_id only -1 (Feeds::CATEGORY_SPECIAL) is supported
|
||||||
* @return array<int, array{'id': int, 'title': string, 'sender': Plugin, 'icon': string}>
|
* @return array<int, array{'id': int, 'title': string, 'sender': Plugin, 'icon': string}>
|
||||||
*/
|
*/
|
||||||
function get_feeds(int $cat_id) {
|
function get_feeds(int $cat_id) {
|
||||||
return $this->feeds[$cat_id] ?? [];
|
return $cat_id === Feeds::CATEGORY_SPECIAL ? $this->special_feeds : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert feed_id (e.g. -129) to pfeed_id first
|
/**
|
||||||
|
* Get the Plugin handling a specific virtual feed.
|
||||||
|
*
|
||||||
|
* Convert feed_id (e.g. -129) to pfeed_id first.
|
||||||
|
*
|
||||||
|
* @return (Plugin&IVirtualFeed)|null a Plugin that implements IVirtualFeed, otherwise null
|
||||||
|
*/
|
||||||
function get_feed_handler(int $pfeed_id): ?Plugin {
|
function get_feed_handler(int $pfeed_id): ?Plugin {
|
||||||
foreach ($this->feeds as $cat) {
|
foreach ($this->special_feeds as $feed) {
|
||||||
foreach ($cat as $feed) {
|
if ($feed['id'] == $pfeed_id) {
|
||||||
if ($feed['id'] == $pfeed_id) {
|
/** @var Plugin&IVirtualFeed $feed['sender'] */
|
||||||
return $feed['sender'];
|
return implements_interface($feed['sender'], 'IVirtualFeed') ? $feed['sender'] : null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -881,14 +867,12 @@ class PluginHost {
|
|||||||
*/
|
*/
|
||||||
function get_method_url(Plugin $sender, string $method, array $params = []): string {
|
function get_method_url(Plugin $sender, string $method, array $params = []): string {
|
||||||
return Config::get_self_url() . "/backend.php?" .
|
return Config::get_self_url() . "/backend.php?" .
|
||||||
http_build_query(
|
http_build_query([
|
||||||
array_merge(
|
'op' => 'pluginhandler',
|
||||||
[
|
'plugin' => strtolower(get_class($sender)),
|
||||||
"op" => "pluginhandler",
|
'method' => $method,
|
||||||
"plugin" => strtolower(get_class($sender)),
|
...$params,
|
||||||
"method" => $method
|
]);
|
||||||
],
|
|
||||||
$params));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortcut syntax (disabled for now)
|
// shortcut syntax (disabled for now)
|
||||||
@@ -910,12 +894,10 @@ class PluginHost {
|
|||||||
function get_public_method_url(Plugin $sender, string $method, array $params = []): ?string {
|
function get_public_method_url(Plugin $sender, string $method, array $params = []): ?string {
|
||||||
if ($sender->is_public_method($method)) {
|
if ($sender->is_public_method($method)) {
|
||||||
return Config::get_self_url() . "/public.php?" .
|
return Config::get_self_url() . "/public.php?" .
|
||||||
http_build_query(
|
http_build_query([
|
||||||
array_merge(
|
'op' => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method),
|
||||||
[
|
...$params,
|
||||||
"op" => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method),
|
]);
|
||||||
],
|
|
||||||
$params));
|
|
||||||
}
|
}
|
||||||
user_error("get_public_method_url: requested method '$method' of '" . get_class($sender) . "' is private.");
|
user_error("get_public_method_url: requested method '$method' of '" . get_class($sender) . "' is private.");
|
||||||
return null;
|
return null;
|
||||||
@@ -931,4 +913,31 @@ class PluginHost {
|
|||||||
$ref = new ReflectionClass(get_class($plugin));
|
$ref = new ReflectionClass(get_class($plugin));
|
||||||
return basename(dirname(dirname($ref->getFileName()))) == "plugins.local";
|
return basename(dirname(dirname($ref->getFileName()))) == "plugins.local";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exposes sheduled tasks functionality to plugins. For system plugins, tasks registered here are
|
||||||
|
* executed (if due) during housekeeping. For user plugins, tasks are only run after any feeds owned by
|
||||||
|
* the user have been processed in an update batch (which means user is not inactive).
|
||||||
|
*
|
||||||
|
* This behaviour mirrors that of `HOOK_HOUSE_KEEPING` for user plugins.
|
||||||
|
*
|
||||||
|
* @see Scheduler::add_scheduled_task()
|
||||||
|
* @see Plugin::hook_house_keeping()
|
||||||
|
*/
|
||||||
|
function add_scheduled_task(Plugin $sender, string $task_name, string $cron_expression, Closure $callback): bool {
|
||||||
|
if ($this->is_system($sender))
|
||||||
|
$task_name = get_class($sender) . ':' . $task_name;
|
||||||
|
else
|
||||||
|
$task_name = get_class($sender) . ':' . $task_name . ':' . $this->owner_uid;
|
||||||
|
|
||||||
|
return $this->scheduler->add_scheduled_task($task_name, $cron_expression, $callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_due_tasks() : void {
|
||||||
|
$this->scheduler->run_due_tasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function set_scheduler_name(string $name) : void {
|
||||||
|
$this->scheduler->set_name($name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,8 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
* @return array<int, string>
|
* @return array<int, string>
|
||||||
*/
|
*/
|
||||||
public static function get_ts_languages(): array {
|
public static function get_ts_languages(): array {
|
||||||
if (Config::get(Config::DB_TYPE) == 'pgsql') {
|
return array_map('ucfirst',
|
||||||
return array_map('ucfirst',
|
array_column(ORM::for_table('pg_ts_config')->select('cfgname')->find_array(), 'cfgname'));
|
||||||
array_column(ORM::for_table('pg_ts_config')->select('cfgname')->find_array(), 'cfgname'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renameCat(): void {
|
function renameCat(): void {
|
||||||
@@ -76,11 +72,15 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$feeds_obj = ORM::for_table('ttrss_feeds')
|
$feeds_obj = ORM::for_table('ttrss_feeds')
|
||||||
|
->table_alias('f')
|
||||||
->select_many('id', 'title', 'last_error', 'update_interval')
|
->select_many('id', 'title', 'last_error', 'update_interval')
|
||||||
->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
|
->select_expr('SUBSTRING_FOR_DATE(last_updated,1,19)', 'last_updated')
|
||||||
|
->left_outer_join('ttrss_user_entries', [ 'ue.feed_id', '=', 'f.id'], 'ue')
|
||||||
|
->select_expr('COUNT(ue.int_id) AS num_articles')
|
||||||
->where(['cat_id' => $cat_id, 'owner_uid' => $_SESSION['uid']])
|
->where(['cat_id' => $cat_id, 'owner_uid' => $_SESSION['uid']])
|
||||||
->order_by_asc('order_id')
|
->order_by_asc('order_id')
|
||||||
->order_by_asc('title');
|
->order_by_asc('title')
|
||||||
|
->group_by('id');
|
||||||
|
|
||||||
if ($search) {
|
if ($search) {
|
||||||
$feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
|
$feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
|
||||||
@@ -96,7 +96,10 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
'unread' => -1,
|
'unread' => -1,
|
||||||
'error' => $feed->last_error,
|
'error' => $feed->last_error,
|
||||||
'icon' => Feeds::_get_icon($feed->id),
|
'icon' => Feeds::_get_icon($feed->id),
|
||||||
'param' => TimeHelper::make_local_datetime($feed->last_updated, true),
|
'param' => T_sprintf(
|
||||||
|
_ngettext("(%d article / %s)", "(%d articles / %s)", $feed->num_articles),
|
||||||
|
$feed->num_articles,
|
||||||
|
TimeHelper::make_local_datetime($feed->last_updated)),
|
||||||
'updates_disabled' => (int)($feed->update_interval < 0),
|
'updates_disabled' => (int)($feed->update_interval < 0),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -112,6 +115,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
* @return array<string, array<int|string, mixed>|string>
|
* @return array<string, array<int|string, mixed>|string>
|
||||||
*/
|
*/
|
||||||
function _makefeedtree(): array {
|
function _makefeedtree(): array {
|
||||||
|
$profile = $_SESSION['profile'] ?? null;
|
||||||
|
|
||||||
if (clean($_REQUEST['mode'] ?? 0) != 2)
|
if (clean($_REQUEST['mode'] ?? 0) != 2)
|
||||||
$search = $_SESSION["prefs_feed_search"] ?? "";
|
$search = $_SESSION["prefs_feed_search"] ?? "";
|
||||||
@@ -125,7 +129,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
$root['param'] = 0;
|
$root['param'] = 0;
|
||||||
$root['type'] = 'category';
|
$root['type'] = 'category';
|
||||||
|
|
||||||
$enable_cats = get_pref(Prefs::ENABLE_FEED_CATS);
|
$enable_cats = Prefs::get(Prefs::ENABLE_FEED_CATS, $_SESSION['uid'], $profile);
|
||||||
|
|
||||||
if (clean($_REQUEST['mode'] ?? 0) == 2) {
|
if (clean($_REQUEST['mode'] ?? 0) == 2) {
|
||||||
|
|
||||||
@@ -175,7 +179,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
ttrss_labels2 WHERE owner_uid = ? ORDER by caption");
|
ttrss_labels2 WHERE owner_uid = ? ORDER by caption");
|
||||||
$sth->execute([$_SESSION['uid']]);
|
$sth->execute([$_SESSION['uid']]);
|
||||||
|
|
||||||
if (get_pref(Prefs::ENABLE_FEED_CATS)) {
|
if (Prefs::get(Prefs::ENABLE_FEED_CATS, $_SESSION['uid'], $profile)) {
|
||||||
$cat = $this->feedlist_init_cat(Feeds::CATEGORY_LABELS);
|
$cat = $this->feedlist_init_cat(Feeds::CATEGORY_LABELS);
|
||||||
} else {
|
} else {
|
||||||
$cat['items'] = [];
|
$cat['items'] = [];
|
||||||
@@ -240,8 +244,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
/**
|
/**
|
||||||
* Uncategorized is a special case.
|
* Uncategorized is a special case.
|
||||||
*
|
*
|
||||||
* Define a minimal array shape to help PHPStan with the type of $cat['items']
|
* @var array{id: string, bare_id: int, auxcounter: int, name: string, items: array<int, array<string, mixed>>, type: 'category', checkbox: bool, unread: int, child_unread: int}
|
||||||
* @var array{items: array<int, array<string, mixed>>} $cat
|
|
||||||
*/
|
*/
|
||||||
$cat = [
|
$cat = [
|
||||||
'id' => 'CAT:0',
|
'id' => 'CAT:0',
|
||||||
@@ -256,12 +259,16 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
];
|
];
|
||||||
|
|
||||||
$feeds_obj = ORM::for_table('ttrss_feeds')
|
$feeds_obj = ORM::for_table('ttrss_feeds')
|
||||||
|
->table_alias('f')
|
||||||
->select_many('id', 'title', 'last_error', 'update_interval')
|
->select_many('id', 'title', 'last_error', 'update_interval')
|
||||||
->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
|
->select_expr('SUBSTRING_FOR_DATE(last_updated,1,19)', 'last_updated')
|
||||||
|
->left_outer_join('ttrss_user_entries', [ 'ue.feed_id', '=', 'f.id'], 'ue')
|
||||||
|
->select_expr('COUNT(ue.int_id) AS num_articles')
|
||||||
->where('owner_uid', $_SESSION['uid'])
|
->where('owner_uid', $_SESSION['uid'])
|
||||||
->where_null('cat_id')
|
->where_null('cat_id')
|
||||||
->order_by_asc('order_id')
|
->order_by_asc('order_id')
|
||||||
->order_by_asc('title');
|
->order_by_asc('title')
|
||||||
|
->group_by('id');
|
||||||
|
|
||||||
if ($search) {
|
if ($search) {
|
||||||
$feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
|
$feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
|
||||||
@@ -276,7 +283,10 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
'checkbox' => false,
|
'checkbox' => false,
|
||||||
'error' => $feed->last_error,
|
'error' => $feed->last_error,
|
||||||
'icon' => Feeds::_get_icon($feed->id),
|
'icon' => Feeds::_get_icon($feed->id),
|
||||||
'param' => TimeHelper::make_local_datetime($feed->last_updated, true),
|
'param' => T_sprintf(
|
||||||
|
_ngettext("(%d article / %s)", "(%d articles / %s)", $feed->num_articles),
|
||||||
|
$feed->num_articles,
|
||||||
|
TimeHelper::make_local_datetime($feed->last_updated)),
|
||||||
'unread' => -1,
|
'unread' => -1,
|
||||||
'type' => 'feed',
|
'type' => 'feed',
|
||||||
'updates_disabled' => (int)($feed->update_interval < 0),
|
'updates_disabled' => (int)($feed->update_interval < 0),
|
||||||
@@ -293,11 +303,15 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
$feeds_obj = ORM::for_table('ttrss_feeds')
|
$feeds_obj = ORM::for_table('ttrss_feeds')
|
||||||
|
->table_alias('f')
|
||||||
->select_many('id', 'title', 'last_error', 'update_interval')
|
->select_many('id', 'title', 'last_error', 'update_interval')
|
||||||
->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
|
->select_expr('SUBSTRING_FOR_DATE(last_updated,1,19)', 'last_updated')
|
||||||
|
->left_outer_join('ttrss_user_entries', [ 'ue.feed_id', '=', 'f.id'], 'ue')
|
||||||
|
->select_expr('COUNT(ue.int_id) AS num_articles')
|
||||||
->where('owner_uid', $_SESSION['uid'])
|
->where('owner_uid', $_SESSION['uid'])
|
||||||
->order_by_asc('order_id')
|
->order_by_asc('order_id')
|
||||||
->order_by_asc('title');
|
->order_by_asc('title')
|
||||||
|
->group_by('id');
|
||||||
|
|
||||||
if ($search) {
|
if ($search) {
|
||||||
$feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
|
$feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
|
||||||
@@ -312,7 +326,10 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
'checkbox' => false,
|
'checkbox' => false,
|
||||||
'error' => $feed->last_error,
|
'error' => $feed->last_error,
|
||||||
'icon' => Feeds::_get_icon($feed->id),
|
'icon' => Feeds::_get_icon($feed->id),
|
||||||
'param' => TimeHelper::make_local_datetime($feed->last_updated, true),
|
'param' => T_sprintf(
|
||||||
|
_ngettext("(%d article / %s)", "(%d articles / %s)", $feed->num_articles),
|
||||||
|
$feed->num_articles,
|
||||||
|
TimeHelper::make_local_datetime($feed->last_updated)),
|
||||||
'unread' => -1,
|
'unread' => -1,
|
||||||
'type' => 'feed',
|
'type' => 'feed',
|
||||||
'updates_disabled' => (int)($feed->update_interval < 0),
|
'updates_disabled' => (int)($feed->update_interval < 0),
|
||||||
@@ -385,7 +402,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
|
|
||||||
if ($item['_reference']) {
|
if ($item['_reference']) {
|
||||||
|
|
||||||
if (strpos($id, "FEED") === 0) {
|
if (str_starts_with($id, "FEED")) {
|
||||||
|
|
||||||
$feed = ORM::for_table('ttrss_feeds')
|
$feed = ORM::for_table('ttrss_feeds')
|
||||||
->where('owner_uid', $_SESSION['uid'])
|
->where('owner_uid', $_SESSION['uid'])
|
||||||
@@ -396,7 +413,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
$feed->cat_id = ($item_id != "root" && $bare_item_id) ? $bare_item_id : null;
|
$feed->cat_id = ($item_id != "root" && $bare_item_id) ? $bare_item_id : null;
|
||||||
$feed->save();
|
$feed->save();
|
||||||
}
|
}
|
||||||
} else if (strpos($id, "CAT:") === 0) {
|
} else if (str_starts_with($id, "CAT:")) {
|
||||||
$this->process_category_order($data_map, $item['_reference'], $item_id,
|
$this->process_category_order($data_map, $item['_reference'], $item_id,
|
||||||
$nest_level+1);
|
$nest_level+1);
|
||||||
|
|
||||||
@@ -523,6 +540,8 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
global $purge_intervals;
|
global $purge_intervals;
|
||||||
global $update_intervals;
|
global $update_intervals;
|
||||||
|
|
||||||
|
$profile = $_SESSION['profile'] ?? null;
|
||||||
|
|
||||||
$feed_id = (int)clean($_REQUEST["id"]);
|
$feed_id = (int)clean($_REQUEST["id"]);
|
||||||
|
|
||||||
$row = ORM::for_table('ttrss_feeds')
|
$row = ORM::for_table('ttrss_feeds')
|
||||||
@@ -537,13 +556,14 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
ob_end_clean();
|
ob_end_clean();
|
||||||
|
|
||||||
$row["icon"] = Feeds::_get_icon($feed_id);
|
$row["icon"] = Feeds::_get_icon($feed_id);
|
||||||
|
$row["auth_pass"] = Feeds::decrypt_feed_pass($row["auth_pass"]);
|
||||||
|
|
||||||
$local_update_intervals = $update_intervals;
|
$local_update_intervals = $update_intervals;
|
||||||
$local_update_intervals[0] .= sprintf(" (%s)", $update_intervals[get_pref(Prefs::DEFAULT_UPDATE_INTERVAL)]);
|
$local_update_intervals[0] .= sprintf(" (%s)", $update_intervals[Prefs::get(Prefs::DEFAULT_UPDATE_INTERVAL, $_SESSION['uid'])]);
|
||||||
|
|
||||||
if (Config::get(Config::FORCE_ARTICLE_PURGE) == 0) {
|
if (Config::get(Config::FORCE_ARTICLE_PURGE) == 0) {
|
||||||
$local_purge_intervals = $purge_intervals;
|
$local_purge_intervals = $purge_intervals;
|
||||||
$default_purge_interval = get_pref(Prefs::PURGE_OLD_DAYS);
|
$default_purge_interval = Prefs::get(Prefs::PURGE_OLD_DAYS, $_SESSION['uid']);
|
||||||
|
|
||||||
if ($default_purge_interval > 0)
|
if ($default_purge_interval > 0)
|
||||||
$local_purge_intervals[0] .= " " . T_nsprintf('(%d day)', '(%d days)', $default_purge_interval, $default_purge_interval);
|
$local_purge_intervals[0] .= " " . T_nsprintf('(%d day)', '(%d days)', $default_purge_interval, $default_purge_interval);
|
||||||
@@ -560,7 +580,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
print json_encode([
|
print json_encode([
|
||||||
"feed" => $row,
|
"feed" => $row,
|
||||||
"cats" => [
|
"cats" => [
|
||||||
"enabled" => get_pref(Prefs::ENABLE_FEED_CATS),
|
"enabled" => Prefs::get(Prefs::ENABLE_FEED_CATS, $_SESSION['uid'], $profile),
|
||||||
"select" => \Controls\select_feeds_cats("cat_id", $row["cat_id"]),
|
"select" => \Controls\select_feeds_cats("cat_id", $row["cat_id"]),
|
||||||
],
|
],
|
||||||
"plugin_data" => $plugin_data,
|
"plugin_data" => $plugin_data,
|
||||||
@@ -573,8 +593,8 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
"access_level" => $user->access_level
|
"access_level" => $user->access_level
|
||||||
],
|
],
|
||||||
"lang" => [
|
"lang" => [
|
||||||
"enabled" => Config::get(Config::DB_TYPE) == "pgsql",
|
"enabled" => true,
|
||||||
"default" => get_pref(Prefs::DEFAULT_SEARCH_LANGUAGE),
|
"default" => Prefs::get(Prefs::DEFAULT_SEARCH_LANGUAGE, $_SESSION['uid'], $profile),
|
||||||
"all" => $this::get_ts_languages(),
|
"all" => $this::get_ts_languages(),
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
@@ -593,10 +613,10 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
$feed_ids = clean($_REQUEST["ids"]);
|
$feed_ids = clean($_REQUEST["ids"]);
|
||||||
|
|
||||||
$local_update_intervals = $update_intervals;
|
$local_update_intervals = $update_intervals;
|
||||||
$local_update_intervals[0] .= sprintf(" (%s)", $update_intervals[get_pref(Prefs::DEFAULT_UPDATE_INTERVAL)]);
|
$local_update_intervals[0] .= sprintf(" (%s)", $update_intervals[Prefs::get(Prefs::DEFAULT_UPDATE_INTERVAL, $_SESSION['uid'])]);
|
||||||
|
|
||||||
$local_purge_intervals = $purge_intervals;
|
$local_purge_intervals = $purge_intervals;
|
||||||
$default_purge_interval = get_pref(Prefs::PURGE_OLD_DAYS);
|
$default_purge_interval = Prefs::get(Prefs::PURGE_OLD_DAYS, $_SESSION['uid']);
|
||||||
|
|
||||||
if ($default_purge_interval > 0)
|
if ($default_purge_interval > 0)
|
||||||
$local_purge_intervals[0] .= " " . T_sprintf("(%d days)", $default_purge_interval);
|
$local_purge_intervals[0] .= " " . T_sprintf("(%d days)", $default_purge_interval);
|
||||||
@@ -621,7 +641,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
<div dojoType="dijit.layout.TabContainer" style="height : 450px">
|
<div dojoType="dijit.layout.TabContainer" style="height : 450px">
|
||||||
<div dojoType="dijit.layout.ContentPane" title="<?= __('General') ?>">
|
<div dojoType="dijit.layout.ContentPane" title="<?= __('General') ?>">
|
||||||
<section>
|
<section>
|
||||||
<?php if (get_pref(Prefs::ENABLE_FEED_CATS)) { ?>
|
<?php if (Prefs::get(Prefs::ENABLE_FEED_CATS, $_SESSION['uid'], $_SESSION['profile'] ?? null)) { ?>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><?= __('Place in category:') ?></label>
|
<label><?= __('Place in category:') ?></label>
|
||||||
<?= \Controls\select_feeds_cats("cat_id", null, ['disabled' => '1']) ?>
|
<?= \Controls\select_feeds_cats("cat_id", null, ['disabled' => '1']) ?>
|
||||||
@@ -629,13 +649,11 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<?php if (Config::get(Config::DB_TYPE) == "pgsql") { ?>
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><?= __('Language:') ?></label>
|
<label><?= __('Language:') ?></label>
|
||||||
<?= \Controls\select_tag("feed_language", "", $this::get_ts_languages(), ["disabled"=> 1]) ?>
|
<?= \Controls\select_tag("feed_language", "", $this::get_ts_languages(), ["disabled"=> 1]) ?>
|
||||||
<?= $this->_batch_toggle_checkbox("feed_language") ?>
|
<?= $this->_batch_toggle_checkbox("feed_language") ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<?php } ?>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
@@ -723,6 +741,11 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
|
|
||||||
$feed_language = clean($_POST["feed_language"] ?? "");
|
$feed_language = clean($_POST["feed_language"] ?? "");
|
||||||
|
|
||||||
|
$key = Config::get(Config::ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
if ($key && $auth_pass)
|
||||||
|
$auth_pass = base64_encode(serialize(Crypt::encrypt_string($auth_pass)));
|
||||||
|
|
||||||
if (!$batch) {
|
if (!$batch) {
|
||||||
|
|
||||||
/* $sth = $this->pdo->prepare("SELECT feed_url FROM ttrss_feeds WHERE id = ?");
|
/* $sth = $this->pdo->prepare("SELECT feed_url FROM ttrss_feeds WHERE id = ?");
|
||||||
@@ -828,7 +851,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "cat_id":
|
case "cat_id":
|
||||||
if (get_pref(Prefs::ENABLE_FEED_CATS)) {
|
if (Prefs::get(Prefs::ENABLE_FEED_CATS, $_SESSION['uid'], $_SESSION['profile'] ?? null)) {
|
||||||
$qpart = "cat_id = ?";
|
$qpart = "cat_id = ?";
|
||||||
$qparams = $cat_id ? [$cat_id] : [null];
|
$qparams = $cat_id ? [$cat_id] : [null];
|
||||||
}
|
}
|
||||||
@@ -938,7 +961,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (get_pref(Prefs::ENABLE_FEED_CATS)) { ?>
|
<?php if (Prefs::get(Prefs::ENABLE_FEED_CATS, $_SESSION['uid'], $_SESSION['profile'] ?? null)) { ?>
|
||||||
<div dojoType="fox.form.DropDownButton">
|
<div dojoType="fox.form.DropDownButton">
|
||||||
<span><?= __('Categories') ?></span>
|
<span><?= __('Categories') ?></span>
|
||||||
<div dojoType="dijit.Menu" style="display: none">
|
<div dojoType="dijit.Menu" style="display: none">
|
||||||
@@ -1077,13 +1100,10 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
private function feedlist_init_cat(int $cat_id): array {
|
private function feedlist_init_cat(int $cat_id): array {
|
||||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
|
||||||
$span->addEvent(__METHOD__ . ": $cat_id");
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => 'CAT:' . $cat_id,
|
'id' => 'CAT:' . $cat_id,
|
||||||
'items' => array(),
|
'items' => array(),
|
||||||
'name' => Feeds::_get_cat_title($cat_id),
|
'name' => Feeds::_get_cat_title($cat_id, $_SESSION['uid']),
|
||||||
'type' => 'category',
|
'type' => 'category',
|
||||||
'unread' => -1, //(int) Feeds::_get_cat_unread($cat_id);
|
'unread' => -1, //(int) Feeds::_get_cat_unread($cat_id);
|
||||||
'bare_id' => $cat_id,
|
'bare_id' => $cat_id,
|
||||||
@@ -1094,11 +1114,8 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
private function feedlist_init_feed(int $feed_id, ?string $title = null, bool $unread = false, string $error = '', string $updated = ''): array {
|
private function feedlist_init_feed(int $feed_id, ?string $title = null, bool $unread = false, string $error = '', string $updated = ''): array {
|
||||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
|
||||||
$span->addEvent(__METHOD__ . ": $feed_id");
|
|
||||||
|
|
||||||
if (!$title)
|
if (!$title)
|
||||||
$title = Feeds::_get_title($feed_id, false);
|
$title = Feeds::_get_title($feed_id, $_SESSION['uid']);
|
||||||
|
|
||||||
if ($unread === false)
|
if ($unread === false)
|
||||||
$unread = Feeds::_get_counters($feed_id, false, true);
|
$unread = Feeds::_get_counters($feed_id, false, true);
|
||||||
@@ -1118,12 +1135,6 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
|
|
||||||
function inactiveFeeds(): void {
|
function inactiveFeeds(): void {
|
||||||
|
|
||||||
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
|
||||||
$interval_qpart = "NOW() - INTERVAL '3 months'";
|
|
||||||
} else {
|
|
||||||
$interval_qpart = "DATE_SUB(NOW(), INTERVAL 3 MONTH)";
|
|
||||||
}
|
|
||||||
|
|
||||||
$inactive_feeds = ORM::for_table('ttrss_feeds')
|
$inactive_feeds = ORM::for_table('ttrss_feeds')
|
||||||
->table_alias('f')
|
->table_alias('f')
|
||||||
->select_many('f.id', 'f.title', 'f.site_url', 'f.feed_url')
|
->select_many('f.id', 'f.title', 'f.site_url', 'f.feed_url')
|
||||||
@@ -1135,7 +1146,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
"(SELECT MAX(ttrss_entries.updated)
|
"(SELECT MAX(ttrss_entries.updated)
|
||||||
FROM ttrss_entries
|
FROM ttrss_entries
|
||||||
JOIN ttrss_user_entries ON ttrss_entries.id = ttrss_user_entries.ref_id
|
JOIN ttrss_user_entries ON ttrss_entries.id = ttrss_user_entries.ref_id
|
||||||
WHERE ttrss_user_entries.feed_id = f.id) < $interval_qpart")
|
WHERE ttrss_user_entries.feed_id = f.id) < NOW() - INTERVAL '3 months'")
|
||||||
->group_by('f.title')
|
->group_by('f.title')
|
||||||
->group_by('f.id')
|
->group_by('f.id')
|
||||||
->group_by('f.site_url')
|
->group_by('f.site_url')
|
||||||
@@ -1144,7 +1155,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
->find_array();
|
->find_array();
|
||||||
|
|
||||||
foreach ($inactive_feeds as $inactive_feed) {
|
foreach ($inactive_feeds as $inactive_feed) {
|
||||||
$inactive_feed['last_article'] = TimeHelper::make_local_datetime($inactive_feed['last_article'], false);
|
$inactive_feed['last_article'] = TimeHelper::make_local_datetime($inactive_feed['last_article']);
|
||||||
}
|
}
|
||||||
|
|
||||||
print json_encode($inactive_feeds);
|
print json_encode($inactive_feeds);
|
||||||
@@ -1203,7 +1214,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
|
|
||||||
function batchSubscribe(): void {
|
function batchSubscribe(): void {
|
||||||
print json_encode([
|
print json_encode([
|
||||||
"enable_cats" => (int)get_pref(Prefs::ENABLE_FEED_CATS),
|
"enable_cats" => (int)Prefs::get(Prefs::ENABLE_FEED_CATS, $_SESSION['uid'], $_SESSION['profile'] ?? null),
|
||||||
"cat_select" => \Controls\select_feeds_cats("cat")
|
"cat_select" => \Controls\select_feeds_cats("cat")
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -1273,7 +1284,7 @@ class Pref_Feeds extends Handler_Protected {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
print json_encode([
|
print json_encode([
|
||||||
"title" => Feeds::_get_title($feed_id, $is_cat),
|
"title" => Feeds::_get_title($feed_id, $_SESSION['uid'], $is_cat),
|
||||||
"link" => $link
|
"link" => $link
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,22 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
const PARAM_ACTIONS = [self::ACTION_TAG, self::ACTION_SCORE,
|
const PARAM_ACTIONS = [self::ACTION_TAG, self::ACTION_SCORE,
|
||||||
self::ACTION_LABEL, self::ACTION_PLUGIN, self::ACTION_REMOVE_TAG];
|
self::ACTION_LABEL, self::ACTION_PLUGIN, self::ACTION_REMOVE_TAG];
|
||||||
|
|
||||||
|
const MAX_ACTIONS_TO_DISPLAY = 3;
|
||||||
|
|
||||||
|
/** @var array<int,array<mixed>> $action_descriptions */
|
||||||
|
private array $action_descriptions = [];
|
||||||
|
|
||||||
|
function before(string $method) : bool {
|
||||||
|
|
||||||
|
$descriptions = ORM::for_table("ttrss_filter_actions")->find_array();
|
||||||
|
|
||||||
|
foreach ($descriptions as $desc) {
|
||||||
|
$this->action_descriptions[$desc['id']] = $desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::before($method);
|
||||||
|
}
|
||||||
|
|
||||||
function csrf_ignore(string $method): bool {
|
function csrf_ignore(string $method): bool {
|
||||||
$csrf_ignored = array("index", "getfiltertree", "savefilterorder");
|
$csrf_ignored = array("index", "getfiltertree", "savefilterorder");
|
||||||
|
|
||||||
@@ -54,122 +70,173 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
$offset = (int) clean($_REQUEST["offset"]);
|
$offset = (int) clean($_REQUEST["offset"]);
|
||||||
$limit = (int) clean($_REQUEST["limit"]);
|
$limit = (int) clean($_REQUEST["limit"]);
|
||||||
|
|
||||||
$filter = array();
|
// catchall fake filter which includes all rules
|
||||||
|
$filter = [
|
||||||
$filter["enabled"] = true;
|
'enabled' => true,
|
||||||
$filter["match_any_rule"] = checkbox_to_sql_bool($_REQUEST["match_any_rule"] ?? false);
|
'match_any_rule' => checkbox_to_sql_bool($_REQUEST['match_any_rule'] ?? false),
|
||||||
$filter["inverse"] = checkbox_to_sql_bool($_REQUEST["inverse"] ?? false);
|
'inverse' => checkbox_to_sql_bool($_REQUEST['inverse'] ?? false),
|
||||||
|
'rules' => [],
|
||||||
$filter["rules"] = array();
|
'actions' => ['dummy-action'],
|
||||||
$filter["actions"] = array("dummy-action");
|
];
|
||||||
|
|
||||||
$res = $this->pdo->query("SELECT id,name FROM ttrss_filter_types");
|
|
||||||
|
|
||||||
/** @var array<int, string> */
|
/** @var array<int, string> */
|
||||||
$filter_types = [];
|
$filter_types = [];
|
||||||
|
|
||||||
while ($line = $res->fetch()) {
|
foreach (ORM::for_table('ttrss_filter_types')->find_many() as $filter_type) {
|
||||||
$filter_types[$line["id"]] = $line["name"];
|
$filter_types[$filter_type->id] = $filter_type->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope_qparts = array();
|
$scope_qparts = [];
|
||||||
|
|
||||||
$rctr = 0;
|
/** @var string $rule_json */
|
||||||
|
foreach (clean($_REQUEST['rule']) as $rule_json) {
|
||||||
|
/** @var array{reg_exp: string, filter_type: int, feed_id: array<int, int|string>, name: string, inverse?: bool}|null */
|
||||||
|
$rule = json_decode($rule_json, true);
|
||||||
|
|
||||||
/** @var string $r */
|
if (is_array($rule)) {
|
||||||
foreach (clean($_REQUEST["rule"]) AS $r) {
|
$rule['type'] = $filter_types[$rule['filter_type']];
|
||||||
/** @var array{'reg_exp': string, 'filter_type': int, 'feed_id': array<int, int|string>, 'name': string}|null */
|
$rule['inverse'] ??= false;
|
||||||
$rule = json_decode($r, true);
|
array_push($filter['rules'], $rule);
|
||||||
|
|
||||||
if ($rule && $rctr < 5) {
|
|
||||||
$rule["type"] = $filter_types[$rule["filter_type"]];
|
|
||||||
unset($rule["filter_type"]);
|
|
||||||
|
|
||||||
$scope_inner_qparts = [];
|
$scope_inner_qparts = [];
|
||||||
|
|
||||||
/** @var int|string $feed_id may be a category string (e.g. 'CAT:7') or feed ID int */
|
/** @var int|string $feed_id may be a category string (e.g. 'CAT:7') or feed ID int */
|
||||||
foreach ($rule["feed_id"] as $feed_id) {
|
foreach ($rule["feed_id"] as $feed_id) {
|
||||||
|
if (str_starts_with("$feed_id", "CAT:")) {
|
||||||
if (strpos("$feed_id", "CAT:") === 0) {
|
|
||||||
$cat_id = (int) substr("$feed_id", 4);
|
$cat_id = (int) substr("$feed_id", 4);
|
||||||
array_push($scope_inner_qparts, "cat_id = " . $cat_id);
|
if ($cat_id > 0)
|
||||||
|
array_push($scope_inner_qparts, "cat_id = " . $cat_id);
|
||||||
|
else
|
||||||
|
array_push($scope_inner_qparts, "cat_id IS NULL");
|
||||||
} else if (is_numeric($feed_id) && $feed_id > 0) {
|
} else if (is_numeric($feed_id) && $feed_id > 0) {
|
||||||
array_push($scope_inner_qparts, "feed_id = " . (int)$feed_id);
|
array_push($scope_inner_qparts, "feed_id = " . (int)$feed_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($scope_inner_qparts) > 0) {
|
if (count($scope_inner_qparts) > 0)
|
||||||
array_push($scope_qparts, "(" . implode(" OR ", $scope_inner_qparts) . ")");
|
array_push($scope_qparts, '(' . implode(' OR ', $scope_inner_qparts) . ')');
|
||||||
}
|
|
||||||
|
|
||||||
array_push($filter["rules"], $rule);
|
|
||||||
|
|
||||||
++$rctr;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($scope_qparts) == 0) $scope_qparts = ["true"];
|
$query = ORM::for_table('ttrss_entries')
|
||||||
|
->table_alias('e')
|
||||||
|
->select_many('e.title', 'e.content', 'e.date_entered', 'e.link', 'e.author', 'ue.tag_cache',
|
||||||
|
['feed_id' => 'f.id', 'feed_title' => 'f.title', 'cat_id' => 'fc.id'])
|
||||||
|
->join('ttrss_user_entries', [ 'ue.ref_id', '=', 'e.id'], 'ue')
|
||||||
|
->left_outer_join('ttrss_feeds', ['f.id', '=', 'ue.feed_id'], 'f')
|
||||||
|
->left_outer_join('ttrss_feed_categories', ['fc.id', '=', 'f.cat_id'], 'fc')
|
||||||
|
->where('ue.owner_uid', $_SESSION['uid'])
|
||||||
|
->order_by_desc('e.date_entered')
|
||||||
|
->limit($limit)
|
||||||
|
->offset($offset);
|
||||||
|
|
||||||
$glue = $filter['match_any_rule'] ? " OR " : " AND ";
|
if (count($scope_qparts) > 0)
|
||||||
$scope_qpart = join($glue, $scope_qparts);
|
$query->where_raw(join($filter['match_any_rule'] ? ' OR ' : ' AND ', $scope_qparts));
|
||||||
|
|
||||||
/** @phpstan-ignore-next-line */
|
$entries = $query->find_array();
|
||||||
if (!$scope_qpart) $scope_qpart = "true";
|
|
||||||
|
|
||||||
$rv = array();
|
$rv = [
|
||||||
|
'pre_filtering_count' => count($entries),
|
||||||
|
'items' => [],
|
||||||
|
];
|
||||||
|
|
||||||
//while ($found < $limit && $offset < $limit * 1000 && time() - $started < ini_get("max_execution_time") * 0.7) {
|
foreach ($entries as $entry) {
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT ttrss_entries.id,
|
// temporary filter which will be used to compare against returned article
|
||||||
ttrss_entries.title,
|
$feed_filter = $filter;
|
||||||
ttrss_feeds.id AS feed_id,
|
$feed_filter['rules'] = [];
|
||||||
ttrss_feeds.title AS feed_title,
|
|
||||||
ttrss_feed_categories.id AS cat_id,
|
|
||||||
content,
|
|
||||||
date_entered,
|
|
||||||
link,
|
|
||||||
author,
|
|
||||||
tag_cache
|
|
||||||
FROM
|
|
||||||
ttrss_entries, ttrss_user_entries
|
|
||||||
LEFT JOIN ttrss_feeds ON (feed_id = ttrss_feeds.id)
|
|
||||||
LEFT JOIN ttrss_feed_categories ON (ttrss_feeds.cat_id = ttrss_feed_categories.id)
|
|
||||||
WHERE
|
|
||||||
ref_id = ttrss_entries.id AND
|
|
||||||
($scope_qpart) AND
|
|
||||||
ttrss_user_entries.owner_uid = ?
|
|
||||||
ORDER BY date_entered DESC LIMIT $limit OFFSET $offset");
|
|
||||||
|
|
||||||
$sth->execute([$_SESSION['uid']]);
|
// only add rules which match result from specific feed or category ID or rules matching all feeds
|
||||||
|
// @phpstan-ignore foreach.emptyArray
|
||||||
|
foreach ($filter['rules'] as $rule) {
|
||||||
|
foreach ($rule['feed_id'] as $rule_feed) {
|
||||||
|
if (($rule_feed === 'CAT:0' && $entry['cat_id'] === null) || // rule matches Uncategorized
|
||||||
|
$rule_feed === 'CAT:' . $entry['cat_id'] || // rule matches category
|
||||||
|
(int)$rule_feed === $entry['feed_id'] || // rule matches feed
|
||||||
|
$rule_feed === '0') { // rule matches all feeds
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
$feed_filter['rules'][] = $rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$rc = RSSUtils::get_article_filters(array($filter), $line['title'], $line['content'], $line['link'],
|
$matched_rules = [];
|
||||||
$line['author'], explode(",", $line['tag_cache']));
|
|
||||||
|
|
||||||
if (count($rc) > 0) {
|
$entry_tags = explode(",", $entry['tag_cache']);
|
||||||
|
|
||||||
$line["content_preview"] = truncate_string(strip_tags($line["content"]), 200, '…');
|
$article_filter_actions = RSSUtils::eval_article_filters([$feed_filter], $entry['title'], $entry['content'], $entry['link'],
|
||||||
|
$entry['author'], $entry_tags, $matched_rules);
|
||||||
|
|
||||||
$excerpt_length = 100;
|
if (count($article_filter_actions) > 0) {
|
||||||
|
$content_preview = "";
|
||||||
|
|
||||||
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
|
$matches = [];
|
||||||
function ($result) use (&$line) {
|
$rules = [];
|
||||||
$line = $result;
|
|
||||||
},
|
|
||||||
$line, $excerpt_length);
|
|
||||||
|
|
||||||
$content_preview = $line["content_preview"];
|
$entry_title = $entry["title"];
|
||||||
|
|
||||||
$tmp = "<li><span class='title'>" . $line["title"] . "</span><br/>" .
|
// technically only one rule may match *here* because we're testing a single (fake) filter defined above
|
||||||
"<span class='feed'>" . $line['feed_title'] . "</span>, <span class='date'>" . mb_substr($line["date_entered"], 0, 16) . "</span>" .
|
// let's keep this forward-compatible in case we'll want to return multiple rules for whatever reason
|
||||||
"<div class='preview text-muted'>" . $content_preview . "</div>" .
|
foreach ($matched_rules as $rule) {
|
||||||
"</li>";
|
$can_highlight_content = false;
|
||||||
|
$can_highlight_title = false;
|
||||||
|
|
||||||
array_push($rv, $tmp);
|
$rule_regexp_match = mb_substr(strip_tags($rule['regexp_matches'][0]), 0, 200);
|
||||||
|
|
||||||
|
$matches[] = $rule_regexp_match;
|
||||||
|
|
||||||
|
$rules[] = self::_get_rule_name($rule, '');
|
||||||
|
|
||||||
|
if (in_array($rule['type'], ['content', 'both'])) {
|
||||||
|
// also stripping [\r\n\t] to match what's done for content in RSSUtils#eval_article_filters()
|
||||||
|
$entry_content_text = strip_tags(preg_replace("/[\r\n\t]/", "", $entry["content"]));
|
||||||
|
|
||||||
|
$match_index = mb_strpos($entry_content_text, $rule_regexp_match);
|
||||||
|
$content_preview = truncate_string(mb_substr($entry_content_text, $match_index), 200);
|
||||||
|
|
||||||
|
if ($match_index > 0)
|
||||||
|
$content_preview = '…' . $content_preview;
|
||||||
|
|
||||||
|
} else if ($rule['type'] == 'link') {
|
||||||
|
$content_preview = $entry['link'];
|
||||||
|
} else if ($rule['type'] == 'author') {
|
||||||
|
$content_preview = $entry['author'];
|
||||||
|
} else if ($rule['type'] == 'tag') {
|
||||||
|
$content_preview = '<i class="material-icons">label_outline</i> ' . implode(', ', $entry_tags);
|
||||||
|
} else {
|
||||||
|
$content_preview = "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($rule['type']) {
|
||||||
|
case "both":
|
||||||
|
$can_highlight_title = true;
|
||||||
|
$can_highlight_content = true;
|
||||||
|
break;
|
||||||
|
case "title":
|
||||||
|
$can_highlight_title = true;
|
||||||
|
break;
|
||||||
|
case "content":
|
||||||
|
case "link":
|
||||||
|
case "author":
|
||||||
|
case "tag":
|
||||||
|
$can_highlight_content = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($can_highlight_content)
|
||||||
|
$content_preview = Sanitizer::highlight_words_str($content_preview, $matches);
|
||||||
|
|
||||||
|
if ($can_highlight_title)
|
||||||
|
$entry_title = Sanitizer::highlight_words_str($entry_title, $matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rv['items'][] = [
|
||||||
|
'title' => $entry_title,
|
||||||
|
'feed_title' => $entry['feed_title'],
|
||||||
|
'date' => mb_substr($entry['date_entered'], 0, 16),
|
||||||
|
'content_preview' => $content_preview,
|
||||||
|
'rules' => $rules
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,148 +244,108 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function _get_rules_list(int $filter_id): string {
|
private function _get_rules_list(int $filter_id): string {
|
||||||
$sth = $this->pdo->prepare("SELECT reg_exp,
|
$rules = ORM::for_table('ttrss_filters2_rules')
|
||||||
inverse,
|
->table_alias('r')
|
||||||
match_on,
|
->join('ttrss_filter_types', ['r.filter_type', '=', 't.id'], 't')
|
||||||
feed_id,
|
->where('filter_id', $filter_id)
|
||||||
cat_id,
|
->select_many(['r.*', 'field' => 't.description'])
|
||||||
cat_filter,
|
->find_many();
|
||||||
ttrss_filter_types.description AS field
|
|
||||||
FROM
|
|
||||||
ttrss_filters2_rules, ttrss_filter_types
|
|
||||||
WHERE
|
|
||||||
filter_id = ? AND filter_type = ttrss_filter_types.id
|
|
||||||
ORDER BY reg_exp");
|
|
||||||
$sth->execute([$filter_id]);
|
|
||||||
|
|
||||||
$rv = "";
|
$rv = "";
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
foreach ($rules as $rule) {
|
||||||
|
if ($rule->match_on) {
|
||||||
|
$feeds = json_decode($rule->match_on, true);
|
||||||
|
$feeds_fmt = [];
|
||||||
|
|
||||||
if ($line["match_on"]) {
|
foreach ($feeds as $feed_id) {
|
||||||
$feeds = json_decode($line["match_on"], true);
|
|
||||||
$feeds_fmt = [];
|
|
||||||
|
|
||||||
foreach ($feeds as $feed_id) {
|
if (str_starts_with($feed_id, "CAT:")) {
|
||||||
|
$feed_id = (int)substr($feed_id, 4);
|
||||||
|
array_push($feeds_fmt, Feeds::_get_cat_title($feed_id, $_SESSION['uid']));
|
||||||
|
} else {
|
||||||
|
if ($feed_id)
|
||||||
|
array_push($feeds_fmt, Feeds::_get_title((int)$feed_id, $_SESSION['uid']));
|
||||||
|
else
|
||||||
|
array_push($feeds_fmt, __("All feeds"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (strpos($feed_id, "CAT:") === 0) {
|
$where = implode(", ", $feeds_fmt);
|
||||||
$feed_id = (int)substr($feed_id, 4);
|
|
||||||
array_push($feeds_fmt, Feeds::_get_cat_title($feed_id));
|
|
||||||
} else {
|
|
||||||
if ($feed_id)
|
|
||||||
array_push($feeds_fmt, Feeds::_get_title((int)$feed_id));
|
|
||||||
else
|
|
||||||
array_push($feeds_fmt, __("All feeds"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$where = implode(", ", $feeds_fmt);
|
} else {
|
||||||
|
$where = $rule->cat_filter ?
|
||||||
|
Feeds::_get_cat_title($rule->cat_id ?? 0, $_SESSION['uid']) :
|
||||||
|
($rule->feed_id ?
|
||||||
|
Feeds::_get_title($rule->feed_id, $_SESSION['uid']) : __("All feeds"));
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
$inverse_class = $rule->inverse ? "inverse" : "";
|
||||||
|
|
||||||
$where = $line["cat_filter"] ?
|
$rv .= "<li class='$inverse_class'>" . T_sprintf("%s on %s in %s %s",
|
||||||
Feeds::_get_cat_title($line["cat_id"] ?? 0) :
|
htmlspecialchars($rule->reg_exp),
|
||||||
($line["feed_id"] ?
|
$rule->field,
|
||||||
Feeds::_get_title($line["feed_id"]) : __("All feeds"));
|
|
||||||
}
|
|
||||||
|
|
||||||
# $where = $line["cat_id"] . "/" . $line["feed_id"];
|
|
||||||
|
|
||||||
$inverse = $line["inverse"] ? "inverse" : "";
|
|
||||||
|
|
||||||
$rv .= "<li class='$inverse'>" . T_sprintf("%s on %s in %s %s",
|
|
||||||
htmlspecialchars($line["reg_exp"]),
|
|
||||||
$line["field"],
|
|
||||||
$where,
|
$where,
|
||||||
$line["inverse"] ? __("(inverse)") : "") . "</li>";
|
$rule->inverse ? __("(inverse)") : "") . "</li>";
|
||||||
}
|
}
|
||||||
|
|
||||||
return $rv;
|
return $rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getfiltertree(): void {
|
function getfiltertree(): void {
|
||||||
$root = array();
|
$root = [
|
||||||
$root['id'] = 'root';
|
'id' => 'root',
|
||||||
$root['name'] = __('Filters');
|
'name' => __('Filters'),
|
||||||
$root['enabled'] = true;
|
'enabled' => true,
|
||||||
$root['items'] = array();
|
'items' => []
|
||||||
|
];
|
||||||
|
|
||||||
$filter_search = ($_SESSION["prefs_filter_search"] ?? "");
|
$filter_search = ($_SESSION["prefs_filter_search"] ?? "");
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT *,
|
$filters = ORM::for_table('ttrss_filters2')
|
||||||
(SELECT action_param FROM ttrss_filters2_actions
|
->where('owner_uid', $_SESSION['uid'])
|
||||||
WHERE filter_id = ttrss_filters2.id ORDER BY id LIMIT 1) AS action_param,
|
->order_by_asc(['order_id', 'title'])
|
||||||
(SELECT action_id FROM ttrss_filters2_actions
|
->find_many();
|
||||||
WHERE filter_id = ttrss_filters2.id ORDER BY id LIMIT 1) AS action_id,
|
|
||||||
(SELECT description FROM ttrss_filter_actions
|
|
||||||
WHERE id = (SELECT action_id FROM ttrss_filters2_actions
|
|
||||||
WHERE filter_id = ttrss_filters2.id ORDER BY id LIMIT 1)) AS action_name,
|
|
||||||
(SELECT reg_exp FROM ttrss_filters2_rules
|
|
||||||
WHERE filter_id = ttrss_filters2.id ORDER BY id LIMIT 1) AS reg_exp
|
|
||||||
FROM ttrss_filters2 WHERE
|
|
||||||
owner_uid = ? ORDER BY order_id, title");
|
|
||||||
$sth->execute([$_SESSION['uid']]);
|
|
||||||
|
|
||||||
$folder = array();
|
$folder = [
|
||||||
$folder['items'] = array();
|
'items' => []
|
||||||
|
];
|
||||||
|
|
||||||
while ($line = $sth->fetch()) {
|
foreach ($filters as $filter) {
|
||||||
|
$details = $this->_get_details($filter->id);
|
||||||
|
|
||||||
$name = $this->_get_name($line["id"]);
|
if ($filter_search &&
|
||||||
|
mb_stripos($filter->title, $filter_search) === false &&
|
||||||
|
!ORM::for_table('ttrss_filters2_rules')
|
||||||
|
->where('filter_id', $filter->id)
|
||||||
|
->where_raw('LOWER(reg_exp) LIKE LOWER(?)', ["%$filter_search%"])
|
||||||
|
->find_one()) {
|
||||||
|
|
||||||
$match_ok = false;
|
continue;
|
||||||
if ($filter_search) {
|
|
||||||
if (mb_strpos($line['title'], $filter_search) !== false) {
|
|
||||||
$match_ok = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rules_sth = $this->pdo->prepare("SELECT reg_exp
|
|
||||||
FROM ttrss_filters2_rules WHERE filter_id = ?");
|
|
||||||
$rules_sth->execute([$line['id']]);
|
|
||||||
|
|
||||||
while ($rule_line = $rules_sth->fetch()) {
|
|
||||||
if (mb_strpos($rule_line['reg_exp'], $filter_search) !== false) {
|
|
||||||
$match_ok = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($line['action_id'] == self::ACTION_LABEL) {
|
$item = [
|
||||||
$label_sth = $this->pdo->prepare("SELECT fg_color, bg_color
|
'id' => 'FILTER:' . $filter->id,
|
||||||
FROM ttrss_labels2 WHERE caption = ? AND
|
'bare_id' => $filter->id,
|
||||||
owner_uid = ?");
|
'bare_name' => $details['title'],
|
||||||
$label_sth->execute([$line['action_param'], $_SESSION['uid']]);
|
'name' => $details['title_summary'],
|
||||||
|
'param' => $details['actions_summary'],
|
||||||
|
'checkbox' => false,
|
||||||
|
'last_triggered' => $filter->last_triggered ? TimeHelper::make_local_datetime($filter->last_triggered) : null,
|
||||||
|
'enabled' => sql_bool_to_bool($filter->enabled),
|
||||||
|
'rules' => $this->_get_rules_list($filter->id)
|
||||||
|
];
|
||||||
|
|
||||||
if ($label_row = $label_sth->fetch()) {
|
array_push($folder['items'], $item);
|
||||||
//$fg_color = $label_row["fg_color"];
|
|
||||||
$bg_color = $label_row["bg_color"];
|
|
||||||
|
|
||||||
$name[1] = "<i class=\"material-icons\" style='color : $bg_color; margin-right : 4px'>label</i>" . $name[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$filter = array();
|
|
||||||
$filter['id'] = 'FILTER:' . $line['id'];
|
|
||||||
$filter['bare_id'] = $line['id'];
|
|
||||||
$filter['name'] = $name[0];
|
|
||||||
$filter['param'] = $name[1];
|
|
||||||
$filter['checkbox'] = false;
|
|
||||||
$filter['last_triggered'] = $line["last_triggered"] ? TimeHelper::make_local_datetime($line["last_triggered"], false) : null;
|
|
||||||
$filter['enabled'] = sql_bool_to_bool($line["enabled"]);
|
|
||||||
$filter['rules'] = $this->_get_rules_list($line['id']);
|
|
||||||
|
|
||||||
if (!$filter_search || $match_ok) {
|
|
||||||
array_push($folder['items'], $filter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$root['items'] = $folder['items'];
|
$root['items'] = $folder['items'];
|
||||||
|
|
||||||
$fl = array();
|
$fl = [
|
||||||
$fl['identifier'] = 'id';
|
'identifier' => 'id',
|
||||||
$fl['label'] = 'name';
|
'label' => 'name',
|
||||||
$fl['items'] = array($root);
|
'items' => [$root]
|
||||||
|
];
|
||||||
|
|
||||||
print json_encode($fl);
|
print json_encode($fl);
|
||||||
}
|
}
|
||||||
@@ -422,7 +449,7 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
/**
|
/**
|
||||||
* @param array<string, mixed>|null $rule
|
* @param array<string, mixed>|null $rule
|
||||||
*/
|
*/
|
||||||
private function _get_rule_name(?array $rule = null): string {
|
private function _get_rule_name(?array $rule = null, string $format = 'html'): string {
|
||||||
if (!$rule) $rule = json_decode(clean($_REQUEST["rule"]), true);
|
if (!$rule) $rule = json_decode(clean($_REQUEST["rule"]), true);
|
||||||
|
|
||||||
$feeds = $rule["feed_id"];
|
$feeds = $rule["feed_id"];
|
||||||
@@ -432,12 +459,12 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
|
|
||||||
foreach ($feeds as $feed_id) {
|
foreach ($feeds as $feed_id) {
|
||||||
|
|
||||||
if (strpos($feed_id, "CAT:") === 0) {
|
if (str_starts_with($feed_id, "CAT:")) {
|
||||||
$feed_id = (int)substr($feed_id, 4);
|
$feed_id = (int)substr($feed_id, 4);
|
||||||
array_push($feeds_fmt, Feeds::_get_cat_title($feed_id));
|
array_push($feeds_fmt, Feeds::_get_cat_title($feed_id, $_SESSION['uid']));
|
||||||
} else {
|
} else {
|
||||||
if ($feed_id)
|
if ($feed_id)
|
||||||
array_push($feeds_fmt, Feeds::_get_title((int)$feed_id));
|
array_push($feeds_fmt, Feeds::_get_title((int)$feed_id, $_SESSION['uid']));
|
||||||
else
|
else
|
||||||
array_push($feeds_fmt, __("All feeds"));
|
array_push($feeds_fmt, __("All feeds"));
|
||||||
}
|
}
|
||||||
@@ -457,49 +484,45 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
|
|
||||||
$inverse = isset($rule["inverse"]) ? "inverse" : "";
|
$inverse = isset($rule["inverse"]) ? "inverse" : "";
|
||||||
|
|
||||||
return "<span class='filterRule $inverse'>" .
|
if ($format === 'html')
|
||||||
T_sprintf("%s on %s in %s %s", htmlspecialchars($rule["reg_exp"]),
|
return "<span class='filterRule $inverse'>" .
|
||||||
"<span class='field'>$filter_type</span>", "<span class='feed'>$feed</span>", isset($rule["inverse"]) ? __("(inverse)") : "") . "</span>";
|
T_sprintf("%s on %s in %s %s", htmlspecialchars($rule["reg_exp"]),
|
||||||
}
|
"<span class='field'>$filter_type</span>", "<span class='feed'>$feed</span>", isset($rule["inverse"]) ? __("(inverse)") : "") . "</span>";
|
||||||
|
else
|
||||||
|
return T_sprintf("%s on %s in %s %s", $rule["reg_exp"],
|
||||||
|
$filter_type, $feed, isset($rule["inverse"]) ? __("(inverse)") : "");
|
||||||
|
}
|
||||||
|
|
||||||
function printRuleName(): void {
|
function printRuleName(): void {
|
||||||
print $this->_get_rule_name(json_decode(clean($_REQUEST["rule"]), true));
|
print $this->_get_rule_name(json_decode(clean($_REQUEST["rule"]), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed>|null $action
|
* @param array<string,mixed>|ArrayAccess<string, mixed>|null $action
|
||||||
*/
|
*/
|
||||||
private function _get_action_name(?array $action = null): string {
|
private function _get_action_name(array|ArrayAccess|null $action = null): string {
|
||||||
if (!$action) {
|
if (!$action) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT description FROM
|
$title = __($this->action_descriptions[$action['action_id']]['description']) ??
|
||||||
ttrss_filter_actions WHERE id = ?");
|
T_sprintf('Unknown action: %d', $action['action_id']);
|
||||||
$sth->execute([(int)$action["action_id"]]);
|
|
||||||
|
|
||||||
$title = "";
|
if ($action["action_id"] == self::ACTION_PLUGIN) {
|
||||||
|
list ($pfclass, $pfaction) = explode(":", $action["action_param"]);
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
$filter_actions = PluginHost::getInstance()->get_filter_actions();
|
||||||
|
|
||||||
$title = __($row["description"]);
|
foreach ($filter_actions as $fclass => $factions) {
|
||||||
|
foreach ($factions as $faction) {
|
||||||
if ($action["action_id"] == self::ACTION_PLUGIN) {
|
if ($pfaction == $faction["action"] && $pfclass == $fclass) {
|
||||||
list ($pfclass, $pfaction) = explode(":", $action["action_param"]);
|
$title .= ": " . $fclass . ": " . $faction["description"];
|
||||||
|
break;
|
||||||
$filter_actions = PluginHost::getInstance()->get_filter_actions();
|
|
||||||
|
|
||||||
foreach ($filter_actions as $fclass => $factions) {
|
|
||||||
foreach ($factions as $faction) {
|
|
||||||
if ($pfaction == $faction["action"] && $pfclass == $fclass) {
|
|
||||||
$title .= ": " . $fclass . ": " . $faction["description"];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (in_array($action["action_id"], self::PARAM_ACTIONS)) {
|
|
||||||
$title .= ": " . $action["action_param"];
|
|
||||||
}
|
}
|
||||||
|
} else if (in_array($action["action_id"], self::PARAM_ACTIONS)) {
|
||||||
|
$title .= ": " . $action["action_param"];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $title;
|
return $title;
|
||||||
@@ -541,6 +564,25 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
$sth->execute([...$ids, $_SESSION['uid']]);
|
$sth->execute([...$ids, $_SESSION['uid']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function _clone_rules_and_actions(int $filter_id, ?int $src_filter_id = null): bool {
|
||||||
|
$sth = $this->pdo->prepare('INSERT INTO ttrss_filters2_rules
|
||||||
|
(filter_id, reg_exp, inverse, filter_type, feed_id, cat_id, match_on, cat_filter)
|
||||||
|
SELECT :filter_id, reg_exp, inverse, filter_type, feed_id, cat_id, match_on, cat_filter
|
||||||
|
FROM ttrss_filters2_rules
|
||||||
|
WHERE filter_id = :src_filter_id');
|
||||||
|
|
||||||
|
if (!$sth->execute(['filter_id' => $filter_id, 'src_filter_id' => $src_filter_id]))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
$sth = $this->pdo->prepare('INSERT INTO ttrss_filters2_actions
|
||||||
|
(filter_id, action_id, action_param)
|
||||||
|
SELECT :filter_id, action_id, action_param
|
||||||
|
FROM ttrss_filters2_actions
|
||||||
|
WHERE filter_id = :src_filter_id');
|
||||||
|
|
||||||
|
return $sth->execute(['filter_id' => $filter_id, 'src_filter_id' => $src_filter_id]);
|
||||||
|
}
|
||||||
|
|
||||||
private function _save_rules_and_actions(int $filter_id): void {
|
private function _save_rules_and_actions(int $filter_id): void {
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("DELETE FROM ttrss_filters2_rules WHERE filter_id = ?");
|
$sth = $this->pdo->prepare("DELETE FROM ttrss_filters2_rules WHERE filter_id = ?");
|
||||||
@@ -623,11 +665,24 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function add(): void {
|
/**
|
||||||
$enabled = checkbox_to_sql_bool($_REQUEST["enabled"] ?? false);
|
* @param null|array{'src_filter_id': int, 'title': string, 'enabled': 0|1, 'match_any_rule': 0|1, 'inverse': 0|1} $props
|
||||||
$match_any_rule = checkbox_to_sql_bool($_REQUEST["match_any_rule"] ?? false);
|
*/
|
||||||
$title = clean($_REQUEST["title"]);
|
function add(?array $props = null): void {
|
||||||
$inverse = checkbox_to_sql_bool($_REQUEST["inverse"] ?? false);
|
if ($props === null) {
|
||||||
|
$src_filter_id = null;
|
||||||
|
$title = clean($_REQUEST['title']);
|
||||||
|
$enabled = checkbox_to_sql_bool($_REQUEST['enabled'] ?? false);
|
||||||
|
$match_any_rule = checkbox_to_sql_bool($_REQUEST['match_any_rule'] ?? false);
|
||||||
|
$inverse = checkbox_to_sql_bool($_REQUEST['inverse'] ?? false);
|
||||||
|
} else {
|
||||||
|
// see checkbox_to_sql_bool() for 0 vs false justification
|
||||||
|
$src_filter_id = $props['src_filter_id'];
|
||||||
|
$title = clean($props['title']);
|
||||||
|
$enabled = $props['enabled'];
|
||||||
|
$match_any_rule = $props['match_any_rule'];
|
||||||
|
$inverse = $props['inverse'];
|
||||||
|
}
|
||||||
|
|
||||||
$this->pdo->beginTransaction();
|
$this->pdo->beginTransaction();
|
||||||
|
|
||||||
@@ -635,22 +690,44 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
|
|
||||||
$sth = $this->pdo->prepare("INSERT INTO ttrss_filters2
|
$sth = $this->pdo->prepare("INSERT INTO ttrss_filters2
|
||||||
(owner_uid, match_any_rule, enabled, title, inverse) VALUES
|
(owner_uid, match_any_rule, enabled, title, inverse) VALUES
|
||||||
(?, ?, ?, ?, ?)");
|
(?, ?, ?, ?, ?) RETURNING id");
|
||||||
|
|
||||||
$sth->execute([$_SESSION['uid'], $match_any_rule, $enabled, $title, $inverse]);
|
$sth->execute([$_SESSION['uid'], $match_any_rule, $enabled, $title, $inverse]);
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT MAX(id) AS id FROM ttrss_filters2
|
|
||||||
WHERE owner_uid = ?");
|
|
||||||
$sth->execute([$_SESSION['uid']]);
|
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
$filter_id = $row['id'];
|
$filter_id = $row['id'];
|
||||||
$this->_save_rules_and_actions($filter_id);
|
|
||||||
|
if ($src_filter_id === null)
|
||||||
|
$this->_save_rules_and_actions($filter_id);
|
||||||
|
else
|
||||||
|
$this->_clone_rules_and_actions($filter_id, $src_filter_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->pdo->commit();
|
$this->pdo->commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clone(): void {
|
||||||
|
/** @var array<int, int> */
|
||||||
|
$src_filter_ids = array_map('intval', array_filter(explode(',', clean($_REQUEST['ids'] ?? ''))));
|
||||||
|
$new_filter_title = count($src_filter_ids) === 1 ? clean($_REQUEST['new_filter_title'] ?? null) : null;
|
||||||
|
|
||||||
|
$src_filters = ORM::for_table('ttrss_filters2')
|
||||||
|
->where('owner_uid', $_SESSION['uid'])
|
||||||
|
->where_id_in($src_filter_ids)
|
||||||
|
->find_many();
|
||||||
|
|
||||||
|
foreach ($src_filters as $src_filter) {
|
||||||
|
// see checkbox_to_sql_bool() for 0+1 justification
|
||||||
|
$this->add([
|
||||||
|
'src_filter_id' => $src_filter->id,
|
||||||
|
'title' => $new_filter_title ?? sprintf(__('Clone of %s'), $src_filter->title),
|
||||||
|
'enabled' => 0,
|
||||||
|
'match_any_rule' => $src_filter->match_any_rule ? 1 : 0,
|
||||||
|
'inverse' => $src_filter->inverse ? 1 : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function index(): void {
|
function index(): void {
|
||||||
if (array_key_exists("search", $_REQUEST)) {
|
if (array_key_exists("search", $_REQUEST)) {
|
||||||
$filter_search = clean($_REQUEST["search"]);
|
$filter_search = clean($_REQUEST["search"]);
|
||||||
@@ -685,6 +762,8 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
|
|
||||||
<button dojoType="dijit.form.Button" onclick="return Filters.edit()">
|
<button dojoType="dijit.form.Button" onclick="return Filters.edit()">
|
||||||
<?= __('Create filter') ?></button>
|
<?= __('Create filter') ?></button>
|
||||||
|
<button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').cloneSelectedFilters()">
|
||||||
|
<?= __('Clone') ?></button>
|
||||||
<button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').joinSelectedFilters()">
|
<button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').joinSelectedFilters()">
|
||||||
<?= __('Combine') ?></button>
|
<?= __('Combine') ?></button>
|
||||||
<button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').removeSelectedFilters()">
|
<button dojoType="dijit.form.Button" onclick="return dijit.byId('filterTree').removeSelectedFilters()">
|
||||||
@@ -714,7 +793,6 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function editrule(): void {
|
function editrule(): void {
|
||||||
/** @var array<int, int|string> */
|
|
||||||
$feed_ids = explode(",", clean($_REQUEST["ids"]));
|
$feed_ids = explode(",", clean($_REQUEST["ids"]));
|
||||||
|
|
||||||
print json_encode([
|
print json_encode([
|
||||||
@@ -723,58 +801,86 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int, string>
|
* @return array{'title': string, 'title_summary': string, 'actions_summary': string}
|
||||||
*/
|
*/
|
||||||
private function _get_name(int $id): array {
|
private function _get_details(int $id): array {
|
||||||
|
|
||||||
$sth = $this->pdo->prepare(
|
$filter = ORM::for_table("ttrss_filters2")
|
||||||
"SELECT title,match_any_rule,f.inverse AS inverse,COUNT(DISTINCT r.id) AS num_rules,COUNT(DISTINCT a.id) AS num_actions
|
->table_alias('f')
|
||||||
FROM ttrss_filters2 AS f LEFT JOIN ttrss_filters2_rules AS r
|
->select('f.title')
|
||||||
ON (r.filter_id = f.id)
|
->select('f.match_any_rule')
|
||||||
LEFT JOIN ttrss_filters2_actions AS a
|
->select('f.inverse')
|
||||||
ON (a.filter_id = f.id) WHERE f.id = ? GROUP BY f.title, f.match_any_rule, f.inverse");
|
->select_expr('COUNT(DISTINCT r.id)', 'num_rules')
|
||||||
$sth->execute([$id]);
|
->select_expr('COUNT(DISTINCT a.id)', 'num_actions')
|
||||||
|
->left_outer_join('ttrss_filters2_rules', ['r.filter_id', '=', 'f.id'], 'r')
|
||||||
|
->left_outer_join('ttrss_filters2_actions', ['a.filter_id', '=', 'f.id'], 'a')
|
||||||
|
->where('f.id', $id)
|
||||||
|
->group_by_expr('f.title, f.match_any_rule, f.inverse')
|
||||||
|
->find_one();
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
$title = $filter->title ?: __('[No caption]');
|
||||||
|
$title_summary = [
|
||||||
|
sprintf(
|
||||||
|
_ngettext("%s (%d rule)", "%s (%d rules)", (int) $filter->num_rules),
|
||||||
|
$title,
|
||||||
|
$filter->num_rules)];
|
||||||
|
|
||||||
$title = $row["title"];
|
if ($filter->match_any_rule) array_push($title_summary, __("matches any rule"));
|
||||||
$num_rules = $row["num_rules"];
|
if ($filter->inverse) array_push($title_summary, __("inverse"));
|
||||||
$num_actions = $row["num_actions"];
|
|
||||||
$match_any_rule = $row["match_any_rule"];
|
|
||||||
$inverse = $row["inverse"];
|
|
||||||
|
|
||||||
if (!$title) $title = __("[No caption]");
|
$actions = ORM::for_table("ttrss_filters2_actions")
|
||||||
|
->where("filter_id", $id)
|
||||||
|
->order_by_asc('id')
|
||||||
|
->find_many();
|
||||||
|
|
||||||
$title = sprintf(_ngettext("%s (%d rule)", "%s (%d rules)", (int) $num_rules), $title, $num_rules);
|
/** @var array<string> $actions_summary */
|
||||||
|
$actions_summary = [];
|
||||||
|
$cumulative_score = 0;
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2_actions
|
// we're going to show a summary adjustment so we skip individual score action descriptions here
|
||||||
WHERE filter_id = ? ORDER BY id LIMIT 1");
|
foreach ($actions as $action) {
|
||||||
$sth->execute([$id]);
|
if ($action->action_id == self::ACTION_SCORE) {
|
||||||
|
$cumulative_score += (int) $action->action_param;
|
||||||
$actions = "";
|
continue;
|
||||||
|
|
||||||
if ($line = $sth->fetch()) {
|
|
||||||
$actions = $this->_get_action_name($line);
|
|
||||||
|
|
||||||
$num_actions -= 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($match_any_rule) $title .= " (" . __("matches any rule") . ")";
|
array_push($actions_summary, "<li>" . self::_get_action_name($action) . "</li>");
|
||||||
if ($inverse) $title .= " (" . __("inverse") . ")";
|
|
||||||
|
|
||||||
if ($num_actions > 0)
|
|
||||||
$actions = sprintf(_ngettext("%s (+%d action)", "%s (+%d actions)", (int) $num_actions), $actions, $num_actions);
|
|
||||||
|
|
||||||
return [$title, $actions];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
// inject a fake action description using cumulative filter score
|
||||||
|
if ($cumulative_score != 0) {
|
||||||
|
array_unshift($actions_summary,
|
||||||
|
"<li>" . self::_get_action_name(["action_id" => self::ACTION_SCORE, "action_param" => $cumulative_score]) . "</li>");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($actions_summary) > self::MAX_ACTIONS_TO_DISPLAY) {
|
||||||
|
$actions_not_shown = count($actions_summary) - self::MAX_ACTIONS_TO_DISPLAY;
|
||||||
|
$actions_summary = array_slice($actions_summary, 0, self::MAX_ACTIONS_TO_DISPLAY);
|
||||||
|
|
||||||
|
array_push($actions_summary,
|
||||||
|
"<li class='text-muted'><em>" . sprintf(_ngettext("(+%d action)", "(+%d actions)", $actions_not_shown), $actions_not_shown)) . "</em></li>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $title,
|
||||||
|
'title_summary' => implode(', ', $title_summary),
|
||||||
|
'actions_summary' => implode('', $actions_summary),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function join(): void {
|
function join(): void {
|
||||||
/** @var array<int, int> */
|
/** @var array<int, int> */
|
||||||
$ids = array_map("intval", explode(",", clean($_REQUEST["ids"])));
|
$ids = array_map("intval", explode(",", clean($_REQUEST["ids"])));
|
||||||
|
|
||||||
|
// fail early if any provided filter IDs aren't owned by the current user
|
||||||
|
$unowned_filter_count = ORM::for_table('ttrss_filters2')
|
||||||
|
->where_in('id', $ids)
|
||||||
|
->where_not_equal('owner_uid', $_SESSION['uid'])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($unowned_filter_count)
|
||||||
|
return;
|
||||||
|
|
||||||
if (count($ids) > 1) {
|
if (count($ids) > 1) {
|
||||||
$base_id = array_shift($ids);
|
$base_id = array_shift($ids);
|
||||||
$ids_qmarks = arr_qmarks($ids);
|
$ids_qmarks = arr_qmarks($ids);
|
||||||
@@ -877,7 +983,7 @@ class Pref_Filters extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_pref(Prefs::ENABLE_FEED_CATS)) {
|
if (Prefs::get(Prefs::ENABLE_FEED_CATS, $_SESSION['uid'], $_SESSION['profile'] ?? null)) {
|
||||||
|
|
||||||
if (!$root_id) $root_id = null;
|
if (!$root_id) $root_id = null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
use chillerlan\QRCode;
|
|
||||||
|
|
||||||
class Pref_Prefs extends Handler_Protected {
|
class Pref_Prefs extends Handler_Protected {
|
||||||
/** @var array<Prefs::*, array<int, string>> */
|
/** @var array<Prefs::*, array<int, string>> */
|
||||||
private array $pref_help = [];
|
private array $pref_help = [];
|
||||||
@@ -177,7 +175,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
|
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
|
||||||
|
|
||||||
if (implements_interface($authenticator, "IAuthModule2")) {
|
if (implements_interface($authenticator, "IAuthModule2")) {
|
||||||
/** @var IAuthModule2 $authenticator */
|
/** @var Plugin&IAuthModule2 $authenticator */
|
||||||
print format_notice($authenticator->change_password($_SESSION["uid"], $old_pw, $new_pw));
|
print format_notice($authenticator->change_password($_SESSION["uid"], $old_pw, $new_pw));
|
||||||
} else {
|
} else {
|
||||||
print "ERROR: ".format_error("Function not supported by authentication module.");
|
print "ERROR: ".format_error("Function not supported by authentication module.");
|
||||||
@@ -185,6 +183,8 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveconfig(): void {
|
function saveconfig(): void {
|
||||||
|
$profile = $_SESSION['profile'] ?? null;
|
||||||
|
|
||||||
$boolean_prefs = explode(",", clean($_POST["boolean_prefs"]));
|
$boolean_prefs = explode(",", clean($_POST["boolean_prefs"]));
|
||||||
|
|
||||||
foreach ($boolean_prefs as $pref) {
|
foreach ($boolean_prefs as $pref) {
|
||||||
@@ -199,7 +199,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
|
|
||||||
switch ($pref_name) {
|
switch ($pref_name) {
|
||||||
case Prefs::DIGEST_PREFERRED_TIME:
|
case Prefs::DIGEST_PREFERRED_TIME:
|
||||||
if (get_pref(Prefs::DIGEST_PREFERRED_TIME) != $value) {
|
if (Prefs::get(Prefs::DIGEST_PREFERRED_TIME, $_SESSION['uid']) != $value) {
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("UPDATE ttrss_users SET
|
$sth = $this->pdo->prepare("UPDATE ttrss_users SET
|
||||||
last_digest_sent = NULL WHERE id = ?");
|
last_digest_sent = NULL WHERE id = ?");
|
||||||
@@ -207,14 +207,10 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Prefs::USER_LANGUAGE:
|
|
||||||
if (!$need_reload) $need_reload = $_SESSION["language"] != $value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Prefs::USER_CSS_THEME:
|
case Prefs::USER_CSS_THEME:
|
||||||
if (!$need_reload) $need_reload = get_pref($pref_name) != $value;
|
case Prefs::USER_LANGUAGE:
|
||||||
|
if (!$need_reload) $need_reload = Prefs::get($pref_name, $_SESSION['uid'], $profile) != $value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Prefs::BLACKLISTED_TAGS:
|
case Prefs::BLACKLISTED_TAGS:
|
||||||
$cats = FeedItem_Common::normalize_categories(explode(",", $value));
|
$cats = FeedItem_Common::normalize_categories(explode(",", $value));
|
||||||
asort($cats);
|
asort($cats);
|
||||||
@@ -223,7 +219,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Prefs::is_valid($pref_name)) {
|
if (Prefs::is_valid($pref_name)) {
|
||||||
Prefs::set($pref_name, $value, $_SESSION["uid"], $_SESSION["profile"] ?? null);
|
Prefs::set($pref_name, $value, $_SESSION['uid'], $profile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,7 +461,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
print "<img src=".($this->_get_otp_qrcode_img()).">";
|
print "<img src='{$this->_get_otp_qrcode_img()}' style='width: 25%'>";
|
||||||
|
|
||||||
print_notice("You will need to generate app passwords for API clients if you enable OTP.");
|
print_notice("You will need to generate app passwords for API clients if you enable OTP.");
|
||||||
|
|
||||||
@@ -595,10 +591,6 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($pref_name == Prefs::DEFAULT_SEARCH_LANGUAGE && Config::get(Config::DB_TYPE) != "pgsql") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($prefs_available[$pref_name])) {
|
if (isset($prefs_available[$pref_name])) {
|
||||||
|
|
||||||
$item = $prefs_available[$pref_name];
|
$item = $prefs_available[$pref_name];
|
||||||
@@ -655,7 +647,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
<?= \Controls\button_tag(\Controls\icon("palette") . " " . __("Customize"), "",
|
<?= \Controls\button_tag(\Controls\icon("palette") . " " . __("Customize"), "",
|
||||||
["onclick" => "Helpers.Prefs.customizeCSS()"]) ?>
|
["onclick" => "Helpers.Prefs.customizeCSS()"]) ?>
|
||||||
<?= \Controls\button_tag(\Controls\icon("open_in_new") . " " . __("More themes..."), "",
|
<?= \Controls\button_tag(\Controls\icon("open_in_new") . " " . __("More themes..."), "",
|
||||||
["class" => "alt-info", "onclick" => "window.open(\"https://tt-rss.org/Themes/\")"]) ?>
|
["class" => "alt-info", "onclick" => "window.open(\"https://github.com/tt-rss/tt-rss/wiki/Themes\")"]) ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
@@ -721,7 +713,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
|
|
||||||
print \Controls\button_tag(\Controls\icon("help") . " " . __("More info..."), "", [
|
print \Controls\button_tag(\Controls\icon("help") . " " . __("More info..."), "", [
|
||||||
"class" => "alt-info",
|
"class" => "alt-info",
|
||||||
"onclick" => "window.open('https://tt-rss.org/wiki/SSL%20Certificate%20Authentication')"]);
|
"onclick" => "window.open('https://github.com/tt-rss/tt-rss/wiki/SSL-Certificate-Authentication')"]);
|
||||||
|
|
||||||
} else if ($pref_name == Prefs::DIGEST_PREFERRED_TIME) {
|
} else if ($pref_name == Prefs::DIGEST_PREFERRED_TIME) {
|
||||||
print "<input dojoType=\"dijit.form.ValidationTextBox\"
|
print "<input dojoType=\"dijit.form.ValidationTextBox\"
|
||||||
@@ -802,7 +794,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
|
|
||||||
function getPluginsList(): void {
|
function getPluginsList(): void {
|
||||||
$system_enabled = array_map("trim", explode(",", (string)Config::get(Config::PLUGINS)));
|
$system_enabled = array_map("trim", explode(",", (string)Config::get(Config::PLUGINS)));
|
||||||
$user_enabled = array_map("trim", explode(",", get_pref(Prefs::_ENABLED_PLUGINS)));
|
$user_enabled = array_map('trim', explode(',', Prefs::get(Prefs::_ENABLED_PLUGINS, $_SESSION['uid'], $_SESSION['profile'] ?? null)));
|
||||||
|
|
||||||
$tmppluginhost = new PluginHost();
|
$tmppluginhost = new PluginHost();
|
||||||
$tmppluginhost->load_all($tmppluginhost::KIND_ALL, $_SESSION["uid"], true);
|
$tmppluginhost->load_all($tmppluginhost::KIND_ALL, $_SESSION["uid"], true);
|
||||||
@@ -886,7 +878,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
print_error(
|
print_error(
|
||||||
T_sprintf("The following plugins use per-feed content hooks. This may cause excessive data usage and origin server load resulting in a ban of your instance: <b>%s</b>" ,
|
T_sprintf("The following plugins use per-feed content hooks. This may cause excessive data usage and origin server load resulting in a ban of your instance: <b>%s</b>" ,
|
||||||
implode(", ", array_map(fn($plugin) => get_class($plugin), $feed_handlers))
|
implode(", ", array_map(fn($plugin) => get_class($plugin), $feed_handlers))
|
||||||
) . " (<a href='https://tt-rss.org/wiki/FeedHandlerPlugins' target='_blank'>".__("More info...")."</a>)"
|
) . " (<a href='https://github.com/tt-rss/tt-rss/wiki/Feed-Handler-Plugins' target='_blank'>".__("More info...")."</a>)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
?> -->
|
?> -->
|
||||||
@@ -898,7 +890,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
</div>
|
</div>
|
||||||
<div dojoType="dijit.layout.ContentPane" region="bottom">
|
<div dojoType="dijit.layout.ContentPane" region="bottom">
|
||||||
|
|
||||||
<button dojoType='dijit.form.Button' class="alt-info pull-right" onclick='window.open("https://tt-rss.org/Plugins/")'>
|
<button dojoType='dijit.form.Button' class="alt-info pull-right" onclick='window.open("https://github.com/tt-rss/tt-rss/wiki/Plugins")'>
|
||||||
<i class='material-icons'>help</i>
|
<i class='material-icons'>help</i>
|
||||||
<?= __("More info") ?>
|
<?= __("More info") ?>
|
||||||
</button>
|
</button>
|
||||||
@@ -976,7 +968,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
|
|
||||||
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
|
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
|
||||||
|
|
||||||
/** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
|
/** @var Auth_Internal|null $authenticator -- this is only here to make check_password() visible to static analyzer */
|
||||||
if ($authenticator->check_password($_SESSION["uid"], $password)) {
|
if ($authenticator->check_password($_SESSION["uid"], $password)) {
|
||||||
if (UserHelper::enable_otp($_SESSION["uid"], $otp_check)) {
|
if (UserHelper::enable_otp($_SESSION["uid"], $otp_check)) {
|
||||||
print "OK";
|
print "OK";
|
||||||
@@ -991,7 +983,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
function otpdisable(): void {
|
function otpdisable(): void {
|
||||||
$password = clean($_REQUEST["password"]);
|
$password = clean($_REQUEST["password"]);
|
||||||
|
|
||||||
/** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
|
/** @var Auth_Internal|null $authenticator -- this is only here to make check_password() visible to static analyzer */
|
||||||
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
|
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
|
||||||
|
|
||||||
if ($authenticator->check_password($_SESSION["uid"], $password)) {
|
if ($authenticator->check_password($_SESSION["uid"], $password)) {
|
||||||
@@ -1031,7 +1023,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
function setplugins(): void {
|
function setplugins(): void {
|
||||||
$plugins = array_filter($_REQUEST["plugins"] ?? [], 'clean');
|
$plugins = array_filter($_REQUEST["plugins"] ?? [], 'clean');
|
||||||
|
|
||||||
set_pref(Prefs::_ENABLED_PLUGINS, implode(",", $plugins));
|
Prefs::set(Prefs::_ENABLED_PLUGINS, implode(',', $plugins), $_SESSION['uid'], $_SESSION['profile'] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _get_plugin_version(Plugin $plugin): string {
|
function _get_plugin_version(Plugin $plugin): string {
|
||||||
@@ -1095,9 +1087,19 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
2 => ["pipe", "w"], // STDERR
|
2 => ["pipe", "w"], // STDERR
|
||||||
];
|
];
|
||||||
|
|
||||||
$proc = proc_open("git fetch -q origin -a && git log HEAD..origin/master --oneline", $descriptorspec, $pipes, $plugin_dir);
|
// TODO: clean up handling main+master
|
||||||
|
$proc = proc_open("git fetch -q origin -a && git log HEAD..origin/main --oneline", $descriptorspec, $pipes, $plugin_dir);
|
||||||
|
|
||||||
if (is_resource($proc)) {
|
if (is_resource($proc)) {
|
||||||
|
$rv = [
|
||||||
|
"stdout" => stream_get_contents($pipes[1]),
|
||||||
|
"stderr" => stream_get_contents($pipes[2]),
|
||||||
|
"git_status" => proc_close($proc),
|
||||||
|
];
|
||||||
|
$rv["need_update"] = !empty($rv["stdout"]);
|
||||||
|
} else {
|
||||||
|
$proc = proc_open("git fetch -q origin -a && git log HEAD..origin/master --oneline", $descriptorspec, $pipes, $plugin_dir);
|
||||||
|
|
||||||
$rv = [
|
$rv = [
|
||||||
"stdout" => stream_get_contents($pipes[1]),
|
"stdout" => stream_get_contents($pipes[1]),
|
||||||
"stderr" => stream_get_contents($pipes[2]),
|
"stderr" => stream_get_contents($pipes[2]),
|
||||||
@@ -1127,12 +1129,25 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
2 => ["pipe", "w"], // STDERR
|
2 => ["pipe", "w"], // STDERR
|
||||||
];
|
];
|
||||||
|
|
||||||
$proc = proc_open("git fetch origin -a && git log HEAD..origin/master --oneline && git pull --ff-only origin master", $descriptorspec, $pipes, $plugin_dir);
|
// TODO: clean up handling main+master
|
||||||
|
$proc = proc_open("git fetch origin -a && git log HEAD..origin/main --oneline && git pull --ff-only origin main", $descriptorspec, $pipes, $plugin_dir);
|
||||||
|
|
||||||
if (is_resource($proc)) {
|
if (is_resource($proc)) {
|
||||||
$rv["stdout"] = stream_get_contents($pipes[1]);
|
$rv = [
|
||||||
$rv["stderr"] = stream_get_contents($pipes[2]);
|
'stdout' => stream_get_contents($pipes[1]),
|
||||||
$rv["git_status"] = proc_close($proc);
|
'stderr' => stream_get_contents($pipes[2]),
|
||||||
|
'git_status' => proc_close($proc),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$proc = proc_open("git fetch origin -a && git log HEAD..origin/master --oneline && git pull --ff-only origin master", $descriptorspec, $pipes, $plugin_dir);
|
||||||
|
|
||||||
|
if (is_resource($proc)) {
|
||||||
|
$rv = [
|
||||||
|
'stdout' => stream_get_contents($pipes[1]),
|
||||||
|
'stderr' => stream_get_contents($pipes[2]),
|
||||||
|
'git_status' => proc_close($proc),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1282,9 +1297,12 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
* @return array<int, array{'name': string, 'description': string, 'topics': array<int, string>, 'html_url': string, 'clone_url': string, 'last_update': string}>
|
* @return array<int, array{'name': string, 'description': string, 'topics': array<int, string>, 'html_url': string, 'clone_url': string, 'last_update': string}>
|
||||||
*/
|
*/
|
||||||
private function _get_available_plugins(): array {
|
private function _get_available_plugins(): array {
|
||||||
|
// TODO: Get this working again. https://tt-rss.org/plugins.json won't exist after 2025-11-01 (probably).
|
||||||
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
|
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
|
||||||
$content = json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
|
// $content = json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
|
||||||
|
$content = false;
|
||||||
|
|
||||||
|
/** @phpstan-ignore if.alwaysFalse (intentionally disabling for now) */
|
||||||
if ($content) {
|
if ($content) {
|
||||||
return $content;
|
return $content;
|
||||||
}
|
}
|
||||||
@@ -1316,14 +1334,8 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
|
|
||||||
function updateLocalPlugins(): void {
|
function updateLocalPlugins(): void {
|
||||||
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
|
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
|
||||||
$plugins = explode(",", $_REQUEST["plugins"] ?? "");
|
$plugins = array_filter(explode(",", $_REQUEST["plugins"] ?? ""), "strlen");
|
||||||
|
|
||||||
if ($plugins !== false) {
|
|
||||||
$plugins = array_filter($plugins, 'strlen');
|
|
||||||
}
|
|
||||||
|
|
||||||
$root_dir = Config::get_self_dir();
|
$root_dir = Config::get_self_dir();
|
||||||
|
|
||||||
$rv = [];
|
$rv = [];
|
||||||
|
|
||||||
if ($plugins) {
|
if ($plugins) {
|
||||||
@@ -1356,7 +1368,7 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function customizeCSS(): void {
|
function customizeCSS(): void {
|
||||||
$value = get_pref(Prefs::USER_STYLESHEET);
|
$value = Prefs::get(Prefs::USER_STYLESHEET, $_SESSION['uid'], $_SESSION['profile'] ?? null);
|
||||||
$value = str_replace("<br/>", "\n", $value);
|
$value = str_replace("<br/>", "\n", $value);
|
||||||
|
|
||||||
print json_encode(["value" => $value]);
|
print json_encode(["value" => $value]);
|
||||||
@@ -1526,10 +1538,10 @@ class Pref_Prefs extends Handler_Protected {
|
|||||||
<?= htmlspecialchars($pass["title"]) ?>
|
<?= htmlspecialchars($pass["title"]) ?>
|
||||||
</td>
|
</td>
|
||||||
<td class='text-muted'>
|
<td class='text-muted'>
|
||||||
<?= TimeHelper::make_local_datetime($pass['created'], false) ?>
|
<?= TimeHelper::make_local_datetime($pass['created']) ?>
|
||||||
</td>
|
</td>
|
||||||
<td class='text-muted'>
|
<td class='text-muted'>
|
||||||
<?= TimeHelper::make_local_datetime($pass['last_used'], false) ?>
|
<?= TimeHelper::make_local_datetime($pass['last_used']) ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|||||||
@@ -28,6 +28,41 @@ class Pref_System extends Handler_Administrative {
|
|||||||
print json_encode(['rc' => $rc, 'error' => $mailer->error()]);
|
print json_encode(['rc' => $rc, 'error' => $mailer->error()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getscheduledtasks(): void {
|
||||||
|
?>
|
||||||
|
<table width='100%' class='event-log'>
|
||||||
|
<tr>
|
||||||
|
<th><?= __("Task name") ?></th>
|
||||||
|
<th><?= __("Schedule") ?></th>
|
||||||
|
<th><?= __("Last executed") ?></th>
|
||||||
|
<th><?= __("Duration (seconds)") ?></th>
|
||||||
|
<th><?= __("Return code") ?></th>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$task_records = ORM::for_table('ttrss_scheduled_tasks')
|
||||||
|
->order_by_asc(['last_cron_expression', 'task_name'])
|
||||||
|
->find_many();
|
||||||
|
|
||||||
|
foreach ($task_records as $task) {
|
||||||
|
$row_style = $task->last_rc === 0 ? 'text-success' : 'text-error';
|
||||||
|
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="<?= $row_style ?>"><?= $task->task_name ?></td>
|
||||||
|
<td><?= $task->last_cron_expression ?></td>
|
||||||
|
<td><?= TimeHelper::make_local_datetime($task->last_run) ?></td>
|
||||||
|
<td><?= $task->last_duration ?></td>
|
||||||
|
<td><?= $task->last_rc ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
function getphpinfo(): void {
|
function getphpinfo(): void {
|
||||||
ob_start();
|
ob_start();
|
||||||
phpinfo();
|
phpinfo();
|
||||||
@@ -38,16 +73,11 @@ class Pref_System extends Handler_Administrative {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function _log_viewer(int $page, int $severity): void {
|
private function _log_viewer(int $page, int $severity): void {
|
||||||
$errno_values = [];
|
$errno_values = match ($severity) {
|
||||||
|
E_USER_ERROR => [E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR],
|
||||||
switch ($severity) {
|
E_USER_WARNING => [E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR, E_WARNING, E_USER_WARNING, E_DEPRECATED, E_USER_DEPRECATED],
|
||||||
case E_USER_ERROR:
|
default => [],
|
||||||
$errno_values = [ E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR ];
|
};
|
||||||
break;
|
|
||||||
case E_USER_WARNING:
|
|
||||||
$errno_values = [ E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR, E_WARNING, E_USER_WARNING, E_DEPRECATED, E_USER_DEPRECATED ];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count($errno_values) > 0) {
|
if (count($errno_values) > 0) {
|
||||||
$errno_qmarks = arr_qmarks($errno_values);
|
$errno_qmarks = arr_qmarks($errno_values);
|
||||||
@@ -148,7 +178,7 @@ class Pref_System extends Handler_Administrative {
|
|||||||
<td class='errstr'><?= $line["errstr"] . "\n" . $line["context"] ?></td>
|
<td class='errstr'><?= $line["errstr"] . "\n" . $line["context"] ?></td>
|
||||||
<td class='login'><?= $line["login"] ?></td>
|
<td class='login'><?= $line["login"] ?></td>
|
||||||
<td class='timestamp'>
|
<td class='timestamp'>
|
||||||
<?= TimeHelper::make_local_datetime($line["created_at"], false) ?>
|
<?= TimeHelper::make_local_datetime($line['created_at']) ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
@@ -210,6 +240,17 @@ class Pref_System extends Handler_Administrative {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div dojoType='dijit.layout.AccordionPane' title='<i class="material-icons">alarm</i> <?= __('Scheduled tasks') ?>'>
|
||||||
|
<script type='dojo/method' event='onSelected' args='evt'>
|
||||||
|
if (this.domNode.querySelector('.loading'))
|
||||||
|
window.setTimeout(() => {
|
||||||
|
xhr.post("backend.php", {op: 'Pref_System', method: 'getscheduledtasks'}, (reply) => {
|
||||||
|
this.attr('content', `<div class='phpinfo'>${reply}</div>`);
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
</script>
|
||||||
|
<span class='loading'><?= __("Loading, please wait...") ?></span>
|
||||||
|
</div>
|
||||||
<div dojoType='dijit.layout.AccordionPane' title='<i class="material-icons">info</i> <?= __('PHP Information') ?>'>
|
<div dojoType='dijit.layout.AccordionPane' title='<i class="material-icons">info</i> <?= __('PHP Information') ?>'>
|
||||||
<script type='dojo/method' event='onSelected' args='evt'>
|
<script type='dojo/method' event='onSelected' args='evt'>
|
||||||
if (this.domNode.querySelector('.loading'))
|
if (this.domNode.querySelector('.loading'))
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class Pref_Users extends Handler_Administrative {
|
|||||||
|
|
||||||
function edit(): void {
|
function edit(): void {
|
||||||
$user = ORM::for_table('ttrss_users')
|
$user = ORM::for_table('ttrss_users')
|
||||||
->select_expr("id,login,access_level,email,full_name,otp_enabled")
|
->select_many('id', 'login', 'access_level', 'email', 'full_name', 'otp_enabled')
|
||||||
->find_one((int)$_REQUEST["id"])
|
->find_one((int)$_REQUEST["id"])
|
||||||
->as_array();
|
->as_array();
|
||||||
|
|
||||||
@@ -23,32 +23,25 @@ class Pref_Users extends Handler_Administrative {
|
|||||||
function userdetails(): void {
|
function userdetails(): void {
|
||||||
$id = (int) clean($_REQUEST["id"]);
|
$id = (int) clean($_REQUEST["id"]);
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT login,
|
$user = ORM::for_table('ttrss_users')
|
||||||
".SUBSTRING_FOR_DATE."(last_login,1,16) AS last_login,
|
->table_alias('u')
|
||||||
access_level,
|
->select_many('u.login', 'u.access_level')
|
||||||
(SELECT COUNT(int_id) FROM ttrss_user_entries
|
->select_many_expr([
|
||||||
WHERE owner_uid = id) AS stored_articles,
|
'created' => 'SUBSTRING_FOR_DATE(u.created,1,16)',
|
||||||
".SUBSTRING_FOR_DATE."(created,1,16) AS created
|
'last_login' => 'SUBSTRING_FOR_DATE(u.last_login,1,16)',
|
||||||
FROM ttrss_users
|
'stored_articles' => '(SELECT COUNT(ue.int_id) FROM ttrss_user_entries ue WHERE ue.owner_uid = u.id)',
|
||||||
WHERE id = ?");
|
])
|
||||||
$sth->execute([$id]);
|
->find_one($id);
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($user) {
|
||||||
|
$created = TimeHelper::make_local_datetime($user->created);
|
||||||
|
$last_login = TimeHelper::make_local_datetime($user->last_login);
|
||||||
|
|
||||||
$last_login = TimeHelper::make_local_datetime(
|
$user_owned_feeds = ORM::for_table('ttrss_feeds')
|
||||||
$row["last_login"], true);
|
->select_many('id', 'title', 'site_url')
|
||||||
|
->where('owner_uid', $id)
|
||||||
$created = TimeHelper::make_local_datetime(
|
->order_by_expr('LOWER(title)')
|
||||||
$row["created"], true);
|
->find_many();
|
||||||
|
|
||||||
$stored_articles = $row["stored_articles"];
|
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("SELECT COUNT(id) as num_feeds FROM ttrss_feeds
|
|
||||||
WHERE owner_uid = ?");
|
|
||||||
$sth->execute([$id]);
|
|
||||||
$row = $sth->fetch();
|
|
||||||
|
|
||||||
$num_feeds = $row["num_feeds"];
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -64,31 +57,25 @@ class Pref_Users extends Handler_Administrative {
|
|||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><?= __('Subscribed feeds') ?>:</label>
|
<label><?= __('Subscribed feeds') ?>:</label>
|
||||||
<?= $num_feeds ?>
|
<?= count($user_owned_feeds) ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><?= __('Stored articles') ?>:</label>
|
<label><?= __('Stored articles') ?>:</label>
|
||||||
<?= $stored_articles ?>
|
<?= $user->stored_articles ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<?php
|
|
||||||
$sth = $this->pdo->prepare("SELECT id,title,site_url FROM ttrss_feeds
|
|
||||||
WHERE owner_uid = ? ORDER BY title");
|
|
||||||
$sth->execute([$id]);
|
|
||||||
?>
|
|
||||||
|
|
||||||
<ul class="panel panel-scrollable list list-unstyled">
|
<ul class="panel panel-scrollable list list-unstyled">
|
||||||
<?php while ($row = $sth->fetch()) { ?>
|
<?php foreach ($user_owned_feeds as $feed) { ?>
|
||||||
<li>
|
<li>
|
||||||
<?php
|
<?php
|
||||||
$icon_url = Feeds::_get_icon_url($row['id'], 'images/blank_icon.gif');
|
$icon_url = Feeds::_get_icon_url($feed->id, 'images/blank_icon.gif');
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<img class="icon" src="<?= htmlspecialchars($icon_url) ?>">
|
<img class="icon" src="<?= htmlspecialchars($icon_url) ?>">
|
||||||
|
|
||||||
<a target="_blank" href="<?= htmlspecialchars($row["site_url"]) ?>">
|
<a target="_blank" href="<?= htmlspecialchars($feed->site_url) ?>">
|
||||||
<?= htmlspecialchars($row["title"]) ?>
|
<?= htmlspecialchars($feed->title) ?>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
@@ -136,14 +123,9 @@ class Pref_Users extends Handler_Administrative {
|
|||||||
|
|
||||||
foreach ($ids as $id) {
|
foreach ($ids as $id) {
|
||||||
if ($id != $_SESSION["uid"] && $id != 1) {
|
if ($id != $_SESSION["uid"] && $id != 1) {
|
||||||
$sth = $this->pdo->prepare("DELETE FROM ttrss_tags WHERE owner_uid = ?");
|
ORM::for_table('ttrss_tags')->where('owner_uid', $id)->delete_many();
|
||||||
$sth->execute([$id]);
|
ORM::for_table('ttrss_feeds')->where('owner_uid', $id)->delete_many();
|
||||||
|
ORM::for_table('ttrss_users')->where('id', $id)->delete_many();
|
||||||
$sth = $this->pdo->prepare("DELETE FROM ttrss_feeds WHERE owner_uid = ?");
|
|
||||||
$sth->execute([$id]);
|
|
||||||
|
|
||||||
$sth = $this->pdo->prepare("DELETE FROM ttrss_users WHERE id = ?");
|
|
||||||
$sth->execute([$id]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,8 +264,8 @@ class Pref_Users extends Handler_Administrative {
|
|||||||
</td>
|
</td>
|
||||||
<td><?= $access_level_names[$user["access_level"]] ?></td>
|
<td><?= $access_level_names[$user["access_level"]] ?></td>
|
||||||
<td><?= $user["num_feeds"] ?></td>
|
<td><?= $user["num_feeds"] ?></td>
|
||||||
<td class='text-muted'><?= TimeHelper::make_local_datetime($user["created"], false) ?></td>
|
<td class='text-muted'><?= TimeHelper::make_local_datetime($user['created']) ?></td>
|
||||||
<td class='text-muted'><?= TimeHelper::make_local_datetime($user["last_login"], false) ?></td>
|
<td class='text-muted'><?= TimeHelper::make_local_datetime($user['last_login']) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -144,14 +144,12 @@ class Prefs {
|
|||||||
Prefs::_PREFS_MIGRATED
|
Prefs::_PREFS_MIGRATED
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @var Prefs|null */
|
private static ?Prefs $instance = null;
|
||||||
private static $instance;
|
|
||||||
|
|
||||||
/** @var array<string, bool|int|string> */
|
/** @var array<string, bool|int|string> */
|
||||||
private $cache = [];
|
private array $cache = [];
|
||||||
|
|
||||||
/** @var PDO */
|
private ?PDO $pdo = null;
|
||||||
private $pdo;
|
|
||||||
|
|
||||||
public static function get_instance() : Prefs {
|
public static function get_instance() : Prefs {
|
||||||
if (self::$instance == null)
|
if (self::$instance == null)
|
||||||
@@ -164,10 +162,7 @@ class Prefs {
|
|||||||
return isset(self::_DEFAULTS[$pref_name]);
|
return isset(self::_DEFAULTS[$pref_name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static function get_default(string $pref_name): bool|int|null|string {
|
||||||
* @return bool|int|null|string
|
|
||||||
*/
|
|
||||||
static function get_default(string $pref_name) {
|
|
||||||
if (self::is_valid($pref_name))
|
if (self::is_valid($pref_name))
|
||||||
return self::_DEFAULTS[$pref_name][0];
|
return self::_DEFAULTS[$pref_name][0];
|
||||||
else
|
else
|
||||||
@@ -193,14 +188,14 @@ class Prefs {
|
|||||||
/**
|
/**
|
||||||
* @return array<int, array<string, bool|int|null|string>>
|
* @return array<int, array<string, bool|int|null|string>>
|
||||||
*/
|
*/
|
||||||
static function get_all(int $owner_uid, ?int $profile_id = null) {
|
static function get_all(int $owner_uid, ?int $profile_id = null): array {
|
||||||
return self::get_instance()->_get_all($owner_uid, $profile_id);
|
return self::get_instance()->_get_all($owner_uid, $profile_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int, array<string, bool|int|null|string>>
|
* @return array<int, array<string, bool|int|null|string>>
|
||||||
*/
|
*/
|
||||||
private function _get_all(int $owner_uid, ?int $profile_id = null) {
|
private function _get_all(int $owner_uid, ?int $profile_id = null): array {
|
||||||
$rv = [];
|
$rv = [];
|
||||||
|
|
||||||
$ref = new ReflectionClass(get_class($this));
|
$ref = new ReflectionClass(get_class($this));
|
||||||
@@ -247,17 +242,11 @@ class Prefs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static function get(string $pref_name, int $owner_uid, ?int $profile_id = null): bool|int|null|string {
|
||||||
* @return bool|int|null|string
|
|
||||||
*/
|
|
||||||
static function get(string $pref_name, int $owner_uid, ?int $profile_id = null) {
|
|
||||||
return self::get_instance()->_get($pref_name, $owner_uid, $profile_id);
|
return self::get_instance()->_get($pref_name, $owner_uid, $profile_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function _get(string $pref_name, int $owner_uid, ?int $profile_id): bool|int|null|string {
|
||||||
* @return bool|int|null|string
|
|
||||||
*/
|
|
||||||
private function _get(string $pref_name, int $owner_uid, ?int $profile_id) {
|
|
||||||
if (isset(self::_DEFAULTS[$pref_name])) {
|
if (isset(self::_DEFAULTS[$pref_name])) {
|
||||||
if (!$profile_id || in_array($pref_name, self::_PROFILE_BLACKLIST)) $profile_id = null;
|
if (!$profile_id || in_array($pref_name, self::_PROFILE_BLACKLIST)) $profile_id = null;
|
||||||
|
|
||||||
@@ -298,34 +287,22 @@ class Prefs {
|
|||||||
return isset($this->cache[$cache_key]);
|
return isset($this->cache[$cache_key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function _get_cache(string $pref_name, int $owner_uid, ?int $profile_id): bool|int|null|string {
|
||||||
* @return bool|int|null|string
|
|
||||||
*/
|
|
||||||
private function _get_cache(string $pref_name, int $owner_uid, ?int $profile_id) {
|
|
||||||
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
|
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
|
||||||
return $this->cache[$cache_key] ?? null;
|
return $this->cache[$cache_key] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function _set_cache(string $pref_name, bool|int|string $value, int $owner_uid, ?int $profile_id): void {
|
||||||
* @param bool|int|string $value
|
|
||||||
*/
|
|
||||||
private function _set_cache(string $pref_name, $value, int $owner_uid, ?int $profile_id): void {
|
|
||||||
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
|
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
|
||||||
|
|
||||||
$this->cache[$cache_key] = $value;
|
$this->cache[$cache_key] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static function set(string $pref_name, bool|int|string $value, int $owner_uid, ?int $profile_id, bool $strip_tags = true): bool {
|
||||||
* @param bool|int|string $value
|
|
||||||
*/
|
|
||||||
static function set(string $pref_name, $value, int $owner_uid, ?int $profile_id, bool $strip_tags = true): bool {
|
|
||||||
return self::get_instance()->_set($pref_name, $value, $owner_uid, $profile_id);
|
return self::get_instance()->_set($pref_name, $value, $owner_uid, $profile_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function _set(string $pref_name, bool|int|string $value, int $owner_uid, ?int $profile_id, bool $strip_tags = true): bool {
|
||||||
* @param bool|int|string $value
|
|
||||||
*/
|
|
||||||
private function _set(string $pref_name, $value, int $owner_uid, ?int $profile_id, bool $strip_tags = true): bool {
|
|
||||||
if (!$profile_id) $profile_id = null;
|
if (!$profile_id) $profile_id = null;
|
||||||
|
|
||||||
if ($profile_id && in_array($pref_name, self::_PROFILE_BLACKLIST))
|
if ($profile_id && in_array($pref_name, self::_PROFILE_BLACKLIST))
|
||||||
|
|||||||
191
classes/RPC.php
191
classes/RPC.php
@@ -23,7 +23,7 @@ class RPC extends Handler_Protected {
|
|||||||
|
|
||||||
for ($i = 0; $i < $l10n->total; $i++) {
|
for ($i = 0; $i < $l10n->total; $i++) {
|
||||||
if (isset($l10n->table_originals[$i * 2 + 2]) && $orig = $l10n->get_original_string($i)) {
|
if (isset($l10n->table_originals[$i * 2 + 2]) && $orig = $l10n->get_original_string($i)) {
|
||||||
if(strpos($orig, "\000") !== false) { // Plural forms
|
if(str_contains($orig, "\000")) { // Plural forms
|
||||||
$key = explode(chr(0), $orig);
|
$key = explode(chr(0), $orig);
|
||||||
|
|
||||||
$rv[$key[0]] = _ngettext($key[0], $key[1], 1); // Singular
|
$rv[$key[0]] = _ngettext($key[0], $key[1], 1); // Singular
|
||||||
@@ -42,8 +42,9 @@ class RPC extends Handler_Protected {
|
|||||||
|
|
||||||
function togglepref(): void {
|
function togglepref(): void {
|
||||||
$key = clean($_REQUEST["key"]);
|
$key = clean($_REQUEST["key"]);
|
||||||
set_pref($key, !get_pref($key));
|
$profile = $_SESSION['profile'] ?? null;
|
||||||
$value = get_pref($key);
|
Prefs::set($key, !Prefs::get($key, $_SESSION['uid'], $profile), $_SESSION['uid'], $profile);
|
||||||
|
$value = Prefs::get($key, $_SESSION['uid'], $profile);
|
||||||
|
|
||||||
print json_encode(array("param" =>$key, "value" => $value));
|
print json_encode(array("param" =>$key, "value" => $value));
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ class RPC extends Handler_Protected {
|
|||||||
$key = clean($_REQUEST['key']);
|
$key = clean($_REQUEST['key']);
|
||||||
$value = $_REQUEST['value'];
|
$value = $_REQUEST['value'];
|
||||||
|
|
||||||
set_pref($key, $value, $_SESSION["uid"], $key != 'USER_STYLESHEET');
|
Prefs::set($key, $value, $_SESSION['uid'], $_SESSION['profile'] ?? null, $key != 'USER_STYLESHEET');
|
||||||
|
|
||||||
print json_encode(array("param" =>$key, "value" => $value));
|
print json_encode(array("param" =>$key, "value" => $value));
|
||||||
}
|
}
|
||||||
@@ -68,6 +69,8 @@ class RPC extends Handler_Protected {
|
|||||||
|
|
||||||
$sth->execute([$mark, $id, $_SESSION['uid']]);
|
$sth->execute([$mark, $id, $_SESSION['uid']]);
|
||||||
|
|
||||||
|
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_MARK_TOGGLED, [$id]);
|
||||||
|
|
||||||
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +82,6 @@ class RPC extends Handler_Protected {
|
|||||||
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
|
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
|
||||||
$sth->execute([...$ids, $_SESSION['uid']]);
|
$sth->execute([...$ids, $_SESSION['uid']]);
|
||||||
|
|
||||||
Article::_purge_orphans();
|
|
||||||
|
|
||||||
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +95,8 @@ class RPC extends Handler_Protected {
|
|||||||
|
|
||||||
$sth->execute([$pub, $id, $_SESSION['uid']]);
|
$sth->execute([$pub, $id, $_SESSION['uid']]);
|
||||||
|
|
||||||
|
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_PUBLISH_TOGGLED, [$id]);
|
||||||
|
|
||||||
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
print json_encode(array("message" => "UPDATE_COUNTERS"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +109,6 @@ class RPC extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAllCounters(): void {
|
function getAllCounters(): void {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
@$seq = (int) $_REQUEST['seq'];
|
@$seq = (int) $_REQUEST['seq'];
|
||||||
|
|
||||||
$feed_id_count = (int) ($_REQUEST["feed_id_count"] ?? -1);
|
$feed_id_count = (int) ($_REQUEST["feed_id_count"] ?? -1);
|
||||||
@@ -126,7 +127,8 @@ class RPC extends Handler_Protected {
|
|||||||
else
|
else
|
||||||
$label_ids = array_map("intval", clean($_REQUEST["label_ids"] ?? []));
|
$label_ids = array_map("intval", clean($_REQUEST["label_ids"] ?? []));
|
||||||
|
|
||||||
$counters = is_array($feed_ids) && !get_pref(Prefs::DISABLE_CONDITIONAL_COUNTERS) ?
|
$counters = is_array($feed_ids)
|
||||||
|
&& !Prefs::get(Prefs::DISABLE_CONDITIONAL_COUNTERS, $_SESSION['uid'], $_SESSION['profile'] ?? null) ?
|
||||||
Counters::get_conditional($feed_ids, $label_ids) : Counters::get_all();
|
Counters::get_conditional($feed_ids, $label_ids) : Counters::get_all();
|
||||||
|
|
||||||
$reply = [
|
$reply = [
|
||||||
@@ -134,7 +136,6 @@ class RPC extends Handler_Protected {
|
|||||||
'seq' => $seq
|
'seq' => $seq
|
||||||
];
|
];
|
||||||
|
|
||||||
$span->end();
|
|
||||||
print json_encode($reply);
|
print json_encode($reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,8 +177,6 @@ class RPC extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanityCheck(): void {
|
function sanityCheck(): void {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$_SESSION["hasSandbox"] = self::_param_to_bool($_REQUEST["hasSandbox"] ?? false);
|
$_SESSION["hasSandbox"] = self::_param_to_bool($_REQUEST["hasSandbox"] ?? false);
|
||||||
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
|
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
|
||||||
|
|
||||||
@@ -209,8 +208,6 @@ class RPC extends Handler_Protected {
|
|||||||
} else {
|
} else {
|
||||||
print Errors::to_json($error, $error_params);
|
print Errors::to_json($error, $error_params);
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*function completeLabels() {
|
/*function completeLabels() {
|
||||||
@@ -248,110 +245,11 @@ class RPC extends Handler_Protected {
|
|||||||
function setWidescreen(): void {
|
function setWidescreen(): void {
|
||||||
$wide = (int) clean($_REQUEST["wide"]);
|
$wide = (int) clean($_REQUEST["wide"]);
|
||||||
|
|
||||||
set_pref(Prefs::WIDESCREEN_MODE, $wide);
|
Prefs::set(Prefs::WIDESCREEN_MODE, $wide, $_SESSION['uid'], $_SESSION['profile'] ?? null);
|
||||||
|
|
||||||
print json_encode(["wide" => $wide]);
|
print json_encode(["wide" => $wide]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static function updaterandomfeed_real(): void {
|
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$default_interval = (int) Prefs::get_default(Prefs::DEFAULT_UPDATE_INTERVAL);
|
|
||||||
|
|
||||||
// Test if the feed need a update (update interval exceded).
|
|
||||||
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
|
||||||
$update_limit_qpart = "AND ((
|
|
||||||
update_interval = 0
|
|
||||||
AND (p.value IS NULL OR p.value != '-1')
|
|
||||||
AND last_updated < NOW() - CAST((COALESCE(p.value, '$default_interval') || ' minutes') AS INTERVAL)
|
|
||||||
) OR (
|
|
||||||
update_interval > 0
|
|
||||||
AND last_updated < NOW() - CAST((update_interval || ' minutes') AS INTERVAL)
|
|
||||||
) OR (
|
|
||||||
update_interval >= 0
|
|
||||||
AND (p.value IS NULL OR p.value != '-1')
|
|
||||||
AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
|
|
||||||
))";
|
|
||||||
} else {
|
|
||||||
$update_limit_qpart = "AND ((
|
|
||||||
update_interval = 0
|
|
||||||
AND (p.value IS NULL OR p.value != '-1')
|
|
||||||
AND last_updated < DATE_SUB(NOW(), INTERVAL CONVERT(COALESCE(p.value, '$default_interval'), SIGNED INTEGER) MINUTE)
|
|
||||||
) OR (
|
|
||||||
update_interval > 0
|
|
||||||
AND last_updated < DATE_SUB(NOW(), INTERVAL update_interval MINUTE)
|
|
||||||
) OR (
|
|
||||||
update_interval >= 0
|
|
||||||
AND (p.value IS NULL OR p.value != '-1')
|
|
||||||
AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
|
|
||||||
))";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test if feed is currently being updated by another process.
|
|
||||||
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
|
||||||
$updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < NOW() - INTERVAL '5 minutes')";
|
|
||||||
} else {
|
|
||||||
$updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < DATE_SUB(NOW(), INTERVAL 5 MINUTE))";
|
|
||||||
}
|
|
||||||
|
|
||||||
$random_qpart = Db::sql_random_function();
|
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
|
||||||
|
|
||||||
// we could be invoked from public.php with no active session
|
|
||||||
if (!empty($_SESSION["uid"])) {
|
|
||||||
$owner_check_qpart = "AND f.owner_uid = ".$pdo->quote($_SESSION["uid"]);
|
|
||||||
} else {
|
|
||||||
$owner_check_qpart = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = "SELECT f.feed_url,f.id
|
|
||||||
FROM
|
|
||||||
ttrss_feeds f, ttrss_users u LEFT JOIN ttrss_user_prefs2 p ON
|
|
||||||
(p.owner_uid = u.id AND profile IS NULL AND pref_name = 'DEFAULT_UPDATE_INTERVAL')
|
|
||||||
WHERE
|
|
||||||
f.owner_uid = u.id AND
|
|
||||||
u.access_level NOT IN (".sprintf("%d, %d", UserHelper::ACCESS_LEVEL_DISABLED, UserHelper::ACCESS_LEVEL_READONLY).")
|
|
||||||
$owner_check_qpart
|
|
||||||
$update_limit_qpart
|
|
||||||
$updstart_thresh_qpart
|
|
||||||
ORDER BY $random_qpart LIMIT 30";
|
|
||||||
|
|
||||||
$res = $pdo->query($query);
|
|
||||||
|
|
||||||
$num_updated = 0;
|
|
||||||
|
|
||||||
$tstart = time();
|
|
||||||
|
|
||||||
while ($line = $res->fetch()) {
|
|
||||||
$feed_id = $line["id"];
|
|
||||||
|
|
||||||
if (time() - $tstart < ini_get("max_execution_time") * 0.7) {
|
|
||||||
RSSUtils::update_rss_feed($feed_id, true);
|
|
||||||
++$num_updated;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purge orphans and cleanup tags
|
|
||||||
Article::_purge_orphans();
|
|
||||||
//cleanup_tags(14, 50000);
|
|
||||||
|
|
||||||
if ($num_updated > 0) {
|
|
||||||
print json_encode(array("message" => "UPDATE_COUNTERS",
|
|
||||||
"num_updated" => $num_updated));
|
|
||||||
} else {
|
|
||||||
print json_encode(array("message" => "NOTHING_TO_UPDATE"));
|
|
||||||
}
|
|
||||||
|
|
||||||
$span->end();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updaterandomfeed(): void {
|
|
||||||
self::updaterandomfeed_real();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, int> $ids
|
* @param array<int, int> $ids
|
||||||
*/
|
*/
|
||||||
@@ -374,6 +272,8 @@ class RPC extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$sth->execute([...$ids, $_SESSION['uid']]);
|
$sth->execute([...$ids, $_SESSION['uid']]);
|
||||||
|
|
||||||
|
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_MARK_TOGGLED, $ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -398,11 +298,11 @@ class RPC extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$sth->execute([...$ids, $_SESSION['uid']]);
|
$sth->execute([...$ids, $_SESSION['uid']]);
|
||||||
|
|
||||||
|
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_PUBLISH_TOGGLED, $ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
function log(): void {
|
function log(): void {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$msg = clean($_REQUEST['msg'] ?? "");
|
$msg = clean($_REQUEST['msg'] ?? "");
|
||||||
$file = basename(clean($_REQUEST['file'] ?? ""));
|
$file = basename(clean($_REQUEST['file'] ?? ""));
|
||||||
$line = (int) clean($_REQUEST['line'] ?? 0);
|
$line = (int) clean($_REQUEST['line'] ?? 0);
|
||||||
@@ -414,13 +314,9 @@ class RPC extends Handler_Protected {
|
|||||||
|
|
||||||
echo json_encode(array("message" => "HOST_ERROR_LOGGED"));
|
echo json_encode(array("message" => "HOST_ERROR_LOGGED"));
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkforupdates(): void {
|
function checkforupdates(): void {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$rv = ["changeset" => [], "plugins" => []];
|
$rv = ["changeset" => [], "plugins" => []];
|
||||||
|
|
||||||
$version = Config::get_version(false);
|
$version = Config::get_version(false);
|
||||||
@@ -428,9 +324,12 @@ class RPC extends Handler_Protected {
|
|||||||
$git_timestamp = $version["timestamp"] ?? false;
|
$git_timestamp = $version["timestamp"] ?? false;
|
||||||
$git_commit = $version["commit"] ?? false;
|
$git_commit = $version["commit"] ?? false;
|
||||||
|
|
||||||
|
// TODO: Get this working again. https://tt-rss.org/version.json won't exist after 2025-11-01 (probably).
|
||||||
if (Config::get(Config::CHECK_FOR_UPDATES) && $_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && $git_timestamp) {
|
if (Config::get(Config::CHECK_FOR_UPDATES) && $_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && $git_timestamp) {
|
||||||
$content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]);
|
// $content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]);
|
||||||
|
$content = false;
|
||||||
|
|
||||||
|
/** @phpstan-ignore if.alwaysFalse (intentionally disabling for now) */
|
||||||
if ($content) {
|
if ($content) {
|
||||||
$content = json_decode($content, true);
|
$content = json_decode($content, true);
|
||||||
|
|
||||||
@@ -446,8 +345,6 @@ class RPC extends Handler_Protected {
|
|||||||
$rv["plugins"] = Pref_Prefs::_get_updated_plugins();
|
$rv["plugins"] = Pref_Prefs::_get_updated_plugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
print json_encode($rv);
|
print json_encode($rv);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,8 +352,7 @@ class RPC extends Handler_Protected {
|
|||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
private function _make_init_params(): array {
|
private function _make_init_params(): array {
|
||||||
$span = Tracer::start(__METHOD__);
|
$profile = $_SESSION['profile'] ?? null;
|
||||||
|
|
||||||
$params = array();
|
$params = array();
|
||||||
|
|
||||||
foreach ([Prefs::ON_CATCHUP_SHOW_NEXT_FEED, Prefs::HIDE_READ_FEEDS,
|
foreach ([Prefs::ON_CATCHUP_SHOW_NEXT_FEED, Prefs::HIDE_READ_FEEDS,
|
||||||
@@ -465,21 +361,21 @@ class RPC extends Handler_Protected {
|
|||||||
Prefs::FRESH_ARTICLE_MAX_AGE, Prefs::HIDE_READ_SHOWS_SPECIAL,
|
Prefs::FRESH_ARTICLE_MAX_AGE, Prefs::HIDE_READ_SHOWS_SPECIAL,
|
||||||
Prefs::COMBINED_DISPLAY_MODE, Prefs::DEBUG_HEADLINE_IDS, Prefs::CDM_ENABLE_GRID] as $param) {
|
Prefs::COMBINED_DISPLAY_MODE, Prefs::DEBUG_HEADLINE_IDS, Prefs::CDM_ENABLE_GRID] as $param) {
|
||||||
|
|
||||||
$params[strtolower($param)] = (int) get_pref($param);
|
$params[strtolower($param)] = (int) Prefs::get($param, $_SESSION['uid'], $profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
$params["safe_mode"] = !empty($_SESSION["safe_mode"]);
|
$params["safe_mode"] = !empty($_SESSION["safe_mode"]);
|
||||||
$params["check_for_updates"] = Config::get(Config::CHECK_FOR_UPDATES);
|
$params["check_for_updates"] = Config::get(Config::CHECK_FOR_UPDATES);
|
||||||
$params["icons_url"] = Config::get_self_url() . '/public.php';
|
$params["icons_url"] = Config::get_self_url() . '/public.php';
|
||||||
$params["cookie_lifetime"] = Config::get(Config::SESSION_COOKIE_LIFETIME);
|
$params["cookie_lifetime"] = Config::get(Config::SESSION_COOKIE_LIFETIME);
|
||||||
$params["default_view_mode"] = get_pref(Prefs::_DEFAULT_VIEW_MODE);
|
$params["default_view_mode"] = Prefs::get(Prefs::_DEFAULT_VIEW_MODE, $_SESSION['uid'], $profile);
|
||||||
$params["default_view_limit"] = (int) get_pref(Prefs::_DEFAULT_VIEW_LIMIT);
|
$params["default_view_limit"] = (int) Prefs::get(Prefs::_DEFAULT_VIEW_LIMIT, $_SESSION['uid'], $profile);
|
||||||
$params["default_view_order_by"] = get_pref(Prefs::_DEFAULT_VIEW_ORDER_BY);
|
$params["default_view_order_by"] = Prefs::get(Prefs::_DEFAULT_VIEW_ORDER_BY, $_SESSION['uid'], $profile);
|
||||||
$params["bw_limit"] = (int) ($_SESSION["bw_limit"] ?? false);
|
$params["bw_limit"] = (int) ($_SESSION["bw_limit"] ?? false);
|
||||||
$params["is_default_pw"] = UserHelper::is_default_password();
|
$params["is_default_pw"] = UserHelper::is_default_password();
|
||||||
$params["label_base_index"] = LABEL_BASE_INDEX;
|
$params["label_base_index"] = LABEL_BASE_INDEX;
|
||||||
|
|
||||||
$theme = get_pref(Prefs::USER_CSS_THEME);
|
$theme = Prefs::get(Prefs::USER_CSS_THEME, $_SESSION['uid'], $profile);
|
||||||
$params["theme"] = theme_exists($theme) ? $theme : "";
|
$params["theme"] = theme_exists($theme) ? $theme : "";
|
||||||
|
|
||||||
$params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
|
$params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
|
||||||
@@ -501,16 +397,13 @@ class RPC extends Handler_Protected {
|
|||||||
$params["max_feed_id"] = (int) $max_feed_id;
|
$params["max_feed_id"] = (int) $max_feed_id;
|
||||||
$params["num_feeds"] = (int) $num_feeds;
|
$params["num_feeds"] = (int) $num_feeds;
|
||||||
$params["hotkeys"] = $this->get_hotkeys_map();
|
$params["hotkeys"] = $this->get_hotkeys_map();
|
||||||
$params["widescreen"] = (int) get_pref(Prefs::WIDESCREEN_MODE);
|
$params["widescreen"] = (int) Prefs::get(Prefs::WIDESCREEN_MODE, $_SESSION['uid'], $profile);
|
||||||
$params['simple_update'] = Config::get(Config::SIMPLE_UPDATE_MODE);
|
|
||||||
$params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif");
|
$params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif");
|
||||||
$params["icon_oval"] = $this->image_to_base64("images/oval.svg");
|
$params["icon_oval"] = $this->image_to_base64("images/oval.svg");
|
||||||
$params["icon_three_dots"] = $this->image_to_base64("images/three-dots.svg");
|
$params["icon_three_dots"] = $this->image_to_base64("images/three-dots.svg");
|
||||||
$params["icon_blank"] = $this->image_to_base64("images/blank_icon.gif");
|
$params["icon_blank"] = $this->image_to_base64("images/blank_icon.gif");
|
||||||
$params["labels"] = Labels::get_all($_SESSION["uid"]);
|
$params["labels"] = Labels::get_all($_SESSION["uid"]);
|
||||||
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return $params;
|
return $params;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,8 +423,6 @@ class RPC extends Handler_Protected {
|
|||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
static function _make_runtime_info(): array {
|
static function _make_runtime_info(): array {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
|
|
||||||
$data = array();
|
$data = array();
|
||||||
|
|
||||||
$pdo = Db::pdo();
|
$pdo = Db::pdo();
|
||||||
@@ -546,21 +437,16 @@ class RPC extends Handler_Protected {
|
|||||||
|
|
||||||
$data["max_feed_id"] = (int) $max_feed_id;
|
$data["max_feed_id"] = (int) $max_feed_id;
|
||||||
$data["num_feeds"] = (int) $num_feeds;
|
$data["num_feeds"] = (int) $num_feeds;
|
||||||
$data['cdm_expanded'] = get_pref(Prefs::CDM_EXPANDED);
|
$data['cdm_expanded'] = Prefs::get(Prefs::CDM_EXPANDED, $_SESSION['uid'], $_SESSION['profile'] ?? null);
|
||||||
$data["labels"] = Labels::get_all($_SESSION["uid"]);
|
$data["labels"] = Labels::get_all($_SESSION["uid"]);
|
||||||
|
|
||||||
if (Config::get(Config::LOG_DESTINATION) == 'sql' && $_SESSION['access_level'] >= UserHelper::ACCESS_LEVEL_ADMIN) {
|
if (Config::get(Config::LOG_DESTINATION) == 'sql' && $_SESSION['access_level'] >= UserHelper::ACCESS_LEVEL_ADMIN) {
|
||||||
if (Config::get(Config::DB_TYPE) == 'pgsql') {
|
|
||||||
$log_interval = "created_at > NOW() - interval '1 hour'";
|
|
||||||
} else {
|
|
||||||
$log_interval = "created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)";
|
|
||||||
}
|
|
||||||
|
|
||||||
$sth = $pdo->prepare("SELECT COUNT(id) AS cid
|
$sth = $pdo->prepare("SELECT COUNT(id) AS cid
|
||||||
FROM ttrss_error_log
|
FROM ttrss_error_log
|
||||||
WHERE
|
WHERE
|
||||||
errno NOT IN (".E_USER_NOTICE.", ".E_USER_DEPRECATED.") AND
|
errno NOT IN (".E_USER_NOTICE.", ".E_USER_DEPRECATED.") AND
|
||||||
$log_interval AND
|
created_at > NOW() - INTERVAL '1 hour' AND
|
||||||
errstr NOT LIKE '%Returning bool from comparison function is deprecated%' AND
|
errstr NOT LIKE '%Returning bool from comparison function is deprecated%' AND
|
||||||
errstr NOT LIKE '%imagecreatefromstring(): Data is not in a recognized format%'");
|
errstr NOT LIKE '%imagecreatefromstring(): Data is not in a recognized format%'");
|
||||||
$sth->execute();
|
$sth->execute();
|
||||||
@@ -597,8 +483,6 @@ class RPC extends Handler_Protected {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,7 +682,7 @@ class RPC extends Handler_Protected {
|
|||||||
|
|
||||||
if (!empty($omap[$action])) {
|
if (!empty($omap[$action])) {
|
||||||
foreach ($omap[$action] as $sequence) {
|
foreach ($omap[$action] as $sequence) {
|
||||||
if (strpos($sequence, "|") !== false) {
|
if (str_contains($sequence, "|")) {
|
||||||
$sequence = substr($sequence,
|
$sequence = substr($sequence,
|
||||||
strpos($sequence, "|")+1,
|
strpos($sequence, "|")+1,
|
||||||
strlen($sequence));
|
strlen($sequence));
|
||||||
@@ -809,16 +693,11 @@ class RPC extends Handler_Protected {
|
|||||||
if (strlen($keys[$i]) > 1) {
|
if (strlen($keys[$i]) > 1) {
|
||||||
$tmp = '';
|
$tmp = '';
|
||||||
foreach (str_split($keys[$i]) as $c) {
|
foreach (str_split($keys[$i]) as $c) {
|
||||||
switch ($c) {
|
$tmp .= match ($c) {
|
||||||
case '*':
|
'*' => __('Shift') . '+',
|
||||||
$tmp .= __('Shift') . '+';
|
'^' => __('Ctrl') . '+',
|
||||||
break;
|
default => $c,
|
||||||
case '^':
|
};
|
||||||
$tmp .= __('Ctrl') . '+';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$tmp .= $c;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$keys[$i] = $tmp;
|
$keys[$i] = $tmp;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@ class Sanitizer {
|
|||||||
$entries = $xpath->query('//*');
|
$entries = $xpath->query('//*');
|
||||||
|
|
||||||
foreach ($entries as $entry) {
|
foreach ($entries as $entry) {
|
||||||
|
/** @var DOMElement $entry */
|
||||||
|
|
||||||
if (!in_array($entry->nodeName, $allowed_elements)) {
|
if (!in_array($entry->nodeName, $allowed_elements)) {
|
||||||
$entry->parentNode->removeChild($entry);
|
$entry->parentNode->removeChild($entry);
|
||||||
}
|
}
|
||||||
@@ -18,11 +20,11 @@ class Sanitizer {
|
|||||||
|
|
||||||
foreach ($entry->attributes as $attr) {
|
foreach ($entry->attributes as $attr) {
|
||||||
|
|
||||||
if (strpos($attr->nodeName, 'on') === 0) {
|
if (str_starts_with($attr->nodeName, 'on')) {
|
||||||
array_push($attrs_to_remove, $attr);
|
array_push($attrs_to_remove, $attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strpos($attr->nodeName, "data-") === 0) {
|
if (str_starts_with($attr->nodeName, 'data-')) {
|
||||||
array_push($attrs_to_remove, $attr);
|
array_push($attrs_to_remove, $attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,18 +59,76 @@ class Sanitizer {
|
|||||||
return parse_url(Config::get_self_url(), PHP_URL_SCHEME) == 'https';
|
return parse_url(Config::get_self_url(), PHP_URL_SCHEME) == 'https';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string> $words */
|
||||||
|
public static function highlight_words_str(string $str, array $words) : string {
|
||||||
|
$doc = new DOMDocument();
|
||||||
|
|
||||||
|
if ($doc->loadHTML('<?xml encoding="UTF-8"><span>' . $str . '</span>')) {
|
||||||
|
$xpath = new DOMXPath($doc);
|
||||||
|
|
||||||
|
if (self::highlight_words($doc, $xpath, $words)) {
|
||||||
|
$res = $doc->saveHTML();
|
||||||
|
|
||||||
|
/* strip everything outside of <body>...</body> */
|
||||||
|
$res_frag = array();
|
||||||
|
|
||||||
|
if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
|
||||||
|
return $res_frag[1];
|
||||||
|
} else {
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string> $words */
|
||||||
|
public static function highlight_words(DOMDocument &$doc, DOMXPath $xpath, array $words) : bool {
|
||||||
|
$rv = false;
|
||||||
|
|
||||||
|
foreach ($words as $word) {
|
||||||
|
|
||||||
|
// http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
|
||||||
|
$elements = $xpath->query("//*/text()");
|
||||||
|
|
||||||
|
foreach ($elements as $child) {
|
||||||
|
|
||||||
|
$fragment = $doc->createDocumentFragment();
|
||||||
|
$text = $child->textContent;
|
||||||
|
|
||||||
|
while (($pos = mb_stripos($text, $word)) !== false) {
|
||||||
|
$fragment->appendChild(new DOMText(mb_substr($text, 0, (int)$pos)));
|
||||||
|
$word = mb_substr($text, (int)$pos, mb_strlen($word));
|
||||||
|
$highlight = $doc->createElement('span');
|
||||||
|
$highlight->appendChild(new DOMText($word));
|
||||||
|
$highlight->setAttribute('class', 'highlight');
|
||||||
|
$fragment->appendChild($highlight);
|
||||||
|
$text = mb_substr($text, $pos + mb_strlen($word));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($text)) $fragment->appendChild(new DOMText($text));
|
||||||
|
|
||||||
|
$child->parentNode->replaceChild($fragment, $child);
|
||||||
|
|
||||||
|
$rv = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rv;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, string>|null $highlight_words Words to highlight in the HTML output.
|
* @param array<int, string>|null $highlight_words Words to highlight in the HTML output.
|
||||||
*
|
*
|
||||||
* @return false|string The HTML, or false if an error occurred.
|
* @return false|string The HTML, or false if an error occurred.
|
||||||
*/
|
*/
|
||||||
public static function sanitize(string $str, ?bool $force_remove_images = false, ?int $owner = null, ?string $site_url = null, ?array $highlight_words = null, ?int $article_id = null) {
|
public static function sanitize(string $str, ?bool $force_remove_images = false, ?int $owner = null, ?string $site_url = null, ?array $highlight_words = null, ?int $article_id = null): false|string {
|
||||||
$span = OpenTelemetry\API\Trace\Span::getCurrent();
|
|
||||||
$span->addEvent("Sanitizer::sanitize");
|
|
||||||
|
|
||||||
if (!$owner && isset($_SESSION["uid"]))
|
if (!$owner && isset($_SESSION["uid"]))
|
||||||
$owner = $_SESSION["uid"];
|
$owner = $_SESSION["uid"];
|
||||||
|
|
||||||
|
$profile = isset($_SESSION['uid']) && $owner == $_SESSION['uid'] && isset($_SESSION['profile']) ? $_SESSION['profile'] : null;
|
||||||
|
|
||||||
$res = trim($str); if (!$res) return '';
|
$res = trim($str); if (!$res) return '';
|
||||||
|
|
||||||
$doc = new DOMDocument();
|
$doc = new DOMDocument();
|
||||||
@@ -81,6 +141,7 @@ class Sanitizer {
|
|||||||
|
|
||||||
$entries = $xpath->query('(//a[@href]|//img[@src]|//source[@srcset|@src]|//video[@poster])');
|
$entries = $xpath->query('(//a[@href]|//img[@src]|//source[@srcset|@src]|//video[@poster])');
|
||||||
|
|
||||||
|
/** @var DOMElement $entry */
|
||||||
foreach ($entries as $entry) {
|
foreach ($entries as $entry) {
|
||||||
|
|
||||||
if ($entry->hasAttribute('href')) {
|
if ($entry->hasAttribute('href')) {
|
||||||
@@ -117,8 +178,7 @@ class Sanitizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($entry->hasAttribute('src') &&
|
if ($entry->hasAttribute('src') &&
|
||||||
($owner && get_pref(Prefs::STRIP_IMAGES, $owner)) || $force_remove_images || ($_SESSION["bw_limit"] ?? false)) {
|
($owner && Prefs::get(Prefs::STRIP_IMAGES, $owner, $profile)) || $force_remove_images || ($_SESSION['bw_limit'] ?? false)) {
|
||||||
|
|
||||||
$p = $doc->createElement('p');
|
$p = $doc->createElement('p');
|
||||||
|
|
||||||
$a = $doc->createElement('a');
|
$a = $doc->createElement('a');
|
||||||
@@ -143,6 +203,8 @@ class Sanitizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$entries = $xpath->query('//iframe');
|
$entries = $xpath->query('//iframe');
|
||||||
|
|
||||||
|
/** @var DOMElement $entry */
|
||||||
foreach ($entries as $entry) {
|
foreach ($entries as $entry) {
|
||||||
if (!self::iframe_whitelisted($entry)) {
|
if (!self::iframe_whitelisted($entry)) {
|
||||||
$entry->setAttribute('sandbox', 'allow-scripts');
|
$entry->setAttribute('sandbox', 'allow-scripts');
|
||||||
@@ -194,34 +256,8 @@ class Sanitizer {
|
|||||||
$div->appendChild($entry);
|
$div->appendChild($entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($highlight_words)) {
|
if (is_array($highlight_words))
|
||||||
foreach ($highlight_words as $word) {
|
self::highlight_words($doc, $xpath, $highlight_words);
|
||||||
|
|
||||||
// http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
|
|
||||||
|
|
||||||
$elements = $xpath->query("//*/text()");
|
|
||||||
|
|
||||||
foreach ($elements as $child) {
|
|
||||||
|
|
||||||
$fragment = $doc->createDocumentFragment();
|
|
||||||
$text = $child->textContent;
|
|
||||||
|
|
||||||
while (($pos = mb_stripos($text, $word)) !== false) {
|
|
||||||
$fragment->appendChild(new DOMText(mb_substr($text, 0, (int)$pos)));
|
|
||||||
$word = mb_substr($text, (int)$pos, mb_strlen($word));
|
|
||||||
$highlight = $doc->createElement('span');
|
|
||||||
$highlight->appendChild(new DOMText($word));
|
|
||||||
$highlight->setAttribute('class', 'highlight');
|
|
||||||
$fragment->appendChild($highlight);
|
|
||||||
$text = mb_substr($text, $pos + mb_strlen($word));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($text)) $fragment->appendChild(new DOMText($text));
|
|
||||||
|
|
||||||
$child->parentNode->replaceChild($fragment, $child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$res = $doc->saveHTML();
|
$res = $doc->saveHTML();
|
||||||
|
|
||||||
|
|||||||
171
classes/Scheduler.php
Normal file
171
classes/Scheduler.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
class Scheduler {
|
||||||
|
private static ?Scheduler $instance = null;
|
||||||
|
|
||||||
|
const TASK_RC_EXCEPTION = -100;
|
||||||
|
const DEFAULT_NAME = 'Default Scheduler';
|
||||||
|
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
private array $scheduled_tasks = [];
|
||||||
|
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
function __construct(string $name = self::DEFAULT_NAME) {
|
||||||
|
$this->set_name($name);
|
||||||
|
|
||||||
|
if ($name === self::DEFAULT_NAME) {
|
||||||
|
$this->add_scheduled_task('purge_orphaned_scheduled_tasks', '@weekly',
|
||||||
|
function() {
|
||||||
|
return $this->purge_orphaned_tasks();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getInstance(): Scheduler {
|
||||||
|
if (self::$instance == null)
|
||||||
|
self::$instance = new self();
|
||||||
|
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets specific identifier for this instance of Scheduler used in debug logging */
|
||||||
|
public function set_name(string $name) : void {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a backend scheduled task which will be executed by updater (if due) during housekeeping.
|
||||||
|
*
|
||||||
|
* The granularity is not strictly guaranteed, housekeeping is invoked several times per hour
|
||||||
|
* depending on how fast feed batch was processed, but no more than once per minute.
|
||||||
|
*
|
||||||
|
* Tasks do not run in user context. Task names may not overlap. Plugins should register tasks
|
||||||
|
* via PluginHost methods (to be implemented later).
|
||||||
|
*
|
||||||
|
* Tasks should return an integer value (return code) which is stored in the database, a value of
|
||||||
|
* 0 is considered successful.
|
||||||
|
*
|
||||||
|
* @param string $task_name unique name for this task, plugins should prefix this with plugin name
|
||||||
|
* @param string $cron_expression schedule for this task in cron format
|
||||||
|
* @param Closure $callback task code that gets executed
|
||||||
|
*/
|
||||||
|
function add_scheduled_task(string $task_name, string $cron_expression, Closure $callback) : bool {
|
||||||
|
$task_name = strtolower($task_name);
|
||||||
|
|
||||||
|
if (isset($this->scheduled_tasks[$task_name])) {
|
||||||
|
user_error("[$this->name] Attempted to override already registered scheduled task $task_name", E_USER_WARNING);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$cron = new Cron\CronExpression($cron_expression);
|
||||||
|
} catch (InvalidArgumentException $e) {
|
||||||
|
user_error("[$this->name] Attempt to register scheduled task $task_name failed: " . $e->getMessage(), E_USER_WARNING);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->scheduled_tasks[$task_name] = [
|
||||||
|
"cron" => $cron,
|
||||||
|
"callback" => $callback,
|
||||||
|
];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute scheduled tasks which are due to run and record last run timestamps.
|
||||||
|
*/
|
||||||
|
function run_due_tasks() : void {
|
||||||
|
Debug::log("[$this->name] Processing all scheduled tasks...");
|
||||||
|
|
||||||
|
$tasks_succeeded = 0;
|
||||||
|
$tasks_failed = 0;
|
||||||
|
|
||||||
|
foreach ($this->scheduled_tasks as $task_name => $task) {
|
||||||
|
$task_record = ORM::for_table('ttrss_scheduled_tasks')
|
||||||
|
->where('task_name', $task_name)
|
||||||
|
->find_one();
|
||||||
|
|
||||||
|
if ($task_record)
|
||||||
|
$last_run = $task_record->last_run;
|
||||||
|
else
|
||||||
|
$last_run = '1970-01-01 00:00';
|
||||||
|
|
||||||
|
// because we don't schedule tasks every minute, we assume that task is due if its
|
||||||
|
// next estimated run based on previous timestamp is in the past
|
||||||
|
if ($task['cron']->getNextRunDate($last_run)->getTimestamp() - time() < 0) {
|
||||||
|
Debug::log("=> Scheduled task $task_name is due, executing...");
|
||||||
|
|
||||||
|
$task_started = time();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$rc = (int) $task['callback']();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
user_error("[$this->name] Scheduled task $task_name failed with exception: " . $e->getMessage(), E_USER_WARNING);
|
||||||
|
|
||||||
|
$rc = self::TASK_RC_EXCEPTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
$task_duration = time() - $task_started;
|
||||||
|
|
||||||
|
if ($rc === 0) {
|
||||||
|
++$tasks_succeeded;
|
||||||
|
Debug::log("<= Scheduled task $task_name has finished in $task_duration seconds.");
|
||||||
|
} else {
|
||||||
|
$tasks_failed++;
|
||||||
|
Debug::log("!! Scheduled task $task_name has failed with RC: $rc after $task_duration seconds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($task_record) {
|
||||||
|
$task_record->last_run = Db::NOW();
|
||||||
|
$task_record->last_duration = $task_duration;
|
||||||
|
$task_record->last_rc = $rc;
|
||||||
|
$task_record->last_cron_expression = $task['cron']->getExpression();
|
||||||
|
|
||||||
|
$task_record->save();
|
||||||
|
} else {
|
||||||
|
$task_record = ORM::for_table('ttrss_scheduled_tasks')->create();
|
||||||
|
|
||||||
|
$task_record->set([
|
||||||
|
'task_name' => $task_name,
|
||||||
|
'last_duration' => $task_duration,
|
||||||
|
'last_rc' => $rc,
|
||||||
|
'last_run' => Db::NOW(),
|
||||||
|
'last_cron_expression' => $task['cron']->getExpression()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$task_record->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug::log("[$this->name] Processing scheduled tasks finished with $tasks_succeeded tasks succeeded and $tasks_failed tasks failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge records of scheduled tasks that aren't currently registered
|
||||||
|
* and haven't ran for a long time.
|
||||||
|
*
|
||||||
|
* @return int 0 if successful, 1 on failure
|
||||||
|
*/
|
||||||
|
private function purge_orphaned_tasks(): int {
|
||||||
|
if (!$this->scheduled_tasks) {
|
||||||
|
Debug::log(__METHOD__ . ' was invoked before scheduled tasks have been registered. This should never happen.');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = ORM::for_table('ttrss_scheduled_tasks')
|
||||||
|
->where_not_in('task_name', array_keys($this->scheduled_tasks))
|
||||||
|
->where_raw("last_run < NOW() - INTERVAL '5 weeks'")
|
||||||
|
->delete_many();
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
$deleted_count = ORM::get_last_statement()->rowCount();
|
||||||
|
|
||||||
|
if ($deleted_count)
|
||||||
|
Debug::log("Purged {$deleted_count} orphaned scheduled tasks.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ class Sessions implements \SessionHandlerInterface {
|
|||||||
private string $session_name;
|
private string $session_name;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->session_expire = min(2147483647 - time() - 1, max(Config::get(Config::SESSION_COOKIE_LIFETIME), 86400));
|
$this->session_expire = min(2147483647 - time() - 1, Config::get(Config::SESSION_COOKIE_LIFETIME));
|
||||||
$this->session_name = Config::get(Config::SESSION_NAME);
|
$this->session_name = Config::get(Config::SESSION_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,11 +29,11 @@ class Sessions implements \SessionHandlerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend the validity of the PHP session cookie (if it exists)
|
* Extend the validity of the PHP session cookie (if it exists) and is persistent (expire > 0)
|
||||||
* @return bool Whether the new cookie was set successfully
|
* @return bool Whether the new cookie was set successfully
|
||||||
*/
|
*/
|
||||||
public function extend_session(): bool {
|
public function extend_session(): bool {
|
||||||
if (isset($_COOKIE[$this->session_name])) {
|
if (isset($_COOKIE[$this->session_name]) && $this->session_expire > 0) {
|
||||||
return setcookie($this->session_name,
|
return setcookie($this->session_name,
|
||||||
$_COOKIE[$this->session_name],
|
$_COOKIE[$this->session_name],
|
||||||
time() + $this->session_expire,
|
time() + $this->session_expire,
|
||||||
@@ -53,17 +53,22 @@ class Sessions implements \SessionHandlerInterface {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function read(string $id): false|string {
|
||||||
* @todo set return type to string|false, and remove ReturnTypeWillChange, when min supported is PHP 8
|
|
||||||
* @return string|false
|
|
||||||
*/
|
|
||||||
#[\ReturnTypeWillChange]
|
|
||||||
public function read(string $id) {
|
|
||||||
$sth = Db::pdo()->prepare('SELECT data FROM ttrss_sessions WHERE id=?');
|
$sth = Db::pdo()->prepare('SELECT data FROM ttrss_sessions WHERE id=?');
|
||||||
$sth->execute([$id]);
|
$sth->execute([$id]);
|
||||||
|
|
||||||
if ($row = $sth->fetch()) {
|
if ($row = $sth->fetch()) {
|
||||||
return base64_decode($row['data']);
|
$data = base64_decode($row['data']);
|
||||||
|
|
||||||
|
if (Config::get(Config::ENCRYPTION_KEY)) {
|
||||||
|
$unserialized_data = @unserialize($data); // avoid leaking plaintext session via error message
|
||||||
|
|
||||||
|
if ($unserialized_data !== false)
|
||||||
|
return Crypt::decrypt_string($unserialized_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if encryption key is missing or session data is not in serialized format, assume plaintext data and return as-is
|
||||||
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
$expire = time() + $this->session_expire;
|
$expire = time() + $this->session_expire;
|
||||||
@@ -74,7 +79,12 @@ class Sessions implements \SessionHandlerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function write(string $id, string $data): bool {
|
public function write(string $id, string $data): bool {
|
||||||
|
|
||||||
|
if (Config::get(Config::ENCRYPTION_KEY))
|
||||||
|
$data = serialize(Crypt::encrypt_string($data));
|
||||||
|
|
||||||
$data = base64_encode($data);
|
$data = base64_encode($data);
|
||||||
|
|
||||||
$expire = time() + $this->session_expire;
|
$expire = time() + $this->session_expire;
|
||||||
|
|
||||||
$sth = Db::pdo()->prepare('SELECT id FROM ttrss_sessions WHERE id=?');
|
$sth = Db::pdo()->prepare('SELECT id FROM ttrss_sessions WHERE id=?');
|
||||||
@@ -95,11 +105,9 @@ class Sessions implements \SessionHandlerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo set return type to int|false, and remove ReturnTypeWillChange, when min supported is PHP 8
|
|
||||||
* @return int|false the number of deleted sessions on success, or false on failure
|
* @return int|false the number of deleted sessions on success, or false on failure
|
||||||
*/
|
*/
|
||||||
#[\ReturnTypeWillChange]
|
public function gc(int $max_lifetime): false|int {
|
||||||
public function gc(int $max_lifetime) {
|
|
||||||
$result = Db::pdo()->query('DELETE FROM ttrss_sessions WHERE expire < ' . time());
|
$result = Db::pdo()->query('DELETE FROM ttrss_sessions WHERE expire < ' . time());
|
||||||
return $result === false ? false : $result->rowCount();
|
return $result === false ? false : $result->rowCount();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class Templator extends MiniTemplator {
|
|||||||
|
|
||||||
/* this reads tt-rss template from templates.local/ or templates/ if only base filename is given */
|
/* this reads tt-rss template from templates.local/ or templates/ if only base filename is given */
|
||||||
function readTemplateFromFile ($fileName) {
|
function readTemplateFromFile ($fileName) {
|
||||||
if (strpos($fileName, "/") === false) {
|
if (!str_contains($fileName, "/")) {
|
||||||
|
|
||||||
$fileName = basename($fileName);
|
$fileName = basename($fileName);
|
||||||
|
|
||||||
|
|||||||
@@ -2,29 +2,39 @@
|
|||||||
class TimeHelper {
|
class TimeHelper {
|
||||||
|
|
||||||
static function smart_date_time(int $timestamp, int $tz_offset = 0, ?int $owner_uid = null, bool $eta_min = false): string {
|
static function smart_date_time(int $timestamp, int $tz_offset = 0, ?int $owner_uid = null, bool $eta_min = false): string {
|
||||||
|
// i.e. if the Unix epoch
|
||||||
|
if ($timestamp - $tz_offset === 0)
|
||||||
|
return __('Never');
|
||||||
|
|
||||||
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
|
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
|
||||||
|
$profile = isset($_SESSION['uid']) && $owner_uid == $_SESSION['uid'] && isset($_SESSION['profile']) ? $_SESSION['profile'] : null;
|
||||||
|
|
||||||
if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
|
if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
|
||||||
return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
|
return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
|
||||||
} else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
|
} else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
|
||||||
$format = get_pref(Prefs::SHORT_DATE_FORMAT, $owner_uid);
|
$format = Prefs::get(Prefs::SHORT_DATE_FORMAT, $owner_uid, $profile);
|
||||||
if (strpos((strtolower($format)), "a") === false)
|
if (!str_contains((strtolower($format)), "a"))
|
||||||
return date("G:i", $timestamp);
|
return date("G:i", $timestamp);
|
||||||
else
|
else
|
||||||
return date("g:i a", $timestamp);
|
return date("g:i a", $timestamp);
|
||||||
} else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
|
} else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
|
||||||
$format = get_pref(Prefs::SHORT_DATE_FORMAT, $owner_uid);
|
$format = Prefs::get(Prefs::SHORT_DATE_FORMAT, $owner_uid, $profile);
|
||||||
return date($format, $timestamp);
|
return date($format, $timestamp);
|
||||||
} else {
|
} else {
|
||||||
$format = get_pref(Prefs::LONG_DATE_FORMAT, $owner_uid);
|
$format = Prefs::get(Prefs::LONG_DATE_FORMAT, $owner_uid, $profile);
|
||||||
return date($format, $timestamp);
|
return date($format, $timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static function make_local_datetime(?string $timestamp, bool $long, ?int $owner_uid = null,
|
/**
|
||||||
|
* @param bool $long Whether to display the datetime in a 'long' format. Only used if $no_smart_dt is true.
|
||||||
|
*/
|
||||||
|
static function make_local_datetime(?string $timestamp, bool $long = false, ?int $owner_uid = null,
|
||||||
bool $no_smart_dt = false, bool $eta_min = false): string {
|
bool $no_smart_dt = false, bool $eta_min = false): string {
|
||||||
|
|
||||||
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
|
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
|
||||||
|
$profile = isset($_SESSION['uid']) && $owner_uid == $_SESSION['uid'] && isset($_SESSION['profile']) ? $_SESSION['profile'] : null;
|
||||||
|
|
||||||
if (!$timestamp) $timestamp = '1970-01-01 0:00';
|
if (!$timestamp) $timestamp = '1970-01-01 0:00';
|
||||||
|
|
||||||
global $utc_tz;
|
global $utc_tz;
|
||||||
@@ -37,7 +47,7 @@ class TimeHelper {
|
|||||||
# We store date in UTC internally
|
# We store date in UTC internally
|
||||||
$dt = new DateTime($timestamp, $utc_tz);
|
$dt = new DateTime($timestamp, $utc_tz);
|
||||||
|
|
||||||
$user_tz_string = get_pref(Prefs::USER_TIMEZONE, $owner_uid);
|
$user_tz_string = Prefs::get(Prefs::USER_TIMEZONE, $owner_uid);
|
||||||
|
|
||||||
if ($user_tz_string != 'Automatic') {
|
if ($user_tz_string != 'Automatic') {
|
||||||
|
|
||||||
@@ -59,9 +69,9 @@ class TimeHelper {
|
|||||||
$tz_offset, $owner_uid, $eta_min);
|
$tz_offset, $owner_uid, $eta_min);
|
||||||
} else {
|
} else {
|
||||||
if ($long)
|
if ($long)
|
||||||
$format = get_pref(Prefs::LONG_DATE_FORMAT, $owner_uid);
|
$format = Prefs::get(Prefs::LONG_DATE_FORMAT, $owner_uid, $profile);
|
||||||
else
|
else
|
||||||
$format = get_pref(Prefs::SHORT_DATE_FORMAT, $owner_uid);
|
$format = Prefs::get(Prefs::SHORT_DATE_FORMAT, $owner_uid, $profile);
|
||||||
|
|
||||||
return date($format, $user_timestamp);
|
return date($format, $user_timestamp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
|
|
||||||
use OpenTelemetry\API\Trace\SpanContextInterface;
|
|
||||||
use OpenTelemetry\API\Trace\SpanInterface;
|
|
||||||
use OpenTelemetry\API\Trace\SpanKind;
|
|
||||||
use OpenTelemetry\API\Trace\TraceFlags;
|
|
||||||
use OpenTelemetry\API\Trace\TraceStateInterface;
|
|
||||||
use OpenTelemetry\Context\ContextInterface;
|
|
||||||
use OpenTelemetry\Context\ContextKey;
|
|
||||||
use OpenTelemetry\Context\ContextKeyInterface;
|
|
||||||
use OpenTelemetry\Context\ImplicitContextKeyedInterface;
|
|
||||||
use OpenTelemetry\Context\ScopeInterface;
|
|
||||||
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
|
|
||||||
use OpenTelemetry\Contrib\Otlp\SpanExporter;
|
|
||||||
use OpenTelemetry\SDK\Common\Attribute\Attributes;
|
|
||||||
use OpenTelemetry\SDK\Resource\ResourceInfo;
|
|
||||||
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
|
|
||||||
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
|
|
||||||
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
|
|
||||||
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
|
|
||||||
use OpenTelemetry\SDK\Trace\TracerProvider;
|
|
||||||
use OpenTelemetry\SemConv\ResourceAttributes;
|
|
||||||
|
|
||||||
class DummyContextInterface implements ContextInterface {
|
|
||||||
|
|
||||||
/** @var DummyContextInterface */
|
|
||||||
private static $instance;
|
|
||||||
|
|
||||||
public function __construct() {
|
|
||||||
self::$instance = $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @phpstan-ignore-next-line */
|
|
||||||
public static function createKey(string $key): ContextKeyInterface { return new ContextKey(); }
|
|
||||||
|
|
||||||
public static function getCurrent(): ContextInterface { return self::$instance; }
|
|
||||||
|
|
||||||
public function activate(): ScopeInterface { return new DummyScopeInterface(); }
|
|
||||||
|
|
||||||
public function with(ContextKeyInterface $key, $value): ContextInterface { return $this; }
|
|
||||||
|
|
||||||
public function withContextValue(ImplicitContextKeyedInterface $value): ContextInterface { return $this; }
|
|
||||||
|
|
||||||
public function get(ContextKeyInterface $key) { return new ContextKey(); }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class DummySpanContextInterface implements SpanContextInterface {
|
|
||||||
|
|
||||||
/** @var DummySpanContextInterface $instance */
|
|
||||||
private static $instance;
|
|
||||||
|
|
||||||
public function __construct() {
|
|
||||||
self::$instance = $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function createFromRemoteParent(string $traceId, string $spanId, int $traceFlags = TraceFlags::DEFAULT, ?TraceStateInterface $traceState = null): SpanContextInterface { return self::$instance; }
|
|
||||||
|
|
||||||
public static function getInvalid(): SpanContextInterface { return self::$instance; }
|
|
||||||
|
|
||||||
public static function create(string $traceId, string $spanId, int $traceFlags = TraceFlags::DEFAULT, ?TraceStateInterface $traceState = null): SpanContextInterface { return self::$instance; }
|
|
||||||
|
|
||||||
public function getTraceId(): string { return ""; }
|
|
||||||
|
|
||||||
public function getTraceIdBinary(): string { return ""; }
|
|
||||||
|
|
||||||
public function getSpanId(): string { return ""; }
|
|
||||||
|
|
||||||
public function getSpanIdBinary(): string { return ""; }
|
|
||||||
|
|
||||||
public function getTraceFlags(): int { return 0; }
|
|
||||||
|
|
||||||
public function getTraceState(): ?TraceStateInterface { return null; }
|
|
||||||
|
|
||||||
public function isValid(): bool { return false; }
|
|
||||||
|
|
||||||
public function isRemote(): bool { return false; }
|
|
||||||
|
|
||||||
public function isSampled(): bool { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
class DummyScopeInterface implements ScopeInterface {
|
|
||||||
public function detach(): int { return 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
class DummySpanInterface implements SpanInterface {
|
|
||||||
|
|
||||||
/** @var DummySpanInterface $instance */
|
|
||||||
private static $instance;
|
|
||||||
|
|
||||||
public function __construct() {
|
|
||||||
self::$instance = $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromContext(ContextInterface $context): SpanInterface { return self::$instance; }
|
|
||||||
|
|
||||||
public static function getCurrent(): SpanInterface { return self::$instance; }
|
|
||||||
|
|
||||||
public static function getInvalid(): SpanInterface { return self::$instance; }
|
|
||||||
|
|
||||||
public static function wrap(SpanContextInterface $spanContext): SpanInterface { return self::$instance; }
|
|
||||||
|
|
||||||
public function getContext(): SpanContextInterface { return new DummySpanContextInterface(); }
|
|
||||||
|
|
||||||
public function isRecording(): bool { return false; }
|
|
||||||
|
|
||||||
/** @phpstan-ignore-next-line */
|
|
||||||
public function setAttribute(string $key, $value): SpanInterface { return self::$instance; }
|
|
||||||
|
|
||||||
/** @phpstan-ignore-next-line */
|
|
||||||
public function setAttributes(iterable $attributes): SpanInterface { return self::$instance; }
|
|
||||||
|
|
||||||
/** @phpstan-ignore-next-line */
|
|
||||||
public function addEvent(string $name, iterable $attributes = [], ?int $timestamp = null): SpanInterface { return $this; }
|
|
||||||
|
|
||||||
/** @phpstan-ignore-next-line */
|
|
||||||
public function recordException(Throwable $exception, iterable $attributes = []): SpanInterface { return $this; }
|
|
||||||
|
|
||||||
public function updateName(string $name): SpanInterface { return $this; }
|
|
||||||
|
|
||||||
public function setStatus(string $code, ?string $description = null): SpanInterface { return $this; }
|
|
||||||
|
|
||||||
public function end(?int $endEpochNanos = null): void { }
|
|
||||||
|
|
||||||
public function activate(): ScopeInterface { return new DummyScopeInterface(); }
|
|
||||||
|
|
||||||
public function storeInContext(ContextInterface $context): ContextInterface { return new DummyContextInterface(); }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Tracer {
|
|
||||||
/** @var Tracer $instance */
|
|
||||||
private static $instance = null;
|
|
||||||
|
|
||||||
/** @var OpenTelemetry\SDK\Trace\TracerProviderInterface $tracerProvider */
|
|
||||||
private $tracerProvider = null;
|
|
||||||
|
|
||||||
/** @var OpenTelemetry\API\Trace\TracerInterface $tracer */
|
|
||||||
private $tracer = null;
|
|
||||||
|
|
||||||
public function __construct() {
|
|
||||||
$OPENTELEMETRY_ENDPOINT = Config::get(Config::OPENTELEMETRY_ENDPOINT);
|
|
||||||
|
|
||||||
if ($OPENTELEMETRY_ENDPOINT) {
|
|
||||||
$transport = (new OtlpHttpTransportFactory())->create($OPENTELEMETRY_ENDPOINT, 'application/x-protobuf');
|
|
||||||
$exporter = new SpanExporter($transport);
|
|
||||||
|
|
||||||
$resource = ResourceInfoFactory::emptyResource()->merge(
|
|
||||||
ResourceInfo::create(Attributes::create(
|
|
||||||
[ResourceAttributes::SERVICE_NAME => Config::get(Config::OPENTELEMETRY_SERVICE)]
|
|
||||||
), ResourceAttributes::SCHEMA_URL),
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->tracerProvider = TracerProvider::builder()
|
|
||||||
->addSpanProcessor(new SimpleSpanProcessor($exporter))
|
|
||||||
->setResource($resource)
|
|
||||||
->setSampler(new ParentBased(new AlwaysOnSampler()))
|
|
||||||
->build();
|
|
||||||
|
|
||||||
$this->tracer = $this->tracerProvider->getTracer('io.opentelemetry.contrib.php');
|
|
||||||
|
|
||||||
$context = TraceContextPropagator::getInstance()->extract(getallheaders());
|
|
||||||
|
|
||||||
$span = $this->tracer->spanBuilder($_SESSION['name'] ?? 'not logged in')
|
|
||||||
->setParent($context)
|
|
||||||
->setSpanKind(SpanKind::KIND_SERVER)
|
|
||||||
->setAttribute('php.request', json_encode($_REQUEST))
|
|
||||||
->setAttribute('php.server', json_encode($_SERVER))
|
|
||||||
->setAttribute('php.session', json_encode($_SESSION ?? []))
|
|
||||||
->startSpan();
|
|
||||||
|
|
||||||
$scope = $span->activate();
|
|
||||||
|
|
||||||
register_shutdown_function(function() use ($span, $scope) {
|
|
||||||
$span->end();
|
|
||||||
$scope->detach();
|
|
||||||
$this->tracerProvider->shutdown();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @return OpenTelemetry\API\Trace\SpanInterface
|
|
||||||
*/
|
|
||||||
private function _start(string $name) {
|
|
||||||
if ($this->tracer != null) {
|
|
||||||
$span = $this->tracer
|
|
||||||
->spanBuilder($name)
|
|
||||||
->setSpanKind(SpanKind::KIND_SERVER)
|
|
||||||
->startSpan();
|
|
||||||
|
|
||||||
$span->activate();
|
|
||||||
} else {
|
|
||||||
$span = new DummySpanInterface();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $span;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @return OpenTelemetry\API\Trace\SpanInterface
|
|
||||||
*/
|
|
||||||
public static function start(string $name) {
|
|
||||||
return self::get_instance()->_start($name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function get_instance() : Tracer {
|
|
||||||
if (self::$instance == null)
|
|
||||||
self::$instance = new self();
|
|
||||||
|
|
||||||
return self::$instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -65,7 +65,7 @@ class UrlHelper {
|
|||||||
$rel_url,
|
$rel_url,
|
||||||
string $owner_element = "",
|
string $owner_element = "",
|
||||||
string $owner_attribute = "",
|
string $owner_attribute = "",
|
||||||
string $content_type = "") {
|
string $content_type = ""): false|string {
|
||||||
|
|
||||||
$rel_parts = parse_url($rel_url);
|
$rel_parts = parse_url($rel_url);
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ class UrlHelper {
|
|||||||
return self::validate($rel_url);
|
return self::validate($rel_url);
|
||||||
|
|
||||||
// protocol-relative URL (rare but they exist)
|
// protocol-relative URL (rare but they exist)
|
||||||
} else if (strpos($rel_url, "//") === 0) {
|
} else if (str_starts_with($rel_url, "//")) {
|
||||||
return self::validate("https:" . $rel_url);
|
return self::validate("https:" . $rel_url);
|
||||||
// allow some extra schemes for A href
|
// allow some extra schemes for A href
|
||||||
} else if (in_array($rel_parts["scheme"] ?? "", self::EXTRA_HREF_SCHEMES, true) &&
|
} else if (in_array($rel_parts["scheme"] ?? "", self::EXTRA_HREF_SCHEMES, true) &&
|
||||||
@@ -118,10 +118,10 @@ class UrlHelper {
|
|||||||
// 1. absolute relative path (/test.html) = no-op, proceed as is
|
// 1. absolute relative path (/test.html) = no-op, proceed as is
|
||||||
|
|
||||||
// 2. dotslash relative URI (./test.html) - strip "./", append base path
|
// 2. dotslash relative URI (./test.html) - strip "./", append base path
|
||||||
if (strpos($rel_parts['path'], './') === 0) {
|
if (str_starts_with($rel_parts['path'], './')) {
|
||||||
$rel_parts['path'] = $base_path . substr($rel_parts['path'], 2);
|
$rel_parts['path'] = $base_path . substr($rel_parts['path'], 2);
|
||||||
// 3. anything else relative (test.html) - append dirname() of base path
|
// 3. anything else relative (test.html) - append dirname() of base path
|
||||||
} else if (strpos($rel_parts['path'], '/') !== 0) {
|
} else if (!str_starts_with($rel_parts['path'], '/')) {
|
||||||
$rel_parts['path'] = $base_path . $rel_parts['path'];
|
$rel_parts['path'] = $base_path . $rel_parts['path'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,12 +136,12 @@ class UrlHelper {
|
|||||||
/** extended filtering involves validation for safe ports and loopback
|
/** extended filtering involves validation for safe ports and loopback
|
||||||
* @return false|string false if something went wrong, otherwise the URL string
|
* @return false|string false if something went wrong, otherwise the URL string
|
||||||
*/
|
*/
|
||||||
static function validate(string $url, bool $extended_filtering = false) {
|
static function validate(string $url, bool $extended_filtering = false): false|string {
|
||||||
|
|
||||||
$url = clean($url);
|
$url = clean($url);
|
||||||
|
|
||||||
# fix protocol-relative URLs
|
# fix protocol-relative URLs
|
||||||
if (strpos($url, "//") === 0)
|
if (str_starts_with($url, "//"))
|
||||||
$url = "https:" . $url;
|
$url = "https:" . $url;
|
||||||
|
|
||||||
$tokens = parse_url($url);
|
$tokens = parse_url($url);
|
||||||
@@ -191,19 +191,15 @@ class UrlHelper {
|
|||||||
if (!in_array($tokens['port'] ?? '', [80, 443, '']))
|
if (!in_array($tokens['port'] ?? '', [80, 443, '']))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (strtolower($tokens['host']) == 'localhost' || $tokens['host'] == '::1' || strpos($tokens['host'], '127.') === 0)
|
if (strtolower($tokens['host']) == 'localhost' || $tokens['host'] == '::1'
|
||||||
|
|| str_starts_with($tokens['host'], '127.'))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $url;
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static function resolve_redirects(string $url, int $timeout): false|string {
|
||||||
* @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();
|
$client = self::get_client();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -218,14 +214,11 @@ class UrlHelper {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
} catch (Exception $ex) {
|
} catch (Exception $ex) {
|
||||||
$span->setAttribute('error', (string) $ex);
|
|
||||||
$span->end();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a history header value doesn't exist there was no redirection and the original URL is fine.
|
// 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);
|
$history_header = $response->getHeader(GuzzleHttp\RedirectMiddleware::HISTORY_HEADER);
|
||||||
$span->end();
|
|
||||||
return ($history_header ? end($history_header) : $url);
|
return ($history_header ? end($history_header) : $url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,11 +228,9 @@ class UrlHelper {
|
|||||||
*/
|
*/
|
||||||
// TODO: max_size currently only works for CURL transfers
|
// TODO: max_size currently only works for CURL transfers
|
||||||
// TODO: multiple-argument way is deprecated, first parameter is a hash now
|
// 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,
|
public static function fetch(array|string $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,
|
4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false, 8: $encoding = false,
|
||||||
9: $auth_type = "basic" */) {
|
9: $auth_type = "basic" */): false|string {
|
||||||
$span = Tracer::start(__METHOD__);
|
|
||||||
$span->setAttribute('func.args', json_encode(func_get_args()));
|
|
||||||
|
|
||||||
self::$fetch_last_error = "";
|
self::$fetch_last_error = "";
|
||||||
self::$fetch_last_error_code = -1;
|
self::$fetch_last_error_code = -1;
|
||||||
@@ -299,19 +290,18 @@ class UrlHelper {
|
|||||||
|
|
||||||
if (!$url) {
|
if (!$url) {
|
||||||
self::$fetch_last_error = 'Requested URL failed extended validation.';
|
self::$fetch_last_error = 'Requested URL failed extended validation.';
|
||||||
$span->setAttribute('error', self::$fetch_last_error);
|
|
||||||
$span->end();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$url_host = parse_url($url, PHP_URL_HOST);
|
// this skip is needed for integration tests, please don't enable in production
|
||||||
$ip_addr = gethostbyname($url_host);
|
if (!getenv('__URLHELPER_ALLOW_LOOPBACK')) {
|
||||||
|
$url_host = parse_url($url, PHP_URL_HOST);
|
||||||
|
$ip_addr = gethostbyname($url_host);
|
||||||
|
|
||||||
if (!$ip_addr || strpos($ip_addr, '127.') === 0) {
|
if (!$ip_addr || str_starts_with($ip_addr, '127.')) {
|
||||||
self::$fetch_last_error = "URL hostname failed to resolve or resolved to a loopback address ($ip_addr)";
|
self::$fetch_last_error = "URL hostname failed to resolve or resolved to a loopback address ($ip_addr)";
|
||||||
$span->setAttribute('error', self::$fetch_last_error);
|
return false;
|
||||||
$span->end();
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$req_options = [
|
$req_options = [
|
||||||
@@ -392,8 +382,6 @@ class UrlHelper {
|
|||||||
} catch (\LengthException $ex) {
|
} catch (\LengthException $ex) {
|
||||||
// Either 'Content-Length' indicated the download limit would be exceeded, or the transfer actually exceeded the download limit.
|
// Either 'Content-Length' indicated the download limit would be exceeded, or the transfer actually exceeded the download limit.
|
||||||
self::$fetch_last_error = $ex->getMessage();
|
self::$fetch_last_error = $ex->getMessage();
|
||||||
$span->setAttribute('error', self::$fetch_last_error);
|
|
||||||
$span->end();
|
|
||||||
return false;
|
return false;
|
||||||
} catch (GuzzleHttp\Exception\GuzzleException $ex) {
|
} catch (GuzzleHttp\Exception\GuzzleException $ex) {
|
||||||
self::$fetch_last_error = $ex->getMessage();
|
self::$fetch_last_error = $ex->getMessage();
|
||||||
@@ -407,13 +395,12 @@ class UrlHelper {
|
|||||||
// to attempt compatibility with unusual configurations.
|
// to attempt compatibility with unusual configurations.
|
||||||
if ($login && $pass && self::$fetch_last_error_code === 403 && $auth_type !== 'any') {
|
if ($login && $pass && self::$fetch_last_error_code === 403 && $auth_type !== 'any') {
|
||||||
$options['auth_type'] = 'any';
|
$options['auth_type'] = 'any';
|
||||||
$span->end();
|
|
||||||
return self::fetch($options);
|
return self::fetch($options);
|
||||||
}
|
}
|
||||||
|
|
||||||
self::$fetch_last_content_type = $ex->getResponse()->getHeaderLine('content-type');
|
self::$fetch_last_content_type = $ex->getResponse()->getHeaderLine('content-type');
|
||||||
|
|
||||||
if ($type && strpos(self::$fetch_last_content_type, "$type") === false)
|
if ($type && !str_contains(self::$fetch_last_content_type, "$type"))
|
||||||
self::$fetch_last_error_content = (string) $ex->getResponse()->getBody();
|
self::$fetch_last_error_content = (string) $ex->getResponse()->getBody();
|
||||||
} elseif (array_key_exists('errno', $ex->getHandlerContext())) {
|
} elseif (array_key_exists('errno', $ex->getHandlerContext())) {
|
||||||
$errno = (int) $ex->getHandlerContext()['errno'];
|
$errno = (int) $ex->getHandlerContext()['errno'];
|
||||||
@@ -424,15 +411,11 @@ class UrlHelper {
|
|||||||
if (($errno === \CURLE_WRITE_ERROR || $errno === \CURLE_BAD_CONTENT_ENCODING) &&
|
if (($errno === \CURLE_WRITE_ERROR || $errno === \CURLE_BAD_CONTENT_ENCODING) &&
|
||||||
$ex->getRequest()->getHeaderLine('accept-encoding') !== 'none') {
|
$ex->getRequest()->getHeaderLine('accept-encoding') !== 'none') {
|
||||||
$options['encoding'] = 'none';
|
$options['encoding'] = 'none';
|
||||||
$span->end();
|
|
||||||
return self::fetch($options);
|
return self::fetch($options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->setAttribute('error', self::$fetch_last_error);
|
|
||||||
$span->end();
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,44 +432,41 @@ class UrlHelper {
|
|||||||
// This shouldn't be necessary given the checks that occur during potential redirects, but we'll do it anyway.
|
// 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)) {
|
if (!self::validate(self::$fetch_effective_url, true)) {
|
||||||
self::$fetch_last_error = "URL received after redirection failed extended validation.";
|
self::$fetch_last_error = "URL received after redirection failed extended validation.";
|
||||||
$span->setAttribute('error', self::$fetch_last_error);
|
|
||||||
$span->end();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @phpstan-ignore argument.type (prior validation ensures the host value exists)
|
||||||
self::$fetch_effective_ip_addr = gethostbyname(parse_url(self::$fetch_effective_url, PHP_URL_HOST));
|
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) {
|
// this skip is needed for integration tests, please don't enable in production
|
||||||
self::$fetch_last_error = 'URL hostname received after redirection failed to resolve or resolved to a loopback address (' .
|
if (!getenv('__URLHELPER_ALLOW_LOOPBACK')) {
|
||||||
self::$fetch_effective_ip_addr . ')';
|
if (!self::$fetch_effective_ip_addr || str_starts_with(self::$fetch_effective_ip_addr, '127.')) {
|
||||||
$span->setAttribute('error', self::$fetch_last_error);
|
self::$fetch_last_error = 'URL hostname received after redirection failed to resolve or resolved to a loopback address (' .
|
||||||
$span->end();
|
self::$fetch_effective_ip_addr . ')';
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$body = (string) $response->getBody();
|
$body = (string) $response->getBody();
|
||||||
|
|
||||||
if (!$body) {
|
if (!$body) {
|
||||||
self::$fetch_last_error = 'Successful response, but no content was received.';
|
self::$fetch_last_error = 'Successful response, but no content was received.';
|
||||||
$span->setAttribute('error', self::$fetch_last_error);
|
|
||||||
$span->end();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$span->end();
|
|
||||||
return $body;
|
return $body;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return false|string false if the provided URL didn't match expected patterns, otherwise the video ID string
|
* @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) {
|
public static function url_to_youtube_vid(string $url): false|string {
|
||||||
$url = str_replace("youtube.com", "youtube-nocookie.com", $url);
|
$url = str_replace("youtube.com", "youtube-nocookie.com", $url);
|
||||||
|
|
||||||
$regexps = [
|
$regexps = [
|
||||||
"/\/\/www\.youtube-nocookie\.com\/v\/([\w-]+)/",
|
"/\/\/www\.youtube-nocookie\.com\/v\/([\w-]+)/",
|
||||||
"/\/\/www\.youtube-nocookie\.com\/embed\/([\w-]+)/",
|
"/\/\/www\.youtube-nocookie\.com\/embed\/([\w-]+)/",
|
||||||
"/\/\/www\.youtube-nocookie\.com\/watch?v=([\w-]+)/",
|
"/\/\/www\.youtube-nocookie\.com\/watch\?v=([\w-]+)/",
|
||||||
"/\/\/youtu.be\/([\w-]+)/",
|
"/\/\/youtu.be\/([\w-]+)/",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class UserHelper {
|
|||||||
*/
|
*/
|
||||||
public static function map_access_level(int $level) : int {
|
public static function map_access_level(int $level) : int {
|
||||||
if (in_array($level, self::ACCESS_LEVELS)) {
|
if (in_array($level, self::ACCESS_LEVELS)) {
|
||||||
/** @phpstan-ignore-next-line */
|
/** @phpstan-ignore return.type (yes it is a UserHelper::ACCESS_LEVEL_* value) */
|
||||||
return $level;
|
return $level;
|
||||||
} else {
|
} else {
|
||||||
user_error("Passed invalid user access level: $level", E_USER_WARNING);
|
user_error("Passed invalid user access level: $level", E_USER_WARNING);
|
||||||
@@ -125,10 +125,6 @@ class UserHelper {
|
|||||||
|
|
||||||
if (empty($_SESSION["csrf_token"]))
|
if (empty($_SESSION["csrf_token"]))
|
||||||
$_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
|
$_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
|
||||||
|
|
||||||
if (Config::get_schema_version() >= 120) {
|
|
||||||
$_SESSION["language"] = get_pref(Prefs::USER_LANGUAGE, $owner_uid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static function load_user_plugins(int $owner_uid, ?PluginHost $pluginhost = null): void {
|
static function load_user_plugins(int $owner_uid, ?PluginHost $pluginhost = null): void {
|
||||||
@@ -136,7 +132,8 @@ class UserHelper {
|
|||||||
if (!$pluginhost) $pluginhost = PluginHost::getInstance();
|
if (!$pluginhost) $pluginhost = PluginHost::getInstance();
|
||||||
|
|
||||||
if ($owner_uid && Config::get_schema_version() >= 100 && empty($_SESSION["safe_mode"])) {
|
if ($owner_uid && Config::get_schema_version() >= 100 && empty($_SESSION["safe_mode"])) {
|
||||||
$plugins = get_pref(Prefs::_ENABLED_PLUGINS, $owner_uid);
|
$profile = isset($_SESSION['uid']) && $owner_uid == $_SESSION['uid'] && isset($_SESSION['profile']) ? $_SESSION['profile'] : null;
|
||||||
|
$plugins = Prefs::get(Prefs::_ENABLED_PLUGINS, $owner_uid, $profile);
|
||||||
|
|
||||||
$pluginhost->load((string)$plugins, PluginHost::KIND_USER, $owner_uid);
|
$pluginhost->load((string)$plugins, PluginHost::KIND_USER, $owner_uid);
|
||||||
|
|
||||||
@@ -184,15 +181,13 @@ class UserHelper {
|
|||||||
$_SESSION["last_login_update"] = time();
|
$_SESSION["last_login_update"] = time();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_SESSION["uid"]) {
|
startup_gettext();
|
||||||
startup_gettext();
|
self::load_user_plugins($_SESSION["uid"]);
|
||||||
self::load_user_plugins($_SESSION["uid"]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static function print_user_stylesheet(): void {
|
static function print_user_stylesheet(): void {
|
||||||
$value = get_pref(Prefs::USER_STYLESHEET);
|
$value = Prefs::get(Prefs::USER_STYLESHEET, $_SESSION['uid'], $_SESSION['profile'] ?? null);
|
||||||
|
|
||||||
if ($value) {
|
if ($value) {
|
||||||
print "<style type='text/css' id='user_css_style'>";
|
print "<style type='text/css' id='user_css_style'>";
|
||||||
@@ -367,7 +362,6 @@ class UserHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param null|int $owner_uid if null, checks current user via session-specific auth module, if set works on internal database only
|
* @param null|int $owner_uid if null, checks current user via session-specific auth module, if set works on internal database only
|
||||||
* @return bool
|
|
||||||
* @throws PDOException
|
* @throws PDOException
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@@ -380,31 +374,19 @@ class UserHelper {
|
|||||||
*
|
*
|
||||||
* @return false|string False if the password couldn't be hashed, otherwise the hash string.
|
* @return false|string False if the password couldn't be hashed, otherwise the hash string.
|
||||||
*/
|
*/
|
||||||
static function hash_password(string $pass, string $salt, string $algo = self::HASH_ALGOS[0]) {
|
static function hash_password(string $pass, string $salt, string $algo = self::HASH_ALGOS[0]): false|string {
|
||||||
$pass_hash = "";
|
$pass_hash = match ($algo) {
|
||||||
|
self::HASH_ALGO_SHA1 => sha1($pass),
|
||||||
|
self::HASH_ALGO_SHA1X => sha1("$salt:$pass"),
|
||||||
|
self::HASH_ALGO_MODE2, self::HASH_ALGO_SSHA256 => hash('sha256', $salt . $pass),
|
||||||
|
self::HASH_ALGO_SSHA512 => hash('sha512', $salt . $pass),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
switch ($algo) {
|
if ($pass_hash === null)
|
||||||
case self::HASH_ALGO_SHA1:
|
user_error("hash_password: unknown hash algo: $algo", E_USER_ERROR);
|
||||||
$pass_hash = sha1($pass);
|
|
||||||
break;
|
|
||||||
case self::HASH_ALGO_SHA1X:
|
|
||||||
$pass_hash = sha1("$salt:$pass");
|
|
||||||
break;
|
|
||||||
case self::HASH_ALGO_MODE2:
|
|
||||||
case self::HASH_ALGO_SSHA256:
|
|
||||||
$pass_hash = hash('sha256', $salt . $pass);
|
|
||||||
break;
|
|
||||||
case self::HASH_ALGO_SSHA512:
|
|
||||||
$pass_hash = hash('sha512', $salt . $pass);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
user_error("hash_password: unknown hash algo: $algo", E_USER_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pass_hash)
|
return $pass_hash ? "$algo:$pass_hash" : false;
|
||||||
return "$algo:$pass_hash";
|
|
||||||
else
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -495,7 +477,6 @@ class UserHelper {
|
|||||||
/**
|
/**
|
||||||
* @param null|int $owner_uid if null, checks current user via session-specific auth module, if set works on internal database only
|
* @param null|int $owner_uid if null, checks current user via session-specific auth module, if set works on internal database only
|
||||||
* @param string $password password to compare hash against
|
* @param string $password password to compare hash against
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
static function user_has_password(?int $owner_uid, string $password) : bool {
|
static function user_has_password(?int $owner_uid, string $password) : bool {
|
||||||
if ($owner_uid) {
|
if ($owner_uid) {
|
||||||
@@ -503,7 +484,6 @@ class UserHelper {
|
|||||||
|
|
||||||
return $authenticator->check_password($owner_uid, $password);
|
return $authenticator->check_password($owner_uid, $password);
|
||||||
} else {
|
} else {
|
||||||
/** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
|
|
||||||
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
|
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
|
||||||
|
|
||||||
if ($authenticator &&
|
if ($authenticator &&
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
{
|
{
|
||||||
"name": "j4mie/idiorm",
|
"name": "j4mie/idiorm",
|
||||||
"type": "vcs",
|
"type": "vcs",
|
||||||
"url": "https://dev.tt-rss.org/fox/idiorm.git"
|
"url": "https://github.com/tt-rss/tt-rss-idiorm.git"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -21,17 +21,16 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"spomky-labs/otphp": "^10.0",
|
"spomky-labs/otphp": "^11.3",
|
||||||
"chillerlan/php-qrcode": "^4.3.3",
|
"chillerlan/php-qrcode": "^5.0.3",
|
||||||
"mervick/material-design-icons": "^2.2",
|
"mervick/material-design-icons": "^2.2",
|
||||||
"j4mie/idiorm": "dev-master",
|
"j4mie/idiorm": "dev-master",
|
||||||
"open-telemetry/exporter-otlp": "^1.0",
|
|
||||||
"php-http/guzzle7-adapter": "^1.0",
|
|
||||||
"soundasleep/html2text": "^2.1",
|
"soundasleep/html2text": "^2.1",
|
||||||
"guzzlehttp/guzzle": "^7.0"
|
"guzzlehttp/guzzle": "^7.0",
|
||||||
|
"dragonmantank/cron-expression": "^3.4"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpstan/phpstan": "1.10.3",
|
"phpstan/phpstan": "2.1.30",
|
||||||
"phpunit/phpunit": "9.5.16",
|
"phpunit/phpunit": "9.5.16",
|
||||||
"phpunit/php-code-coverage": "^9.2"
|
"phpunit/php-code-coverage": "^9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
1622
composer.lock
generated
1622
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,6 @@
|
|||||||
|
|
||||||
etc.
|
etc.
|
||||||
|
|
||||||
See this page for more information: https://tt-rss.org/wiki/GlobalConfig
|
See this page for more information: https://github.com/tt-rss/tt-rss/wiki/Global-Config
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ function _color_hue2rgb(float $m1, float $m2, float $h): int {
|
|||||||
* @return array{0: int, 1: int, 2: int}
|
* @return array{0: int, 1: int, 2: int}
|
||||||
*/
|
*/
|
||||||
function _color_unpack(string $hex, bool $normalize = false): array {
|
function _color_unpack(string $hex, bool $normalize = false): array {
|
||||||
$hex = strpos($hex, '#') !== 0 ? _resolve_htmlcolor($hex) : substr($hex, 1);
|
$hex = str_starts_with($hex, '#') ? substr($hex, 1) : _resolve_htmlcolor($hex);
|
||||||
|
|
||||||
if (strlen($hex) == 4) {
|
if (strlen($hex) == 4) {
|
||||||
$hex = $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3];
|
$hex = $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3];
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
*/
|
*/
|
||||||
function input_tag(string $name, string $value, string $type = "text", array $attributes = [], string $id = ""): string {
|
function input_tag(string $name, string $value, string $type = "text", array $attributes = [], string $id = ""): string {
|
||||||
$attributes_str = attributes_to_string($attributes);
|
$attributes_str = attributes_to_string($attributes);
|
||||||
$dojo_type = strpos($attributes_str, "dojoType") === false ? "dojoType='dijit.form.TextBox'" : "";
|
$dojo_type = str_contains($attributes_str, "dojoType") ? "" : "dojoType='dijit.form.TextBox'";
|
||||||
|
|
||||||
return "<input name=\"".htmlspecialchars($name)."\" $dojo_type ".attributes_to_string($attributes)." id=\"".htmlspecialchars($id)."\"
|
return "<input name=\"".htmlspecialchars($name)."\" $dojo_type ".attributes_to_string($attributes)." id=\"".htmlspecialchars($id)."\"
|
||||||
type=\"$type\" value=\"".htmlspecialchars($value)."\">";
|
type=\"$type\" value=\"".htmlspecialchars($value)."\">";
|
||||||
@@ -56,21 +56,21 @@
|
|||||||
* @param array<string, mixed> $attributes
|
* @param array<string, mixed> $attributes
|
||||||
*/
|
*/
|
||||||
function number_spinner_tag(string $name, string $value, array $attributes = [], string $id = ""): string {
|
function number_spinner_tag(string $name, string $value, array $attributes = [], string $id = ""): string {
|
||||||
return input_tag($name, $value, "text", array_merge(["dojoType" => "dijit.form.NumberSpinner"], $attributes), $id);
|
return input_tag($name, $value, 'text', ['dojoType' => 'dijit.form.NumberSpinner', ...$attributes], $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $attributes
|
* @param array<string, mixed> $attributes
|
||||||
*/
|
*/
|
||||||
function submit_tag(string $value, array $attributes = []): string {
|
function submit_tag(string $value, array $attributes = []): string {
|
||||||
return button_tag($value, "submit", array_merge(["class" => "alt-primary"], $attributes));
|
return button_tag($value, 'submit', ['class' => 'alt-primary', ...$attributes]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $attributes
|
* @param array<string, mixed> $attributes
|
||||||
*/
|
*/
|
||||||
function cancel_dialog_tag(string $value, array $attributes = []): string {
|
function cancel_dialog_tag(string $value, array $attributes = []): string {
|
||||||
return button_tag($value, "", array_merge(["onclick" => "App.dialogOf(this).hide()"], $attributes));
|
return button_tag($value, '', ['onclick' => 'App.dialogOf(this).hide()', ...$attributes]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,13 +81,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $value
|
|
||||||
* @param array<int|string, string> $values
|
* @param array<int|string, string> $values
|
||||||
* @param array<string, mixed> $attributes
|
* @param array<string, mixed> $attributes
|
||||||
*/
|
*/
|
||||||
function select_tag(string $name, $value, array $values, array $attributes = [], string $id = ""): string {
|
function select_tag(string $name, mixed $value, array $values, array $attributes = [], string $id = ""): string {
|
||||||
$attributes_str = attributes_to_string($attributes);
|
$attributes_str = attributes_to_string($attributes);
|
||||||
$dojo_type = strpos($attributes_str, "dojoType") === false ? "dojoType='fox.form.Select'" : "";
|
$dojo_type = str_contains($attributes_str, "dojoType") ? "" : "dojoType='fox.form.Select'";
|
||||||
|
|
||||||
$rv = "<select $dojo_type name=\"".htmlspecialchars($name)."\"
|
$rv = "<select $dojo_type name=\"".htmlspecialchars($name)."\"
|
||||||
id=\"".htmlspecialchars($id)."\" name=\"".htmlspecialchars($name)."\" $attributes_str>";
|
id=\"".htmlspecialchars($id)."\" name=\"".htmlspecialchars($name)."\" $attributes_str>";
|
||||||
@@ -116,7 +115,7 @@
|
|||||||
*/
|
*/
|
||||||
function select_hash(string $name, $value, array $values, array $attributes = [], string $id = ""): string {
|
function select_hash(string $name, $value, array $values, array $attributes = [], string $id = ""): string {
|
||||||
$attributes_str = attributes_to_string($attributes);
|
$attributes_str = attributes_to_string($attributes);
|
||||||
$dojo_type = strpos($attributes_str, "dojoType") === false ? "dojoType='fox.form.Select'" : "";
|
$dojo_type = str_contains($attributes_str, "dojoType") ? "" : "dojoType='fox.form.Select'";
|
||||||
|
|
||||||
$rv = "<select $dojo_type name=\"".htmlspecialchars($name)."\"
|
$rv = "<select $dojo_type name=\"".htmlspecialchars($name)."\"
|
||||||
id=\"".htmlspecialchars($id)."\" name=\"".htmlspecialchars($name)."\" $attributes_str>";
|
id=\"".htmlspecialchars($id)."\" name=\"".htmlspecialchars($name)."\" $attributes_str>";
|
||||||
|
|||||||
@@ -5,15 +5,13 @@
|
|||||||
*/
|
*/
|
||||||
function stylesheet_tag(string $filename, array $attributes = []): string {
|
function stylesheet_tag(string $filename, array $attributes = []): string {
|
||||||
|
|
||||||
$attributes_str = \Controls\attributes_to_string(
|
$attributes_str = \Controls\attributes_to_string([
|
||||||
array_merge(
|
'href' => "$filename?" . filemtime($filename),
|
||||||
[
|
'rel' => 'stylesheet',
|
||||||
"href" => "$filename?" . filemtime($filename),
|
'type' => 'text/css',
|
||||||
"rel" => "stylesheet",
|
'data-orig-href' => $filename,
|
||||||
"type" => "text/css",
|
...$attributes,
|
||||||
"data-orig-href" => $filename
|
]);
|
||||||
],
|
|
||||||
$attributes));
|
|
||||||
|
|
||||||
return "<link $attributes_str/>\n";
|
return "<link $attributes_str/>\n";
|
||||||
}
|
}
|
||||||
@@ -22,14 +20,12 @@ function stylesheet_tag(string $filename, array $attributes = []): string {
|
|||||||
* @param array<string, mixed> $attributes
|
* @param array<string, mixed> $attributes
|
||||||
*/
|
*/
|
||||||
function javascript_tag(string $filename, array $attributes = []): string {
|
function javascript_tag(string $filename, array $attributes = []): string {
|
||||||
$attributes_str = \Controls\attributes_to_string(
|
$attributes_str = \Controls\attributes_to_string([
|
||||||
array_merge(
|
'src' => "$filename?" . filemtime($filename),
|
||||||
[
|
'type' => 'text/javascript',
|
||||||
"src" => "$filename?" . filemtime($filename),
|
'charset' => 'utf-8',
|
||||||
"type" => "text/javascript",
|
...$attributes,
|
||||||
"charset" => "utf-8"
|
]);
|
||||||
],
|
|
||||||
$attributes));
|
|
||||||
|
|
||||||
return "<script $attributes_str></script>\n";
|
return "<script $attributes_str></script>\n";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,40 +2,38 @@
|
|||||||
/**
|
/**
|
||||||
* @param array<int, array<string, mixed>> $trace
|
* @param array<int, array<string, mixed>> $trace
|
||||||
*/
|
*/
|
||||||
function format_backtrace($trace): string {
|
function format_backtrace(array $trace): string {
|
||||||
$rv = "";
|
$rv = "";
|
||||||
$idx = 1;
|
$idx = 1;
|
||||||
|
|
||||||
if (is_array($trace)) {
|
foreach ($trace as $e) {
|
||||||
foreach ($trace as $e) {
|
if (isset($e["file"]) && isset($e["line"])) {
|
||||||
if (isset($e["file"]) && isset($e["line"])) {
|
$fmt_args = [];
|
||||||
$fmt_args = [];
|
|
||||||
|
|
||||||
if (is_array($e["args"] ?? false)) {
|
if (is_array($e["args"] ?? false)) {
|
||||||
foreach ($e["args"] as $a) {
|
foreach ($e["args"] as $a) {
|
||||||
if (is_object($a)) {
|
if (is_object($a)) {
|
||||||
array_push($fmt_args, "{" . get_class($a) . "}");
|
array_push($fmt_args, "{" . get_class($a) . "}");
|
||||||
} else if (is_array($a)) {
|
} else if (is_array($a)) {
|
||||||
array_push($fmt_args, "[" . truncate_string(json_encode($a), 256, "...")) . "]";
|
array_push($fmt_args, "[" . truncate_string(json_encode($a), 256, "...")) . "]";
|
||||||
} else if (is_resource($a)) {
|
} else if (is_resource($a)) {
|
||||||
array_push($fmt_args, truncate_string(get_resource_type($a), 256, "..."));
|
array_push($fmt_args, truncate_string(get_resource_type($a), 256, "..."));
|
||||||
} else if (is_string($a)) {
|
} else if (is_string($a)) {
|
||||||
array_push($fmt_args, truncate_string($a, 256, "..."));
|
array_push($fmt_args, truncate_string($a, 256, "..."));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$filename = str_replace(dirname(__DIR__) . "/", "", $e["file"]);
|
|
||||||
|
|
||||||
$rv .= sprintf("%d. %s(%s): %s(%s)\n",
|
|
||||||
$idx,
|
|
||||||
$filename,
|
|
||||||
$e["line"],
|
|
||||||
$e["function"],
|
|
||||||
implode(", ", $fmt_args));
|
|
||||||
|
|
||||||
$idx++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$filename = str_replace(dirname(__DIR__) . "/", "", $e["file"]);
|
||||||
|
|
||||||
|
$rv .= sprintf("%d. %s(%s): %s(%s)\n",
|
||||||
|
$idx,
|
||||||
|
$filename,
|
||||||
|
$e["line"],
|
||||||
|
$e["function"],
|
||||||
|
implode(", ", $fmt_args));
|
||||||
|
|
||||||
|
$idx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,18 +41,19 @@ function format_backtrace($trace): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ttrss_error_handler(int $errno, string $errstr, string $file, int $line): bool {
|
function ttrss_error_handler(int $errno, string $errstr, string $file, int $line): bool {
|
||||||
// return true in order to avoid default error handling by PHP
|
// return true in order to avoid default error handling by PHP
|
||||||
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
|
if (!(error_reporting() & $errno)) return true;
|
||||||
if (error_reporting() == 0 || !$errno) return true;
|
|
||||||
} else {
|
|
||||||
if (!(error_reporting() & $errno)) return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$file = substr(str_replace(dirname(__DIR__), "", $file), 1);
|
$file = substr(str_replace(dirname(__DIR__), "", $file), 1);
|
||||||
|
|
||||||
$context = format_backtrace(debug_backtrace());
|
$context = format_backtrace(debug_backtrace());
|
||||||
$errstr = truncate_middle($errstr, 16384, " (...) ");
|
$errstr = truncate_middle($errstr, 16384, " (...) ");
|
||||||
|
|
||||||
|
if (php_sapi_name() == 'cli' && class_exists("Debug")) {
|
||||||
|
Debug::log("!! Exception: $errstr ($file:$line)");
|
||||||
|
Debug::log($context);
|
||||||
|
}
|
||||||
|
|
||||||
if (class_exists("Logger"))
|
if (class_exists("Logger"))
|
||||||
return Logger::log_error((int)$errno, $errstr, $file, (int)$line, $context);
|
return Logger::log_error((int)$errno, $errstr, $file, (int)$line, $context);
|
||||||
else
|
else
|
||||||
@@ -76,8 +75,16 @@ function ttrss_fatal_handler(): bool {
|
|||||||
|
|
||||||
$file = substr(str_replace(dirname(__DIR__), "", $file), 1);
|
$file = substr(str_replace(dirname(__DIR__), "", $file), 1);
|
||||||
|
|
||||||
|
if (php_sapi_name() == 'cli' && class_exists("Debug")) {
|
||||||
|
Debug::log("!! Fatal error: $errstr ($file:$line)");
|
||||||
|
Debug::log($context);
|
||||||
|
}
|
||||||
|
|
||||||
if (class_exists("Logger"))
|
if (class_exists("Logger"))
|
||||||
return Logger::log_error((int)$errno, $errstr, $file, (int)$line, $context);
|
Logger::log_error((int)$errno, $errstr, $file, (int)$line, $context);
|
||||||
|
|
||||||
|
if (php_sapi_name() == 'cli')
|
||||||
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -5,21 +5,14 @@
|
|||||||
/** @deprecated by Config::SCHEMA_VERSION */
|
/** @deprecated by Config::SCHEMA_VERSION */
|
||||||
define('SCHEMA_VERSION', Config::SCHEMA_VERSION);
|
define('SCHEMA_VERSION', Config::SCHEMA_VERSION);
|
||||||
|
|
||||||
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
|
|
||||||
libxml_disable_entity_loader(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
libxml_use_internal_errors(true);
|
libxml_use_internal_errors(true);
|
||||||
|
|
||||||
// separate test because this is included before sanity checks
|
// separate test because this is included before sanity checks
|
||||||
if (function_exists("mb_internal_encoding")) mb_internal_encoding("UTF-8");
|
if (function_exists("mb_internal_encoding")) mb_internal_encoding("UTF-8");
|
||||||
|
|
||||||
date_default_timezone_set('UTC');
|
date_default_timezone_set('UTC');
|
||||||
if (defined('E_DEPRECATED')) {
|
|
||||||
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
|
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
|
||||||
} else {
|
|
||||||
error_reporting(E_ALL & ~E_NOTICE);
|
|
||||||
}
|
|
||||||
|
|
||||||
ini_set('display_errors', "false");
|
ini_set('display_errors', "false");
|
||||||
ini_set('display_startup_errors', "false");
|
ini_set('display_startup_errors', "false");
|
||||||
@@ -30,27 +23,20 @@
|
|||||||
|
|
||||||
require_once "autoload.php";
|
require_once "autoload.php";
|
||||||
|
|
||||||
if (Config::get(Config::DB_TYPE) == "pgsql") {
|
/** @deprecated use the 'SUBSTRING_FOR_DATE' string directly */
|
||||||
define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
|
const SUBSTRING_FOR_DATE = 'SUBSTRING_FOR_DATE';
|
||||||
} else {
|
|
||||||
define('SUBSTRING_FOR_DATE', 'SUBSTRING');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool|int|null|string
|
|
||||||
*
|
|
||||||
* @deprecated by Prefs::get()
|
* @deprecated by Prefs::get()
|
||||||
*/
|
*/
|
||||||
function get_pref(string $pref_name, ?int $owner_uid = null) {
|
function get_pref(string $pref_name, ?int $owner_uid = null): bool|int|null|string {
|
||||||
return Prefs::get($pref_name, $owner_uid ? $owner_uid : $_SESSION["uid"], $_SESSION["profile"] ?? null);
|
return Prefs::get($pref_name, $owner_uid ? $owner_uid : $_SESSION["uid"], $_SESSION["profile"] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param bool|int|string $value
|
|
||||||
*
|
|
||||||
* @deprecated by Prefs::set()
|
* @deprecated by Prefs::set()
|
||||||
*/
|
*/
|
||||||
function set_pref(string $pref_name, $value, ?int $owner_uid = null, bool $strip_tags = true): bool {
|
function set_pref(string $pref_name, bool|int|string $value, ?int $owner_uid = null, bool $strip_tags = true): bool {
|
||||||
return Prefs::set($pref_name, $value, $owner_uid ? $owner_uid : $_SESSION["uid"], $_SESSION["profile"] ?? null, $strip_tags);
|
return Prefs::set($pref_name, $value, $owner_uid ? $owner_uid : $_SESSION["uid"], $_SESSION["profile"] ?? null, $strip_tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +74,7 @@
|
|||||||
"uk_UA" => "Українська",
|
"uk_UA" => "Українська",
|
||||||
"sv_SE" => "Svenska",
|
"sv_SE" => "Svenska",
|
||||||
"fi_FI" => "Suomi",
|
"fi_FI" => "Suomi",
|
||||||
|
"ta" => "Tamil",
|
||||||
"tr_TR" => "Türkçe");
|
"tr_TR" => "Türkçe");
|
||||||
|
|
||||||
return $t;
|
return $t;
|
||||||
@@ -123,31 +110,29 @@
|
|||||||
// create a list like "en" => 0.8
|
// create a list like "en" => 0.8
|
||||||
$langs = array_combine($lang_parse[1], $lang_parse[4]);
|
$langs = array_combine($lang_parse[1], $lang_parse[4]);
|
||||||
|
|
||||||
if (is_array($langs)) {
|
// set default to 1 for any without q factor
|
||||||
// set default to 1 for any without q factor
|
foreach ($langs as $lang => $val) {
|
||||||
foreach ($langs as $lang => $val) {
|
if ($val === '') $langs[$lang] = 1;
|
||||||
if ($val === '') $langs[$lang] = 1;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// sort list based on value
|
// sort list based on value
|
||||||
arsort($langs, SORT_NUMERIC);
|
arsort($langs, SORT_NUMERIC);
|
||||||
|
|
||||||
foreach (array_keys($langs) as $lang) {
|
foreach (array_keys($langs) as $lang) {
|
||||||
$lang = strtolower($lang);
|
$lang = strtolower($lang);
|
||||||
|
|
||||||
foreach ($valid_langs as $vlang => $vlocale) {
|
foreach ($valid_langs as $vlang => $vlocale) {
|
||||||
if ($vlang == $lang) {
|
if ($vlang == $lang) {
|
||||||
$selected_locale = $vlocale;
|
$selected_locale = $vlocale;
|
||||||
break 2;
|
break 2;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($_SESSION["uid"]) && get_schema_version() >= 120) {
|
if (!empty($_SESSION["uid"]) && Config::get_schema_version() >= 120) {
|
||||||
$pref_locale = get_pref(Prefs::USER_LANGUAGE, $_SESSION["uid"]);
|
$pref_locale = Prefs::get(Prefs::USER_LANGUAGE, $_SESSION["uid"], $_SESSION["profile"] ?? null);
|
||||||
|
|
||||||
if (!empty($pref_locale) && $pref_locale != 'auto') {
|
if (!empty($pref_locale) && $pref_locale != 'auto') {
|
||||||
$selected_locale = $pref_locale;
|
$selected_locale = $pref_locale;
|
||||||
@@ -179,7 +164,7 @@
|
|||||||
*
|
*
|
||||||
* @return array<string, mixed>|string
|
* @return array<string, mixed>|string
|
||||||
*/
|
*/
|
||||||
function get_version() {
|
function get_version(): array|string {
|
||||||
return Config::get_version();
|
return Config::get_version();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +185,7 @@
|
|||||||
* @return int
|
* @return int
|
||||||
* @throws PDOException
|
* @throws PDOException
|
||||||
*/
|
*/
|
||||||
function getFeedUnread($feed, bool $is_cat = false): int {
|
function getFeedUnread(int|string $feed, bool $is_cat = false): int {
|
||||||
return Feeds::_get_counters($feed, $is_cat, true, $_SESSION["uid"]);
|
return Feeds::_get_counters($feed, $is_cat, true, $_SESSION["uid"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +196,7 @@
|
|||||||
*
|
*
|
||||||
* @return false|string The HTML, or false if an error occurred.
|
* @return false|string The HTML, or false if an error occurred.
|
||||||
*/
|
*/
|
||||||
function sanitize(string $str, bool $force_remove_images = false, ?int $owner = null, ?string $site_url = null, ?array $highlight_words = null, ?int $article_id = null) {
|
function sanitize(string $str, bool $force_remove_images = false, ?int $owner = null, ?string $site_url = null, ?array $highlight_words = null, ?int $article_id = null): false|string {
|
||||||
return Sanitizer::sanitize($str, $force_remove_images, $owner, $site_url, $highlight_words, $article_id);
|
return Sanitizer::sanitize($str, $force_remove_images, $owner, $site_url, $highlight_words, $article_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,9 +204,9 @@
|
|||||||
* @deprecated by UrlHelper::fetch()
|
* @deprecated by UrlHelper::fetch()
|
||||||
*
|
*
|
||||||
* @param array<string, bool|int|string>|string $params
|
* @param array<string, bool|int|string>|string $params
|
||||||
* @return bool|string false if something went wrong, otherwise string contents
|
* @return false|string false if something went wrong, otherwise string contents
|
||||||
*/
|
*/
|
||||||
function fetch_file_contents($params) {
|
function fetch_file_contents(array|string $params): false|string {
|
||||||
return UrlHelper::fetch($params);
|
return UrlHelper::fetch($params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,9 +228,9 @@
|
|||||||
/**
|
/**
|
||||||
* @deprecated by UrlHelper::validate()
|
* @deprecated by UrlHelper::validate()
|
||||||
*
|
*
|
||||||
* @return bool|string false if something went wrong, otherwise the URL string
|
* @return false|string false if something went wrong, otherwise the URL string
|
||||||
*/
|
*/
|
||||||
function validate_url(string $url) {
|
function validate_url(string $url): false|string {
|
||||||
return UrlHelper::validate($url);
|
return UrlHelper::validate($url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +245,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated by TimeHelper::make_local_datetime() */
|
/** @deprecated by TimeHelper::make_local_datetime() */
|
||||||
function make_local_datetime(string $timestamp, bool $long, ?int $owner_uid = null, bool $no_smart_dt = false, bool $eta_min = false): string {
|
function make_local_datetime(string $timestamp, bool $long = false, ?int $owner_uid = null, bool $no_smart_dt = false, bool $eta_min = false): string {
|
||||||
return TimeHelper::make_local_datetime($timestamp, $long, $owner_uid, $no_smart_dt, $eta_min);
|
return TimeHelper::make_local_datetime($timestamp, $long, $owner_uid, $no_smart_dt, $eta_min);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,12 +259,8 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This is used for user http parameters unless HTML code is actually needed.
|
* This is used for user http parameters unless HTML code is actually needed.
|
||||||
*
|
|
||||||
* @param mixed $param
|
|
||||||
*
|
|
||||||
* @return mixed|null
|
|
||||||
*/
|
*/
|
||||||
function clean($param) {
|
function clean(mixed $param): mixed {
|
||||||
if (is_array($param)) {
|
if (is_array($param)) {
|
||||||
return array_map("trim", array_map("strip_tags", $param));
|
return array_map("trim", array_map("strip_tags", $param));
|
||||||
} else if (is_string($param)) {
|
} else if (is_string($param)) {
|
||||||
@@ -290,11 +271,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function with_trailing_slash(string $str) : string {
|
function with_trailing_slash(string $str) : string {
|
||||||
if (substr($str, -1) === "/") {
|
return str_ends_with($str, '/') ? $str : "$str/";
|
||||||
return $str;
|
|
||||||
} else {
|
|
||||||
return "$str/";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function make_password(int $length = 12): string {
|
function make_password(int $length = 12): string {
|
||||||
@@ -352,9 +329,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Convert values accepted by tt-rss as true/false to PHP booleans
|
/** Convert values accepted by tt-rss as true/false to PHP booleans
|
||||||
* @see https://tt-rss.org/ApiReference/#boolean-values
|
* @see https://github.com/tt-rss/tt-rss/wiki/API-Reference#boolean-values
|
||||||
* @param null|string $s null values are considered false
|
* @param null|string $s null values are considered false
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
function sql_bool_to_bool(?string $s): bool {
|
function sql_bool_to_bool(?string $s): bool {
|
||||||
return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
|
return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
|
||||||
@@ -446,7 +422,7 @@
|
|||||||
/**
|
/**
|
||||||
* @return false|string The decoded string or false if an error occurred.
|
* @return false|string The decoded string or false if an error occurred.
|
||||||
*/
|
*/
|
||||||
function gzdecode(string $string) { // no support for 2nd argument
|
function gzdecode(string $string): false|string { // no support for 2nd argument
|
||||||
return file_get_contents('compress.zlib://data:who/cares;base64,'.
|
return file_get_contents('compress.zlib://data:who/cares;base64,'.
|
||||||
base64_encode($string));
|
base64_encode($string));
|
||||||
}
|
}
|
||||||
@@ -479,10 +455,7 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function implements_interface(object|string $class, string $interface): bool {
|
||||||
* @param object|string $class
|
|
||||||
*/
|
|
||||||
function implements_interface($class, string $interface): bool {
|
|
||||||
$class_implemented_interfaces = class_implements($class);
|
$class_implemented_interfaces = class_implements($class);
|
||||||
|
|
||||||
if ($class_implemented_interfaces) {
|
if ($class_implemented_interfaces) {
|
||||||
@@ -491,14 +464,14 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_theme_path(string $theme): string {
|
function get_theme_path(string $theme, string $default = ""): string {
|
||||||
$check = "themes/$theme";
|
$check = "themes/$theme";
|
||||||
if (file_exists($check)) return $check;
|
if (file_exists($check)) return $check;
|
||||||
|
|
||||||
$check = "themes.local/$theme";
|
$check = "themes.local/$theme";
|
||||||
if (file_exists($check)) return $check;
|
if (file_exists($check)) return $check;
|
||||||
|
|
||||||
return "";
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
function theme_exists(string $theme): bool {
|
function theme_exists(string $theme): bool {
|
||||||
@@ -523,4 +496,3 @@
|
|||||||
|
|
||||||
return $ts;
|
return $ts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,22 +33,25 @@
|
|||||||
require({cache:{}});
|
require({cache:{}});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
const __default_light_theme = "<?= get_theme_path(Config::get(Config::DEFAULT_LIGHT_THEME), 'themes/light.css') ?>";
|
||||||
|
const __default_dark_theme = "<?= get_theme_path(Config::get(Config::DEFAULT_DARK_THEME), 'themes/night.css') ?>";
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
/* exported Plugins */
|
/* exported Plugins */
|
||||||
const Plugins = {};
|
const Plugins = {};
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
foreach (PluginHost::getInstance()->get_plugins() as $n => $p) {
|
foreach (PluginHost::getInstance()->get_plugins() as $n => $p) {
|
||||||
if (method_exists($p, "get_login_js")) {
|
$script = $p->get_login_js();
|
||||||
$script = $p->get_login_js();
|
|
||||||
|
|
||||||
if ($script) {
|
if ($script) {
|
||||||
echo "try {
|
echo "try {
|
||||||
$script
|
$script
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('failed to initialize plugin JS: $n', e);
|
console.warn('failed to initialize plugin JS: $n', e);
|
||||||
}";
|
}";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -123,79 +126,84 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<label><?= __("Login:") ?></label>
|
<label><?= __("Login:") ?></label>
|
||||||
<input name="login" id="login" dojoType="dijit.form.TextBox" type="text"
|
<input name="login" id="login" dojoType="dijit.form.TextBox" type="text"
|
||||||
onchange="UtilityApp.fetchProfiles()"
|
onchange="UtilityApp.fetchProfiles()"
|
||||||
onfocus="UtilityApp.fetchProfiles()"
|
onfocus="UtilityApp.fetchProfiles()"
|
||||||
onblur="UtilityApp.fetchProfiles()"
|
onblur="UtilityApp.fetchProfiles()"
|
||||||
required="1" value="<?= $_SESSION["fake_login"] ?? "" ?>" />
|
<?= Config::get(Config::DISABLE_LOGIN_FORM) ? 'disabled="disabled"' : '' ?>
|
||||||
|
required="1" value="<?= $_SESSION["fake_login"] ?? "" ?>" />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label><?= __("Password:") ?></label>
|
<label><?= __("Password:") ?></label>
|
||||||
|
|
||||||
<input type="password" name="password" required="1"
|
<input type="password" name="password" required="1"
|
||||||
dojoType="dijit.form.TextBox"
|
dojoType="dijit.form.TextBox"
|
||||||
class="input input-text"
|
class="input input-text"
|
||||||
onchange="UtilityApp.fetchProfiles()"
|
onchange="UtilityApp.fetchProfiles()"
|
||||||
onfocus="UtilityApp.fetchProfiles()"
|
onfocus="UtilityApp.fetchProfiles()"
|
||||||
onblur="UtilityApp.fetchProfiles()"
|
onblur="UtilityApp.fetchProfiles()"
|
||||||
value="<?= $_SESSION["fake_password"] ?? "" ?>"/>
|
<?= Config::get(Config::DISABLE_LOGIN_FORM) ? 'disabled="disabled"' : '' ?>
|
||||||
|
value="<?= $_SESSION["fake_password"] ?? "" ?>"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<?php if (strpos(Config::get(Config::PLUGINS), "auth_internal") !== false) { ?>
|
<?php if (!Config::get(Config::DISABLE_LOGIN_FORM) && str_contains(Config::get(Config::PLUGINS), "auth_internal")) { ?>
|
||||||
<fieldset class="align-right">
|
<fieldset class="align-right">
|
||||||
<a href="public.php?op=forgotpass"><?= __("I forgot my password") ?></a>
|
<a href="public.php?op=forgotpass"><?= __("I forgot my password") ?></a>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<fieldset>
|
<?php if (!Config::get(Config::DISABLE_LOGIN_FORM)) { ?>
|
||||||
<label><?= __("Profile:") ?></label>
|
<fieldset>
|
||||||
|
<label><?= __("Profile:") ?></label>
|
||||||
|
|
||||||
<select disabled='disabled' name="profile" id="profile" dojoType='dijit.form.Select'>
|
<select disabled='disabled' name="profile" id="profile" dojoType='dijit.form.Select'>
|
||||||
<option><?= __("Default profile") ?></option>
|
<option><?= __("Default profile") ?></option>
|
||||||
</select>
|
</select>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="narrow">
|
|
||||||
<label> </label>
|
|
||||||
|
|
||||||
<label id="bw_limit_label">
|
|
||||||
<?= \Controls\checkbox_tag("bw_limit", false, "",
|
|
||||||
["onchange" => 'UtilityApp.bwLimitChange(this)'], 'bw_limit') ?>
|
|
||||||
<?= __("Use less traffic") ?></label>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div dojoType="dijit.Tooltip" connectId="bw_limit_label" position="below" style="display:none">
|
|
||||||
<?= __("Does not display images in articles, reduces automatic refreshes."); ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset class="narrow">
|
|
||||||
<label> </label>
|
|
||||||
|
|
||||||
<label id="safe_mode_label">
|
|
||||||
<?= \Controls\checkbox_tag("safe_mode") ?>
|
|
||||||
<?= __("Safe mode") ?>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div dojoType="dijit.Tooltip" connectId="safe_mode_label" position="below" style="display:none">
|
|
||||||
<?= __("Uses default theme and prevents all plugins from loading."); ?>
|
|
||||||
</div>
|
|
||||||
<?php if (Config::get(Config::SESSION_COOKIE_LIFETIME) > 0) { ?>
|
|
||||||
|
|
||||||
<fieldset class="narrow">
|
<fieldset class="narrow">
|
||||||
<label> </label>
|
<label> </label>
|
||||||
<label>
|
|
||||||
<?= \Controls\checkbox_tag("remember_me") ?>
|
<label id="bw_limit_label">
|
||||||
<?= __("Remember me") ?>
|
<?= \Controls\checkbox_tag("bw_limit", false, "",
|
||||||
|
["onchange" => 'UtilityApp.bwLimitChange(this)'], 'bw_limit') ?>
|
||||||
|
<?= __("Use less traffic") ?></label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div dojoType="dijit.Tooltip" connectId="bw_limit_label" position="below" style="display:none">
|
||||||
|
<?= __("Does not display images in articles, reduces automatic refreshes."); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="narrow">
|
||||||
|
<label> </label>
|
||||||
|
|
||||||
|
<label id="safe_mode_label">
|
||||||
|
<?= \Controls\checkbox_tag("safe_mode") ?>
|
||||||
|
<?= __("Safe mode") ?>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<div dojoType="dijit.Tooltip" connectId="safe_mode_label" position="below" style="display:none">
|
||||||
|
<?= __("Uses default theme and prevents all plugins from loading."); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (Config::get(Config::SESSION_COOKIE_LIFETIME) > 0) { ?>
|
||||||
|
<fieldset class="narrow">
|
||||||
|
<label> </label>
|
||||||
|
<label>
|
||||||
|
<?= \Controls\checkbox_tag("remember_me") ?>
|
||||||
|
<?= __("Remember me") ?>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<?php } ?>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
<fieldset class="align-right">
|
<fieldset class="align-right">
|
||||||
<label> </label>
|
<label> </label>
|
||||||
|
<?php if (!Config::get(Config::DISABLE_LOGIN_FORM)) { ?>
|
||||||
<?= \Controls\submit_tag(__('Log in')) ?>
|
<?= \Controls\submit_tag(__('Log in')) ?>
|
||||||
|
<?php } ?>
|
||||||
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_LOGINFORM_ADDITIONAL_BUTTONS) ?>
|
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_LOGINFORM_ADDITIONAL_BUTTONS) ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@@ -203,8 +211,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<a href="https://tt-rss.org/">Tiny Tiny RSS</a>
|
<a href="https://github.com/tt-rss/tt-rss">Tiny Tiny RSS</a>
|
||||||
© 2005–<?= date('Y') ?> <a href="https://fakecake.org/">Andrew Dolgov</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
26
index.php
26
index.php
@@ -19,7 +19,7 @@
|
|||||||
<meta name="viewport" content="initial-scale=1,width=device-width" />
|
<meta name="viewport" content="initial-scale=1,width=device-width" />
|
||||||
|
|
||||||
<?php if ($_SESSION["uid"] && empty($_SESSION["safe_mode"])) {
|
<?php if ($_SESSION["uid"] && empty($_SESSION["safe_mode"])) {
|
||||||
$theme = get_pref(Prefs::USER_CSS_THEME);
|
$theme = Prefs::get(Prefs::USER_CSS_THEME, $_SESSION['uid'], $_SESSION['profile'] ?? null);
|
||||||
if ($theme && theme_exists("$theme")) {
|
if ($theme && theme_exists("$theme")) {
|
||||||
echo stylesheet_tag(get_theme_path($theme), ['id' => 'theme_css']);
|
echo stylesheet_tag(get_theme_path($theme), ['id' => 'theme_css']);
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,9 @@
|
|||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const __csrf_token = "<?= $_SESSION["csrf_token"]; ?>";
|
const __csrf_token = "<?= $_SESSION["csrf_token"]; ?>";
|
||||||
|
|
||||||
|
const __default_light_theme = "<?= get_theme_path(Config::get(Config::DEFAULT_LIGHT_THEME), 'themes/light.css') ?>";
|
||||||
|
const __default_dark_theme = "<?= get_theme_path(Config::get(Config::DEFAULT_DARK_THEME), 'themes/night.css') ?>";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php UserHelper::print_user_stylesheet() ?>
|
<?php UserHelper::print_user_stylesheet() ?>
|
||||||
@@ -36,8 +39,9 @@
|
|||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
<?php
|
<?php
|
||||||
foreach (PluginHost::getInstance()->get_plugins() as $p) {
|
foreach (PluginHost::getInstance()->get_plugins() as $p) {
|
||||||
if (method_exists($p, "get_css")) {
|
$css = $p->get_css();
|
||||||
echo $p->get_css();
|
if ($css) {
|
||||||
|
echo $css;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -73,16 +77,14 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
<?php
|
<?php
|
||||||
foreach (PluginHost::getInstance()->get_plugins() as $n => $p) {
|
foreach (PluginHost::getInstance()->get_plugins() as $n => $p) {
|
||||||
if (method_exists($p, "get_js")) {
|
$script = $p->get_js();
|
||||||
$script = $p->get_js();
|
|
||||||
|
|
||||||
if ($script) {
|
if ($script) {
|
||||||
echo "try {
|
echo "try {
|
||||||
$script
|
$script
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('failed to initialize plugin JS: $n', e);
|
console.warn('failed to initialize plugin JS: $n', e);
|
||||||
}";
|
}";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|||||||
45
js/App.js
45
js/App.js
@@ -14,6 +14,7 @@ const App = {
|
|||||||
global_unread: -1,
|
global_unread: -1,
|
||||||
_widescreen_mode: false,
|
_widescreen_mode: false,
|
||||||
_loading_progress: 0,
|
_loading_progress: 0,
|
||||||
|
_night_mode_retry_timeout: false,
|
||||||
hotkey_actions: {},
|
hotkey_actions: {},
|
||||||
is_prefs: false,
|
is_prefs: false,
|
||||||
LABEL_BASE_INDEX: -1024,
|
LABEL_BASE_INDEX: -1024,
|
||||||
@@ -167,12 +168,25 @@ const App = {
|
|||||||
setInitParam: function(k, v) {
|
setInitParam: function(k, v) {
|
||||||
this._initParams[k] = v;
|
this._initParams[k] = v;
|
||||||
},
|
},
|
||||||
nightModeChanged: function(is_night, link) {
|
nightModeChanged: function(is_night, link, retry = 0) {
|
||||||
console.log("night mode changed to", is_night);
|
console.log("nightModeChanged: night mode changed to", is_night, "retry", retry);
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
const css_override = is_night ? "themes/night.css" : "themes/light.css";
|
if (retry < 15) {
|
||||||
link.setAttribute("href", css_override + "?" + Date.now());
|
window.clearTimeout(this._night_mode_retry_timeout);
|
||||||
|
|
||||||
|
this._night_mode_retry_timeout = window.setTimeout(
|
||||||
|
() => this.nightModeChanged(is_night, link, ++retry),
|
||||||
|
3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.post("backend.php", {op: "RPC", method: "getRuntimeInfo"}, () => {
|
||||||
|
const css_override = is_night ? App.getInitParam("default_dark_theme") : App.getInitParam("default_light_theme");
|
||||||
|
|
||||||
|
link.setAttribute("href", css_override + "?" + Date.now());
|
||||||
|
|
||||||
|
window.clearTimeout(this._night_mode_retry_timeout);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setupNightModeDetection: function(callback) {
|
setupNightModeDetection: function(callback) {
|
||||||
@@ -690,6 +704,8 @@ const App = {
|
|||||||
window.onerror = this.Error.onWindowError;
|
window.onerror = this.Error.onWindowError;
|
||||||
|
|
||||||
this.setInitParam("csrf_token", __csrf_token);
|
this.setInitParam("csrf_token", __csrf_token);
|
||||||
|
this.setInitParam("default_light_theme", __default_light_theme);
|
||||||
|
this.setInitParam("default_dark_theme", __default_dark_theme);
|
||||||
|
|
||||||
this.setupNightModeDetection(() => {
|
this.setupNightModeDetection(() => {
|
||||||
parser.parse();
|
parser.parse();
|
||||||
@@ -829,11 +845,6 @@ const App = {
|
|||||||
|
|
||||||
Headlines.initScrollHandler();
|
Headlines.initScrollHandler();
|
||||||
|
|
||||||
if (this.getInitParam("simple_update")) {
|
|
||||||
console.log("scheduling simple feed updater...");
|
|
||||||
window.setInterval(() => { Feeds.updateRandom() }, 30 * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.getInitParam('check_for_updates')) {
|
if (this.getInitParam('check_for_updates')) {
|
||||||
window.setInterval(() => {
|
window.setInterval(() => {
|
||||||
this.checkForUpdates();
|
this.checkForUpdates();
|
||||||
@@ -1158,8 +1169,20 @@ const App = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.hotkey_actions["feed_debug_viewfeed"] = () => {
|
this.hotkey_actions["feed_debug_viewfeed"] = () => {
|
||||||
App.postOpenWindow("backend.php", {op: "Feeds", method: "view",
|
|
||||||
feed: Feeds.getActive(), timestamps: 1, debug: 1, cat: Feeds.activeIsCat(), csrf_token: __csrf_token});
|
let query = {
|
||||||
|
...{op: "Feeds", method: "view", feed: Feeds.getActive(), timestamps: 1,
|
||||||
|
debug: 1, cat: Feeds.activeIsCat(), csrf_token: __csrf_token},
|
||||||
|
...dojo.formToObject("toolbar-main")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Feeds._search_query) {
|
||||||
|
query = Object.assign(query, Feeds._search_query);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('debug_viewfeed', query);
|
||||||
|
|
||||||
|
App.postOpenWindow("backend.php", query);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.hotkey_actions["feed_edit"] = () => {
|
this.hotkey_actions["feed_edit"] = () => {
|
||||||
|
|||||||
@@ -123,15 +123,12 @@ const Article = {
|
|||||||
Article.setActive(0);
|
Article.setActive(0);
|
||||||
},
|
},
|
||||||
displayUrl: function (id) {
|
displayUrl: function (id) {
|
||||||
const query = {op: "Article", method: "getmetadatabyid", id: id};
|
const hl = Headlines.objectById(id);
|
||||||
|
|
||||||
xhr.json("backend.php", query, (reply) => {
|
if (hl?.link)
|
||||||
if (reply && reply.link) {
|
prompt(__("Article URL:"), hl.link);
|
||||||
prompt(__("Article URL:"), reply.link);
|
else
|
||||||
} else {
|
alert(__("No URL could be displayed for this article."));
|
||||||
alert(__("No URL could be displayed for this article."));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
openInNewWindow: function (id) {
|
openInNewWindow: function (id) {
|
||||||
/* global __csrf_token */
|
/* global __csrf_token */
|
||||||
|
|||||||
@@ -58,10 +58,15 @@ const CommonDialogs = {
|
|||||||
${App.getInitParam('enable_feed_cats') ?
|
${App.getInitParam('enable_feed_cats') ?
|
||||||
`
|
`
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label class='inline'>${__('Place in category:')}</label>
|
<label>${__('Place in category:')}</label>
|
||||||
${reply.cat_select}
|
${reply.cat_select}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<label>${__("Update interval:")}</label>
|
||||||
|
${App.FormFields.select_hash("update_interval", 0, reply.intervals.update)}
|
||||||
|
</fieldset>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div id="feedDlg_feedsContainer" style="display : none">
|
<div id="feedDlg_feedsContainer" style="display : none">
|
||||||
@@ -113,10 +118,10 @@ const CommonDialogs = {
|
|||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
`,
|
`,
|
||||||
show_error: function (msg) {
|
show_error: function (msg, additional_info) {
|
||||||
const elem = App.byId("fadd_error_message");
|
const elem = App.byId("fadd_error_message");
|
||||||
|
|
||||||
elem.innerHTML = msg;
|
elem.innerHTML = `${msg}${additional_info ? `<br><br><h4>${__('Additional information')}</h4>${additional_info}` : ''}`;
|
||||||
|
|
||||||
Element.show(elem);
|
Element.show(elem);
|
||||||
},
|
},
|
||||||
@@ -163,7 +168,7 @@ const CommonDialogs = {
|
|||||||
dialog.show_error(__("Specified URL seems to be invalid."));
|
dialog.show_error(__("Specified URL seems to be invalid."));
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
dialog.show_error(__("Specified URL doesn't seem to contain any feeds."));
|
dialog.show_error(__("Specified URL doesn't seem to contain any feeds."), App.escapeHtml(rc['message']));
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
{
|
{
|
||||||
@@ -188,10 +193,10 @@ const CommonDialogs = {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
dialog.show_error(__("Couldn't download the specified URL: %s").replace("%s", rc['message']));
|
dialog.show_error(__("Couldn't download the specified URL."), App.escapeHtml(rc['message']));
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
dialog.show_error(__("XML validation failed: %s").replace("%s", rc['message']));
|
dialog.show_error(__("Invalid content."), App.escapeHtml(rc['message']));
|
||||||
break;
|
break;
|
||||||
case 7:
|
case 7:
|
||||||
dialog.show_error(__("Error while creating feed database entry."));
|
dialog.show_error(__("Error while creating feed database entry."));
|
||||||
@@ -685,7 +690,7 @@ const CommonDialogs = {
|
|||||||
</section>
|
</section>
|
||||||
<footer>
|
<footer>
|
||||||
<button dojoType='dijit.form.Button' style='float : left' class='alt-info'
|
<button dojoType='dijit.form.Button' style='float : left' class='alt-info'
|
||||||
onclick='window.open("https://tt-rss.org/wiki/GeneratedFeeds")'>
|
onclick='window.open("https://github.com/tt-rss/tt-rss/wiki/Generated-Feeds")'>
|
||||||
<i class='material-icons'>help</i> ${__("More info...")}</button>
|
<i class='material-icons'>help</i> ${__("More info...")}</button>
|
||||||
<button dojoType='dijit.form.Button' onclick="return App.dialogOf(this).regenFeedKey('${feed}', '${is_cat}')">
|
<button dojoType='dijit.form.Button' onclick="return App.dialogOf(this).regenFeedKey('${feed}', '${is_cat}')">
|
||||||
${App.FormFields.icon("refresh")}
|
${App.FormFields.icon("refresh")}
|
||||||
|
|||||||
@@ -30,52 +30,45 @@ const Filters = {
|
|||||||
params.offset = offset;
|
params.offset = offset;
|
||||||
params.limit = test_dialog.limit;
|
params.limit = test_dialog.limit;
|
||||||
|
|
||||||
console.log("getTestResults:" + offset);
|
|
||||||
|
|
||||||
xhr.json("backend.php", params, (result) => {
|
xhr.json("backend.php", params, (result) => {
|
||||||
try {
|
try {
|
||||||
if (result && test_dialog && test_dialog.open) {
|
if (result && test_dialog && test_dialog.open) {
|
||||||
test_dialog.results += result.length;
|
|
||||||
|
|
||||||
console.log("got results:" + result.length);
|
|
||||||
|
|
||||||
const loading_message = test_dialog.domNode.querySelector(".loading-message");
|
const loading_message = test_dialog.domNode.querySelector(".loading-message");
|
||||||
const results_list = test_dialog.domNode.querySelector(".filter-results-list");
|
const results_list = test_dialog.domNode.querySelector(".filter-results-list");
|
||||||
|
|
||||||
loading_message.innerHTML = __("Looking for articles (%d processed, %f found)...")
|
if (result.pre_filtering_count > 0) {
|
||||||
.replace("%f", test_dialog.results)
|
test_dialog.results += result.items.length;
|
||||||
.replace("%d", offset);
|
|
||||||
|
|
||||||
console.log(offset + " " + test_dialog.max_offset);
|
loading_message.innerHTML = __("Looking for articles (%d processed, %f found)...")
|
||||||
|
.replace("%f", test_dialog.results)
|
||||||
|
.replace("%d", offset);
|
||||||
|
|
||||||
for (let i = 0; i < result.length; i++) {
|
results_list.innerHTML += result.items.reduce((current, item) => current + `<li title="${App.escapeHtml(item.rules.join('\n'))}"><span class='title'>${item.title}</span>
|
||||||
const tmp = dojo.create("div", { innerHTML: result[i]});
|
— <span class='feed'>${item.feed_title}</span>, <span class='date'>${item.date}</span>
|
||||||
|
<div class='preview text-muted'>${item.content_preview}</div></li>`, '');
|
||||||
|
|
||||||
results_list.innerHTML += tmp.innerHTML;
|
// get the next batch if there may be more available and testing limits haven't been reached
|
||||||
|
if (result.pre_filtering_count === test_dialog.limit &&
|
||||||
|
test_dialog.results < 30 && offset < test_dialog.max_offset) {
|
||||||
|
window.setTimeout(function () {
|
||||||
|
test_dialog.getTestResults(params, offset + test_dialog.limit);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (test_dialog.results < 30 && offset < test_dialog.max_offset) {
|
// all done-- either the backend found no more pre-filtering entries, or test limits were reached
|
||||||
|
test_dialog.domNode.querySelector(".loading-indicator").hide();
|
||||||
|
|
||||||
// get the next batch
|
if (test_dialog.results == 0) {
|
||||||
window.setTimeout(function () {
|
results_list.innerHTML = `<li class="text-center text-muted">
|
||||||
test_dialog.getTestResults(params, offset + test_dialog.limit);
|
${__('No recent articles matching this filter have been found.')}</li>`;
|
||||||
}, 0);
|
|
||||||
|
|
||||||
|
loading_message.innerHTML = __("Articles matching this filter:");
|
||||||
} else {
|
} else {
|
||||||
// all done
|
loading_message.innerHTML = __("Found at least %d articles matching this filter:")
|
||||||
|
.replace("%d", test_dialog.results);
|
||||||
test_dialog.domNode.querySelector(".loading-indicator").hide();
|
|
||||||
|
|
||||||
if (test_dialog.results == 0) {
|
|
||||||
results_list.innerHTML = `<li class="text-center text-muted">
|
|
||||||
${__('No recent articles matching this filter have been found.')}</li>`;
|
|
||||||
|
|
||||||
loading_message.innerHTML = __("Articles matching this filter:");
|
|
||||||
} else {
|
|
||||||
loading_message.innerHTML = __("Found %d articles matching this filter:")
|
|
||||||
.replace("%d", test_dialog.results);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (!result) {
|
} else if (!result) {
|
||||||
@@ -233,7 +226,7 @@ const Filters = {
|
|||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
${App.FormFields.button_tag(App.FormFields.icon("help") + " " + __("More info"), "", {class: 'pull-left alt-info',
|
${App.FormFields.button_tag(App.FormFields.icon("help") + " " + __("More info"), "", {class: 'pull-left alt-info',
|
||||||
onclick: "window.open('https://tt-rss.org/wiki/ContentFilters')"})}
|
onclick: "window.open('https://github.com/tt-rss/tt-rss/wiki/Content-Filters')"})}
|
||||||
${App.FormFields.submit_tag(App.FormFields.icon("save") + " " + __("Save"), {onclick: "App.dialogOf(this).execute()"})}
|
${App.FormFields.submit_tag(App.FormFields.icon("save") + " " + __("Save"), {onclick: "App.dialogOf(this).execute()"})}
|
||||||
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
|
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
|
||||||
</footer>
|
</footer>
|
||||||
@@ -324,7 +317,9 @@ const Filters = {
|
|||||||
</form>
|
</form>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
dijit.byId("filterDlg_actionSelect").attr('value', action.action_id);
|
const actionSelect = dijit.byId("filterDlg_actionSelect").attr('value', action.action_id);
|
||||||
|
|
||||||
|
edit_action_dialog.toggleParam(actionSelect);
|
||||||
|
|
||||||
/*xhr.post("backend.php", {op: 'Pref_Filters', method: 'newaction', action: actionStr}, (reply) => {
|
/*xhr.post("backend.php", {op: 'Pref_Filters', method: 'newaction', action: actionStr}, (reply) => {
|
||||||
edit_action_dialog.attr('content', reply);
|
edit_action_dialog.attr('content', reply);
|
||||||
|
|||||||
@@ -173,6 +173,9 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
|
|||||||
tnode.markedCounterNode = dojo.create('span', { className: 'counterNode marked', innerHTML: args.item.markedcounter });
|
tnode.markedCounterNode = dojo.create('span', { className: 'counterNode marked', innerHTML: args.item.markedcounter });
|
||||||
domConstruct.place(tnode.markedCounterNode, tnode.rowNode, 'first');
|
domConstruct.place(tnode.markedCounterNode, tnode.rowNode, 'first');
|
||||||
|
|
||||||
|
tnode.publishedCounterNode = dojo.create('span', { className: 'counterNode published', innerHTML: args.item.publishedcounter });
|
||||||
|
domConstruct.place(tnode.publishedCounterNode, tnode.rowNode, 'first');
|
||||||
|
|
||||||
tnode.auxCounterNode = dojo.create('span', { className: 'counterNode aux', innerHTML: args.item.auxcounter });
|
tnode.auxCounterNode = dojo.create('span', { className: 'counterNode aux', innerHTML: args.item.auxcounter });
|
||||||
domConstruct.place(tnode.auxCounterNode, tnode.rowNode, 'first');
|
domConstruct.place(tnode.auxCounterNode, tnode.rowNode, 'first');
|
||||||
|
|
||||||
@@ -188,7 +191,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
|
|||||||
updateCounter: function (item) {
|
updateCounter: function (item) {
|
||||||
const tree = this;
|
const tree = this;
|
||||||
|
|
||||||
//console.log("updateCounter: " + item.id[0] + " " + item.unread + " " + tree);
|
// console.log("updateCounter: " + item.id[0] + " U: " + item.unread + " P: " + item.publishedcounter + " M: " + item.markedcounter);
|
||||||
|
|
||||||
let treeNode = tree._itemNodesMap[item.id];
|
let treeNode = tree._itemNodesMap[item.id];
|
||||||
|
|
||||||
@@ -198,6 +201,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
|
|||||||
treeNode.unreadCounterNode.innerHTML = item.unread;
|
treeNode.unreadCounterNode.innerHTML = item.unread;
|
||||||
treeNode.auxCounterNode.innerHTML = item.auxcounter;
|
treeNode.auxCounterNode.innerHTML = item.auxcounter;
|
||||||
treeNode.markedCounterNode.innerHTML = item.markedcounter;
|
treeNode.markedCounterNode.innerHTML = item.markedcounter;
|
||||||
|
treeNode.publishedCounterNode.innerHTML = item.publishedcounter;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getTooltip: function (item) {
|
getTooltip: function (item) {
|
||||||
@@ -224,6 +228,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
|
|||||||
if (item.unread > 0) rc += " Unread";
|
if (item.unread > 0) rc += " Unread";
|
||||||
if (item.auxcounter > 0) rc += " Has_Aux";
|
if (item.auxcounter > 0) rc += " Has_Aux";
|
||||||
if (item.markedcounter > 0) rc += " Has_Marked";
|
if (item.markedcounter > 0) rc += " Has_Marked";
|
||||||
|
if (item.publishedcounter > 0) rc += " Has_Published";
|
||||||
if (item.updates_disabled > 0) rc += " UpdatesDisabled";
|
if (item.updates_disabled > 0) rc += " UpdatesDisabled";
|
||||||
if (item.bare_id >= App.LABEL_BASE_INDEX && item.bare_id < 0 && !is_cat || item.bare_id == Feeds.FEED_ARCHIVED && !is_cat) rc += " Special";
|
if (item.bare_id >= App.LABEL_BASE_INDEX && item.bare_id < 0 && !is_cat || item.bare_id == Feeds.FEED_ARCHIVED && !is_cat) rc += " Special";
|
||||||
if (item.bare_id == Feeds.CATEGORY_SPECIAL && is_cat) rc += " AlwaysVisible";
|
if (item.bare_id == Feeds.CATEGORY_SPECIAL && is_cat) rc += " AlwaysVisible";
|
||||||
|
|||||||
26
js/Feeds.js
26
js/Feeds.js
@@ -23,7 +23,7 @@ const Feeds = {
|
|||||||
infscroll_disabled: 0,
|
infscroll_disabled: 0,
|
||||||
_infscroll_timeout: false,
|
_infscroll_timeout: false,
|
||||||
_filter_query: false, // TODO: figure out the UI for this
|
_filter_query: false, // TODO: figure out the UI for this
|
||||||
_search_query: false,
|
_search_query: null,
|
||||||
last_search_query: [],
|
last_search_query: [],
|
||||||
_viewfeed_wait_timeout: false,
|
_viewfeed_wait_timeout: false,
|
||||||
_feeds_holder_observer: new IntersectionObserver(
|
_feeds_holder_observer: new IntersectionObserver(
|
||||||
@@ -105,6 +105,7 @@ const Feeds = {
|
|||||||
this.setUnread(id, (kind == "cat"), ctr);
|
this.setUnread(id, (kind == "cat"), ctr);
|
||||||
this.setValue(id, (kind == "cat"), 'auxcounter', parseInt(elems[l].auxcounter));
|
this.setValue(id, (kind == "cat"), 'auxcounter', parseInt(elems[l].auxcounter));
|
||||||
this.setValue(id, (kind == "cat"), 'markedcounter', parseInt(elems[l].markedcounter));
|
this.setValue(id, (kind == "cat"), 'markedcounter', parseInt(elems[l].markedcounter));
|
||||||
|
this.setValue(id, (kind == "cat"), 'publishedcounter', parseInt(elems[l].publishedcounter));
|
||||||
|
|
||||||
if (kind != "cat") {
|
if (kind != "cat") {
|
||||||
this.setValue(id, false, 'error', error);
|
this.setValue(id, false, 'error', error);
|
||||||
@@ -159,7 +160,7 @@ const Feeds = {
|
|||||||
this.reload();
|
this.reload();
|
||||||
},
|
},
|
||||||
cancelSearch: function() {
|
cancelSearch: function() {
|
||||||
this._search_query = "";
|
this._search_query = null;
|
||||||
this.reloadCurrent();
|
this.reloadCurrent();
|
||||||
},
|
},
|
||||||
// null = get all data, [] would give empty response for specific type
|
// null = get all data, [] would give empty response for specific type
|
||||||
@@ -270,6 +271,9 @@ const Feeds = {
|
|||||||
|
|
||||||
console.log('got hash', hash);
|
console.log('got hash', hash);
|
||||||
|
|
||||||
|
if (hash.query)
|
||||||
|
this._search_query = {query: hash.query, search_language: hash.search_language};
|
||||||
|
|
||||||
if (hash.f != undefined) {
|
if (hash.f != undefined) {
|
||||||
this.open({feed: parseInt(hash.f), is_cat: parseInt(hash.c)});
|
this.open({feed: parseInt(hash.f), is_cat: parseInt(hash.c)});
|
||||||
} else {
|
} else {
|
||||||
@@ -329,7 +333,12 @@ const Feeds = {
|
|||||||
console.log('setActive', id, is_cat);
|
console.log('setActive', id, is_cat);
|
||||||
|
|
||||||
window.requestIdleCallback(() => {
|
window.requestIdleCallback(() => {
|
||||||
App.Hash.set({f: id, c: is_cat ? 1 : 0});
|
App.Hash.set({
|
||||||
|
f: id,
|
||||||
|
c: is_cat ? 1 : 0,
|
||||||
|
query: Feeds._search_query?.query,
|
||||||
|
search_language: Feeds._search_query?.search_language,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this._active_feed_id = id;
|
this._active_feed_id = id;
|
||||||
@@ -649,7 +658,7 @@ const Feeds = {
|
|||||||
<footer>
|
<footer>
|
||||||
${reply.show_syntax_help ?
|
${reply.show_syntax_help ?
|
||||||
`${App.FormFields.button_tag(App.FormFields.icon("help") + " " + __("Search syntax"), "",
|
`${App.FormFields.button_tag(App.FormFields.icon("help") + " " + __("Search syntax"), "",
|
||||||
{class: 'alt-info pull-left', onclick: "window.open('https://tt-rss.org/wiki/SearchSyntax')"})}
|
{class: 'alt-info pull-left', onclick: "window.open('https://github.com/tt-rss/tt-rss/wiki/Search-Syntax')"})}
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
${App.FormFields.submit_tag(App.FormFields.icon("search") + " " + __('Search'), {onclick: "App.dialogOf(this).execute()"})}
|
${App.FormFields.submit_tag(App.FormFields.icon("search") + " " + __('Search'), {onclick: "App.dialogOf(this).execute()"})}
|
||||||
@@ -664,7 +673,7 @@ const Feeds = {
|
|||||||
|
|
||||||
// disallow empty queries
|
// disallow empty queries
|
||||||
if (!Feeds._search_query.query)
|
if (!Feeds._search_query.query)
|
||||||
Feeds._search_query = false;
|
Feeds._search_query = null;
|
||||||
|
|
||||||
this.hide();
|
this.hide();
|
||||||
Feeds.reloadCurrent();
|
Feeds.reloadCurrent();
|
||||||
@@ -735,13 +744,6 @@ const Feeds = {
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
|
|
||||||
},
|
},
|
||||||
updateRandom: function() {
|
|
||||||
console.log("in update_random_feed");
|
|
||||||
|
|
||||||
xhr.json("backend.php", {op: "RPC", method: "updaterandomfeed"}, () => {
|
|
||||||
//
|
|
||||||
});
|
|
||||||
},
|
|
||||||
renderIcon: function(feed_id, exists) {
|
renderIcon: function(feed_id, exists) {
|
||||||
const icon_url = App.getInitParam("icons_url") + '?' + dojo.objectToQuery({op: 'feed_icon', id: feed_id});
|
const icon_url = App.getInitParam("icons_url") + '?' + dojo.objectToQuery({op: 'feed_icon', id: feed_id});
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
const rules = this.model.store.getValue(args.item, 'rules');
|
const rules = this.model.store.getValue(args.item, 'rules');
|
||||||
|
|
||||||
if (param) {
|
if (param) {
|
||||||
param = dojo.doc.createElement('span');
|
param = dojo.doc.createElement('ul');
|
||||||
param.className = (enabled != false) ? 'labelParam' : 'labelParam filterDisabled';
|
param.className = (enabled != false) ? 'actions_summary' : 'actions_summary filterDisabled';
|
||||||
param.innerHTML = args.item.param[0];
|
param.innerHTML = args.item.param[0];
|
||||||
domConstruct.place(param, tnode.rowNode, 'first');
|
domConstruct.place(param, tnode.rowNode, 'first');
|
||||||
}
|
}
|
||||||
@@ -165,6 +165,39 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
|
|||||||
alert(__("No filters selected."));
|
alert(__("No filters selected."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
cloneSelectedFilters: function() {
|
||||||
|
const sel_rows = this.getSelectedFilters();
|
||||||
|
|
||||||
|
if (sel_rows.length > 0) {
|
||||||
|
const query = {op: "Pref_Filters", method: "clone", ids: sel_rows.toString()};
|
||||||
|
let proceed = false;
|
||||||
|
|
||||||
|
if (sel_rows.length === 1) {
|
||||||
|
const selected_filter = this.model.getCheckedItems()[0];
|
||||||
|
const new_filter_title = prompt(__("Name for new filter:"),
|
||||||
|
__("Clone of %s").replace("%s", this.model.store.getValue(selected_filter, "bare_name")));
|
||||||
|
|
||||||
|
if (new_filter_title) {
|
||||||
|
query.new_filter_title = new_filter_title;
|
||||||
|
proceed = true;
|
||||||
|
}
|
||||||
|
} else if (sel_rows.length > 1) {
|
||||||
|
proceed = confirm(__("Clone selected filters?"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proceed) {
|
||||||
|
Notify.progress(__("Cloning selected filters..."));
|
||||||
|
|
||||||
|
xhr.post("backend.php", query, () => {
|
||||||
|
this.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(__("No filters selected."));
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ window.addEventListener("load", function() {
|
|||||||
apply_night_mode: function (is_night, link) {
|
apply_night_mode: function (is_night, link) {
|
||||||
console.log("night mode changed to", is_night);
|
console.log("night mode changed to", is_night);
|
||||||
|
|
||||||
|
const light_theme = typeof __default_light_theme != 'undefined' ? __default_light_theme : 'themes/light.css';
|
||||||
|
const dark_theme = typeof __default_dark_theme != 'undefined' ? __default_dark_theme : 'themes/night.css';
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
const css_override = is_night ? "themes/night.css" : "themes/light.css";
|
const css_override = is_night ? dark_theme : light_theme;
|
||||||
|
|
||||||
link.setAttribute("href", css_override + "?" + Date.now());
|
link.setAttribute("href", css_override + "?" + Date.now());
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -15,7 +15,7 @@
|
|||||||
"url": "https://github.com/dojo/dijit.git"
|
"url": "https://github.com/dojo/dijit.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dojo": "1.16.5"
|
"dojo": "1.17.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "dijit",
|
"name": "dijit",
|
||||||
"version": "1.16.5",
|
"version": "1.17.3",
|
||||||
"directories": {
|
"directories": {
|
||||||
"lib": "."
|
"lib": "."
|
||||||
},
|
},
|
||||||
"main": "main",
|
"main": "main",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dojo": "1.16.5"
|
"dojo": "1.17.3"
|
||||||
},
|
},
|
||||||
"description": "Dijit provides a complete collection of user interface controls based on Dojo, giving you the power to create web applications that are highly optimized for usability, performance, internationalization, accessibility, but above all deliver an incredible user experience.",
|
"description": "Dijit provides a complete collection of user interface controls based on Dojo, giving you the power to create web applications that are highly optimized for usability, performance, internationalization, accessibility, but above all deliver an incredible user experience.",
|
||||||
"license" : "BSD-3-Clause OR AFL-2.1",
|
"license" : "BSD-3-Clause OR AFL-2.1",
|
||||||
|
|||||||
@@ -5,40 +5,40 @@
|
|||||||
# It will automatically replace previous build of Dojo in ../dojo
|
# It will automatically replace previous build of Dojo in ../dojo
|
||||||
|
|
||||||
# Dojo requires Java runtime to build. Further information on rebuilding Dojo
|
# Dojo requires Java runtime to build. Further information on rebuilding Dojo
|
||||||
# is available here: http://dojotoolkit.org/reference-guide/build/index.html
|
# is available here: https://dojotoolkit.org/reference-guide/build/index.html
|
||||||
|
|
||||||
VERSION=1.16.5
|
VERSION=1.17.3
|
||||||
|
|
||||||
# Download and extract dojo src code if it doesn't already exist
|
# Download and extract dojo src code if it doesn't already exist
|
||||||
if [ ! -d "dojo" ]; then
|
if [ ! -d "dojo" ]; then
|
||||||
TARBALL=dojo-release-$VERSION-src.tar.gz
|
TARBALL=dojo-release-$VERSION-src.tar.gz
|
||||||
if [ ! -f $TARBALL ]; then
|
if [ ! -f $TARBALL ]; then
|
||||||
wget -q http://download.dojotoolkit.org/release-$VERSION/$TARBALL
|
wget https://download.dojotoolkit.org/release-$VERSION/$TARBALL
|
||||||
fi
|
fi
|
||||||
tar -zxf $TARBALL
|
tar -zxf $TARBALL
|
||||||
mv dojo-release-$VERSION-src/* .
|
mv dojo-release-$VERSION-src/* .
|
||||||
rm -rf dojo-release-$VERSION-src
|
rm -rf dojo-release-$VERSION-src
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d util/buildscripts/ ]; then
|
if [ -d util/buildscripts/ ]; then
|
||||||
rm -rf release/dojo
|
rm -rf release/dojo
|
||||||
|
|
||||||
pushd util/buildscripts
|
pushd util/buildscripts
|
||||||
./build.sh profile=../../tt-rss action=release optimize=shrinksafe cssOptimize=comments
|
./build.sh profile=../../tt-rss action=release optimize=shrinksafe cssOptimize=comments
|
||||||
popd
|
popd
|
||||||
|
|
||||||
if [ -d release/dojo ]; then
|
if [ -d release/dojo ]; then
|
||||||
rm -rf ../dojo ../dijit
|
rm -rf ../dojo ../dijit
|
||||||
cp -r release/dojo/dojo ..
|
cp -r release/dojo/dojo ..
|
||||||
cp -r release/dojo/dijit ..
|
cp -r release/dojo/dijit ..
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
find dojo -name '*uncompressed*' -exec rm -- {} \;
|
find dojo -name '*uncompressed*' -exec rm -- {} \;
|
||||||
find dijit -name '*uncompressed*' -exec rm -- {} \;
|
find dijit -name '*uncompressed*' -exec rm -- {} \;
|
||||||
else
|
else
|
||||||
echo $0: ERROR: Dojo build seems to have failed.
|
echo $0: ERROR: Dojo build seems to have failed.
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo $0: ERROR: Please unpack Dojo source release into current directory.
|
echo $0: ERROR: Please unpack Dojo source release into current directory.
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
//>>built
|
//>>built
|
||||||
define("dojo/_base/array",["./kernel","../has","./lang"],function(_1,_2,_3){var _4={},u;function _5(fn){return _4[fn]=new Function("item","index","array",fn);};function _6(_7){var _8=!_7;return function(a,fn,o){var i=0,l=a&&a.length||0,_9;if(l&&typeof a=="string"){a=a.split("");}if(typeof fn=="string"){fn=_4[fn]||_5(fn);}if(o){for(;i<l;++i){_9=!fn.call(o,a[i],i,a);if(_7^_9){return !_9;}}}else{for(;i<l;++i){_9=!fn(a[i],i,a);if(_7^_9){return !_9;}}}return _8;};};function _a(up){var _b=1,_c=0,_d=0;if(!up){_b=_c=_d=-1;}return function(a,x,_e,_f){if(_f&&_b>0){return _10.lastIndexOf(a,x,_e);}var l=a&&a.length||0,end=up?l+_d:_c,i;if(_e===u){i=up?_c:l+_d;}else{if(_e<0){i=l+_e;if(i<0){i=_c;}}else{i=_e>=l?l+_d:_e;}}if(l&&typeof a=="string"){a=a.split("");}for(;i!=end;i+=_b){if(a[i]==x){return i;}}return -1;};};var _10={every:_6(false),some:_6(true),indexOf:_a(true),lastIndexOf:_a(false),forEach:function(arr,_11,_12){var i=0,l=arr&&arr.length||0;if(l&&typeof arr=="string"){arr=arr.split("");}if(typeof _11=="string"){_11=_4[_11]||_5(_11);}if(_12){for(;i<l;++i){_11.call(_12,arr[i],i,arr);}}else{for(;i<l;++i){_11(arr[i],i,arr);}}},map:function(arr,_13,_14,Ctr){var i=0,l=arr&&arr.length||0,out=new (Ctr||Array)(l);if(l&&typeof arr=="string"){arr=arr.split("");}if(typeof _13=="string"){_13=_4[_13]||_5(_13);}if(_14){for(;i<l;++i){out[i]=_13.call(_14,arr[i],i,arr);}}else{for(;i<l;++i){out[i]=_13(arr[i],i,arr);}}return out;},filter:function(arr,_15,_16){var i=0,l=arr&&arr.length||0,out=[],_17;if(l&&typeof arr=="string"){arr=arr.split("");}if(typeof _15=="string"){_15=_4[_15]||_5(_15);}if(_16){for(;i<l;++i){_17=arr[i];if(_15.call(_16,_17,i,arr)){out.push(_17);}}}else{for(;i<l;++i){_17=arr[i];if(_15(_17,i,arr)){out.push(_17);}}}return out;},clearCache:function(){_4={};}};1&&_3.mixin(_1,_10);return _10;});
|
define("dojo/_base/array",["./kernel","../has","./lang"],function(_1,_2,_3){var _4={},u;var _5;if(!_2("csp-restrictions")){_5=function(fn){return _4[fn]=new Function("item","index","array",fn);};}function _6(_7){var _8=!_7;return function(a,fn,o){var i=0,l=a&&a.length||0,_9;if(l&&typeof a=="string"){a=a.split("");}if(typeof fn=="string"){if(_2("csp-restrictions")){throw new TypeError("callback must be a function");}else{fn=_4[fn]||_5(fn);}}if(o){for(;i<l;++i){_9=!fn.call(o,a[i],i,a);if(_7^_9){return !_9;}}}else{for(;i<l;++i){_9=!fn(a[i],i,a);if(_7^_9){return !_9;}}}return _8;};};function _a(up){var _b=1,_c=0,_d=0;if(!up){_b=_c=_d=-1;}return function(a,x,_e,_f){if(_f&&_b>0){return _10.lastIndexOf(a,x,_e);}var l=a&&a.length||0,end=up?l+_d:_c,i;if(_e===u){i=up?_c:l+_d;}else{if(_e<0){i=l+_e;if(i<0){i=_c;}}else{i=_e>=l?l+_d:_e;}}if(l&&typeof a=="string"){a=a.split("");}for(;i!=end;i+=_b){if(a[i]==x){return i;}}return -1;};};var _10={every:_6(false),some:_6(true),indexOf:_a(true),lastIndexOf:_a(false),forEach:function(arr,_11,_12){var i=0,l=arr&&arr.length||0;if(l&&typeof arr=="string"){arr=arr.split("");}if(typeof _11=="string"){if(_2("csp-restrictions")){throw new TypeError("callback must be a function");}else{_11=_4[_11]||_5(_11);}}if(_12){for(;i<l;++i){_11.call(_12,arr[i],i,arr);}}else{for(;i<l;++i){_11(arr[i],i,arr);}}},map:function(arr,_13,_14,Ctr){var i=0,l=arr&&arr.length||0,out=new (Ctr||Array)(l);if(l&&typeof arr=="string"){arr=arr.split("");}if(typeof _13=="string"){if(_2("csp-restrictions")){throw new TypeError("callback must be a function");}else{_13=_4[_13]||_5(_13);}}if(_14){for(;i<l;++i){out[i]=_13.call(_14,arr[i],i,arr);}}else{for(;i<l;++i){out[i]=_13(arr[i],i,arr);}}return out;},filter:function(arr,_15,_16){var i=0,l=arr&&arr.length||0,out=[],_17;if(l&&typeof arr=="string"){arr=arr.split("");}if(typeof _15=="string"){if(_2("csp-restrictions")){throw new TypeError("callback must be a function");}else{_15=_4[_15]||_5(_15);}}if(_16){for(;i<l;++i){_17=arr[i];if(_15.call(_16,_17,i,arr)){out.push(_17);}}}else{for(;i<l;++i){_17=arr[i];if(_15(_17,i,arr)){out.push(_17);}}}return out;},clearCache:function(){_4={};}};1&&_3.mixin(_1,_10);return _10;});
|
||||||
File diff suppressed because one or more lines are too long
@@ -5,4 +5,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
//>>built
|
//>>built
|
||||||
define("dojo/_base/kernel",["../global","../has","./config","require","module"],function(_1,_2,_3,_4,_5){var i,p,_6={},_7={},_8={config:_3,global:_1,dijit:_6,dojox:_7};var _9={dojo:["dojo",_8],dijit:["dijit",_6],dojox:["dojox",_7]},_a=(_4.map&&_4.map[_5.id.match(/[^\/]+/)[0]]),_b;for(p in _a){if(_9[p]){_9[p][0]=_a[p];}else{_9[p]=[_a[p],{}];}}for(p in _9){_b=_9[p];_b[1]._scopeName=_b[0];if(!_3.noGlobals){_1[_b[0]]=_b[1];}}_8.scopeMap=_9;_8.baseUrl=_8.config.baseUrl=_4.baseUrl;_8.isAsync=!1||_4.async;_8.locale=_3.locale;var _c="$Rev:$".match(/[0-9a-f]{7,}/);_8.version={major:1,minor:16,patch:5,flag:"",revision:_c?_c[0]:NaN,toString:function(){var v=_8.version;return v.major+"."+v.minor+"."+v.patch+v.flag+" ("+v.revision+")";}};1||_2.add("extend-dojo",1);if(!_2("csp-restrictions")){(Function("d","d.eval = function(){return d.global.eval ? d.global.eval(arguments[0]) : eval(arguments[0]);}"))(_8);}if(0){_8.exit=function(_d){quit(_d);};}else{_8.exit=function(){};}if(!_2("host-webworker")){1||_2.add("dojo-guarantee-console",1);}if(1){_2.add("console-as-object",function(){return Function.prototype.bind&&console&&typeof console.log==="object";});typeof console!="undefined"||(console={});var cn=["assert","count","debug","dir","dirxml","error","group","groupEnd","info","profile","profileEnd","time","timeEnd","trace","warn","log"];var tn;i=0;while((tn=cn[i++])){if(!console[tn]){(function(){var _e=tn+"";console[_e]=("log" in console)?function(){var a=Array.prototype.slice.call(arguments);a.unshift(_e+":");console["log"](a.join(" "));}:function(){};console[_e]._fake=true;})();}else{if(_2("console-as-object")){console[tn]=Function.prototype.bind.call(console[tn],console);}}}}_2.add("dojo-debug-messages",!!_3.isDebug);_8.deprecated=_8.experimental=function(){};if(_2("dojo-debug-messages")){_8.deprecated=function(_f,_10,_11){var _12="DEPRECATED: "+_f;if(_10){_12+=" "+_10;}if(_11){_12+=" -- will be removed in version: "+_11;}console.warn(_12);};_8.experimental=function(_13,_14){var _15="EXPERIMENTAL: "+_13+" -- APIs subject to change without notice.";if(_14){_15+=" "+_14;}console.warn(_15);};}1||_2.add("dojo-modulePaths",1);if(1){if(_3.modulePaths){_8.deprecated("dojo.modulePaths","use paths configuration");var _16={};for(p in _3.modulePaths){_16[p.replace(/\./g,"/")]=_3.modulePaths[p];}_4({paths:_16});}}1||_2.add("dojo-moduleUrl",1);if(1){_8.moduleUrl=function(_17,url){_8.deprecated("dojo.moduleUrl()","use require.toUrl","2.0");var _18=null;if(_17){_18=_4.toUrl(_17.replace(/\./g,"/")+(url?("/"+url):"")+"/*.*").replace(/\/\*\.\*/,"")+(url?"":"/");}return _18;};}_8._hasResource={};return _8;});
|
define("dojo/_base/kernel",["../global","../has","./config","require","module"],function(_1,_2,_3,_4,_5){var i,p,_6={},_7={},_8={config:_3,global:_1,dijit:_6,dojox:_7};var _9={dojo:["dojo",_8],dijit:["dijit",_6],dojox:["dojox",_7]},_a=(_4.map&&_4.map[_5.id.match(/[^\/]+/)[0]]),_b;for(p in _a){if(_9[p]){_9[p][0]=_a[p];}else{_9[p]=[_a[p],{}];}}for(p in _9){_b=_9[p];_b[1]._scopeName=_b[0];if(!_3.noGlobals){_1[_b[0]]=_b[1];}}_8.scopeMap=_9;_8.baseUrl=_8.config.baseUrl=_4.baseUrl;_8.isAsync=!1||_4.async;_8.locale=_3.locale;var _c="$Rev:$".match(/[0-9a-f]{7,}/);_8.version={major:1,minor:17,patch:3,flag:"",revision:_c?_c[0]:NaN,toString:function(){var v=_8.version;return v.major+"."+v.minor+"."+v.patch+v.flag+" ("+v.revision+")";}};1||_2.add("extend-dojo",1);if(!_2("csp-restrictions")){(Function("d","d.eval = function(){return d.global.eval ? d.global.eval(arguments[0]) : eval(arguments[0]);}"))(_8);}if(0){_8.exit=function(_d){quit(_d);};}else{_8.exit=function(){};}if(!_2("host-webworker")){1||_2.add("dojo-guarantee-console",1);}if(1){_2.add("console-as-object",function(){return Function.prototype.bind&&console&&typeof console.log==="object";});typeof console!="undefined"||(console={});var cn=["assert","count","debug","dir","dirxml","error","group","groupEnd","info","profile","profileEnd","time","timeEnd","trace","warn","log"];var tn;i=0;while((tn=cn[i++])){if(!console[tn]){(function(){var _e=tn+"";console[_e]=("log" in console)?function(){var a=Array.prototype.slice.call(arguments);a.unshift(_e+":");console["log"](a.join(" "));}:function(){};console[_e]._fake=true;})();}else{if(_2("console-as-object")){console[tn]=Function.prototype.bind.call(console[tn],console);}}}}_2.add("dojo-debug-messages",!!_3.isDebug);_8.deprecated=_8.experimental=function(){};if(_2("dojo-debug-messages")){_8.deprecated=function(_f,_10,_11){var _12="DEPRECATED: "+_f;if(_10){_12+=" "+_10;}if(_11){_12+=" -- will be removed in version: "+_11;}console.warn(_12);};_8.experimental=function(_13,_14){var _15="EXPERIMENTAL: "+_13+" -- APIs subject to change without notice.";if(_14){_15+=" "+_14;}console.warn(_15);};}1||_2.add("dojo-modulePaths",1);if(1){if(_3.modulePaths){_8.deprecated("dojo.modulePaths","use paths configuration");var _16={};for(p in _3.modulePaths){_16[p.replace(/\./g,"/")]=_3.modulePaths[p];}_4({paths:_16});}}1||_2.add("dojo-moduleUrl",1);if(1){_8.moduleUrl=function(_17,url){_8.deprecated("dojo.moduleUrl()","use require.toUrl","2.0");var _18=null;if(_17){_18=_4.toUrl(_17.replace(/\./g,"/")+(url?("/"+url):"")+"/*.*").replace(/\/\*\.\*/,"")+(url?"":"/");}return _18;};}_8._hasResource={};return _8;});
|
||||||
2
lib/dojo/dojo.js
vendored
2
lib/dojo/dojo.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
8
lib/dojo/json5.js
Normal file
8
lib/dojo/json5.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2004-2016, The JS Foundation All Rights Reserved.
|
||||||
|
Available via Academic Free License >= 2.1 OR the modified BSD license.
|
||||||
|
see: http://dojotoolkit.org/license for details
|
||||||
|
*/
|
||||||
|
|
||||||
|
//>>built
|
||||||
|
define("dojo/json5",["./json5/parse"],function(_1){return {parse:_1};});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2018 TheCodingMachine
|
Copyright (c) 2012-2018 Aseem Kishore, and [others].
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -18,4 +18,6 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
[others]: https://github.com/json5/json5/contributors
|
||||||
28
lib/dojo/json5/README.md
Normal file
28
lib/dojo/json5/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
These modules are adapted from the [JSON5](https://github.com/json5/json5) project. JSON5 was adopted by
|
||||||
|
the Dojo Toolkit for use by `dojo/parser` to facilitate parsing data attributes without using the unsafe
|
||||||
|
JavaScript function `eval()`. As such only the parsing related modules from JSON5 are included.
|
||||||
|
|
||||||
|
Updates from the JSON5 project can be incorporated into the Dojo Toolkit with the following process:
|
||||||
|
|
||||||
|
* Clone the [JSON5 repository](https://github.com/json5/json5.git)
|
||||||
|
* Convert the relevant files to ES5 syntax with TypeScript's compiler:
|
||||||
|
```bash
|
||||||
|
tsc lib/parse.js lib/unicode.js lib/util.js --allowJs --module ES6 --outDir dojo --removeComments --target ES5
|
||||||
|
```
|
||||||
|
* Visually compare the existing modules in `dojo/json5` with the newly converted modules to see what changes will need
|
||||||
|
to be made
|
||||||
|
* Copy the files from the `json5/dojo` folder to the `dojo/json5` folder
|
||||||
|
* Manual updates:
|
||||||
|
* IMPORTANT: wrap the `lexStates` object property `default:` in quotes => `'default':`
|
||||||
|
* convert indentation to tabs in each module
|
||||||
|
* remove any trailing commas
|
||||||
|
* convert each module to AMD syntax
|
||||||
|
* Update `json5/parse.js` to use `dojo/string` methods for ES5 String methods:
|
||||||
|
* require `'../string'` as `dstring`
|
||||||
|
* replace calls to `codePointAt` with `dstring.codePointAt(str, position)`
|
||||||
|
* replace calls to `String.fromCodePoint` with `dstring.fromCodePoint`
|
||||||
|
* Run Dojo's JSON5 tests to ensure the updates were successful:
|
||||||
|
* `dojo/node_modules/intern-geezer/client.html?config=tests/dojo.intern&suites=tests/unit/json5`
|
||||||
|
* Update the line below recording the most recent update
|
||||||
|
|
||||||
|
Current as of 2020-06-12, commit [32bb2cd](https://github.com/json5/json5/commit/32bb2cdae4864b2ac80a6d9b4045efc4cc54f47a)
|
||||||
8
lib/dojo/json5/parse.js
Normal file
8
lib/dojo/json5/parse.js
Normal file
File diff suppressed because one or more lines are too long
8
lib/dojo/json5/unicode.js
Normal file
8
lib/dojo/json5/unicode.js
Normal file
File diff suppressed because one or more lines are too long
8
lib/dojo/json5/util.js
Normal file
8
lib/dojo/json5/util.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2004-2016, The JS Foundation All Rights Reserved.
|
||||||
|
Available via Academic Free License >= 2.1 OR the modified BSD license.
|
||||||
|
see: http://dojotoolkit.org/license for details
|
||||||
|
*/
|
||||||
|
|
||||||
|
//>>built
|
||||||
|
define("dojo/json5/util",["./unicode"],function(_1){return {isSpaceSeparator:function(c){return typeof c==="string"&&_1.Space_Separator.test(c);},isIdStartChar:function(c){return typeof c==="string"&&((c>="a"&&c<="z")||(c>="A"&&c<="Z")||(c==="$")||(c==="_")||_1.ID_Start.test(c));},isIdContinueChar:function(c){return typeof c==="string"&&((c>="a"&&c<="z")||(c>="A"&&c<="Z")||(c>="0"&&c<="9")||(c==="$")||(c==="_")||(c==="")||(c==="")||_1.ID_Continue.test(c));},isDigit:function(c){return typeof c==="string"&&/[0-9]/.test(c);},isHexDigit:function(c){return typeof c==="string"&&/[0-9A-Fa-f]/.test(c);},};});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user