2770 Commits

Author SHA1 Message Date
supahgreg
a95f0a4844 Uncomment the Weblate item and mention which platforms Docker images are created for. 2025-10-06 21:26:17 +00:00
Greg
2cfb58cf8c Merge pull request #8 from tt-rss/feature/switch-to-github-org
Switch links to the 'tt-rss' GitHub organization
2025-10-06 14:33:29 -05:00
supahgreg
a7d5566aa9 Switch links to the 'tt-rss' GitHub organization. 2025-10-06 19:02:28 +00:00
Greg
14ac789ddc Merge pull request #7 from supahgreg/bugfix/idiorm-composer-info
Correct idiorm dependency info
2025-10-06 11:37:54 -05:00
supahgreg
d89e825c8a Correct idiorm dependency info. 2025-10-06 16:01:54 +00:00
supahgreg
7c69ee73e1 minor: Tweak some words in the README. 2025-10-06 03:23:43 +00:00
supahgreg
f23fff932a README improvements. 2025-10-06 03:12:06 +00:00
supahgreg
958d25b831 minor: Update the wiki link in 'Config::sanity_check()'. 2025-10-06 02:45:35 +00:00
supahgreg
d5c3d75ff2 Add a GitHub pull request template. 2025-10-05 21:42:17 +00:00
supahgreg
b49de6e92c Add some basic content to 'CONTRIBUTING.md'. 2025-10-05 21:32:25 +00:00
supahgreg
ceab15254b minor: make PHPStan happy after a4eed151b7 2025-10-05 21:23:24 +00:00
supahgreg
a4eed151b7 minor: Deal with potentially-missing 'inverse' key when testing filters. 2025-10-05 21:19:42 +00:00
Greg
18f59793e0 Merge pull request #1 from supahgreg/feature/phpstan-bump
Bump PHPStan to 2.1.30 and address new findings.
2025-10-05 15:53:55 -05:00
supahgreg
e0b116f904 Bump PHPStan to 2.1.30 and address new findings.
Also some minor adjacent cleanup.
2025-10-05 20:51:13 +00:00
supahgreg
ec367b23f4 Switch from 'tt-rss-web-static' links to wiki links. 2025-10-05 20:18:56 +00:00
supahgreg
eb05374f24 minor wording tweaks in the Enhancement Request issue template. 2025-10-05 18:36:58 +00:00
supahgreg
0b50b0f1f0 Add some GitHub issue templates. 2025-10-05 18:34:18 +00:00
supahgreg
63464f3729 Add a Dependabot config for PHP package dependency updates. 2025-10-05 05:46:10 +00:00
supahgreg
a135edcfb1 Drop the 'eslint-formatter-gitlab' dev dependency. 2025-10-05 05:26:50 +00:00
supahgreg
2878ae815f Bump node dev dependencies. 2025-10-05 05:23:35 +00:00
supahgreg
46ebef7ebf Bump Dojo from 1.16.5 to 1.17.3.
This should help with some of the security findings.
2025-10-05 04:34:29 +00:00
supahgreg
6e8a188e4a Include a short SHA tag when publishing images. 2025-10-05 03:58:49 +00:00
supahgreg
435358265c Provide expected Docker image build args in the Publish workflow. 2025-10-05 01:51:55 +00:00
supahgreg
cb17b3b95e Mention that Docker images are now being published. 2025-10-05 00:56:27 +00:00
supahgreg
c3fbc81e19 Ignore various paths in the Publish workflow. 2025-10-05 00:55:00 +00:00
supahgreg
0bc55e8366 Enable building+publishing the 'web-nginx' image. 2025-10-05 00:33:04 +00:00
supahgreg
b6fd27c756 Restore behavior of using the 'latest' tag on images.
A 'main' tag was being created+used.

https://github.com/docker/metadata-action?tab=readme-ov-file#latest-tag
2025-10-05 00:21:36 +00:00
supahgreg
fc95bae2a6 Add a Publish workflow. 2025-10-05 00:13:35 +00:00
supahgreg
c3f3eb8387 Revert PHPUnit job name change from 3d8c54877f.
GitHub doesn't handle long-ish names well in the UI, so just go with the default behavior.
2025-10-04 22:20:50 +00:00
supahgreg
3d8c54877f Update some names in the PHP Code Quality workflow. 2025-10-04 22:17:15 +00:00
supahgreg
a32720615f Also trigger the PHP Code Quality workflow if 'phpunit.xml' changes. 2025-10-04 21:56:58 +00:00
supahgreg
4f2423198a Add a workflow job for PHPUnit. 2025-10-04 21:53:44 +00:00
supahgreg
664f37ae71 Appeasing PHPStan.
The two if statements involved are related to calls to 'tt-rss.org' that are intentionally disabled.
2025-10-04 21:20:32 +00:00
supahgreg
591ee81ad3 Add a GitHub workflow for code quality (initially existing PHPStan). 2025-10-04 21:15:25 +00:00
supahgreg
4583ae8dc3 Mention why the commit history is a bit inaccurate on 'github.com'. 2025-10-04 00:52:51 +00:00
supahgreg
d7a91de140 Initial attempt to support both 'main' and 'master' branches. 2025-10-03 22:03:52 +00:00
supahgreg
5e99eb41ec Remove references to, and integrations with, 'tt-rss.org'. 2025-10-03 21:00:43 +00:00
Andrew Dolgov
c67b943aa8 Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!188
2025-10-02 11:17:38 +03:00
Rivo Zängov
7c79b771e1 Translated using Weblate (Estonian)
Currently translated at 41.5% (292 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/et/
2025-10-01 13:02:03 +00:00
Andrew Dolgov
b369bf8107 Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!187
2025-09-27 21:22:48 +03:00
BlueTurtle
437f8515ad Translated using Weblate (French)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/fr/
2025-09-27 12:02:22 +02:00
Andrew Dolgov
35aa534c71 Merge branch 'bugfix/cli-opml-export-owner_uid' into 'master'
Fix OPML exporting via 'update.php'.

See merge request tt-rss/tt-rss!186
2025-09-18 07:18:17 +03:00
wn_
98779acdc5 Fix OPML exporting via 'update.php'. 2025-09-18 00:29:31 +00:00
Andrew Dolgov
139632c417 Merge branch 'builtin-feed' into 'master'
use built-in feed for integration test

See merge request tt-rss/tt-rss!185
2025-09-02 22:24:44 +03:00
Andrew Dolgov
36644365c8 use built-in feed for integration test 2025-09-02 22:22:24 +03:00
Andrew Dolgov
be3ee920b1 Merge branch 'update-comicpress' into 'master'
Update ComicPress logic

See merge request tt-rss/tt-rss!183
2025-09-02 21:24:44 +03:00
vjkcxl
c914d0710f Fix Danby Draws 2025-09-02 11:15:21 -05:00
vjkcxl
17bd835530 Revert whitespace changes 2025-09-02 10:59:59 -05:00
vjkcxl
17c6d7af8d Hopefully fix PHPStan 2025-09-01 14:42:00 -05:00
vjkcxl
6c0bcd90ed Try to add types 2025-09-01 14:32:23 -05:00
vjkcxl
efe6fbd3fa Update ComicPress logic
This updates the logic to work across a variety of additional sites.
Additionally, it grabs the author's comments from comics, such as the text on Buttersafe.

This does not update the list of supported comics.
There are too many comic sites to enumerate all of them anyway.
2025-09-01 14:06:10 -05:00
wn
98dbf49733 Merge branch 'bugfix/published-feed-title' into 'master'
Fix getting the title for syndicated feeds.

See merge request tt-rss/tt-rss!182
2025-08-29 10:03:27 -05:00
wn_
ecef0ae951 Tweak the 'Feeds::_get_title()' param order to make PHP happy.
Required params need to go before optional.
2025-08-29 12:28:05 +00:00
wn_
fd5ce90efe Minor fix in 'OPML#opml_export()'. 2025-08-29 12:22:27 +00:00
wn_
e5c5a1bf42 Make 'owner_uid' required for 'Feeds::_get_title()' and 'Feeds::_get_cat_title()'. 2025-08-29 12:19:04 +00:00
wn_
9aafc7bb8d Fix getting the title for syndicated feeds.
https://gitlab.tt-rss.org/tt-rss/tt-rss/-/merge_requests/181 missed that 'Feeds::_get_title()' gets invoked when headlines are generated for syndicated feeds.
2025-08-29 12:04:20 +00:00
Andrew Dolgov
57cd48d9f7 Merge branch 'bugfix/limit-more-by-uid' into 'master'
Filter more results by user ID.

See merge request tt-rss/tt-rss!181
2025-08-26 08:14:10 +03:00
wn_
9982871ac1 Filter more results by user ID. 2025-08-25 16:06:41 +00:00
Andrew Dolgov
2d12ced897 Merge branch 'feature/guzzle-7.10.0' into 'master'
Bump Guzzle to 7.10.0 for PHP 8.5 compatibility.

See merge request tt-rss/tt-rss!180
2025-08-25 13:39:41 +03:00
wn_
618cb5bf78 Bump Guzzle to 7.10.0 for PHP 8.5 compatibility.
https://github.com/guzzle/guzzle/compare/7.9.2...7.10.0
2025-08-24 14:40:07 +00:00
Andrew Dolgov
f7fc00326e Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!179
2025-08-15 08:02:19 +03:00
Edgars Andersons
31d5887831 Translated using Weblate (Latvian)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/lv/
2025-08-14 20:02:12 +00:00
Andrew Dolgov
0669995c47 Merge branch 'autoswitch-retries' into 'master'
bump amount of retries when trying to auto-switch theme

See merge request tt-rss/tt-rss!178
2025-08-14 18:37:05 +03:00
Andrew Dolgov
25f416719b bump amount of retries when trying to auto-switch theme 2025-08-14 18:34:26 +03:00
Andrew Dolgov
e8db412dfc Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!177
2025-08-13 22:05:27 +03:00
josé m
c60b4a6a69 Translated using Weblate (Galician)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/gl/
2025-08-13 19:02:17 +00:00
Andrew Dolgov
a627f40477 Merge branch 'docker-updater-patch' into 'master'
Add update-ca-certificates to updater.sh

See merge request tt-rss/tt-rss!176
2025-08-13 15:02:31 +03:00
Julian Labus
1cd05e5b61 Add update-ca-certificates to updater.sh 2025-08-13 13:38:33 +02:00
Andrew Dolgov
20843a9d15 Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!175
2025-08-11 21:45:06 +03:00
Patrick Ahles
aff4e3e840 Translated using Weblate (French)
Currently translated at 99.4% (699 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/fr/
2025-08-11 18:02:49 +02:00
Andrew Dolgov
13e1d674ee Merge branch 'bugfix/default-dark-theme-bookmarklets' into 'master'
Fix default dark mode CSS path in UtilityJS, use configurable default themes in bookmarklets.

See merge request tt-rss/tt-rss!174
2025-08-10 17:30:57 +03:00
wn_
b803f85ec2 Fix default dark mode CSS path in UtilityJS, use configurable default themes in bookmarklets. 2025-08-10 12:27:38 +00:00
Andrew Dolgov
c6624d06a8 Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!173
2025-08-10 12:26:28 +03:00
Edgars Andersons
9de87345c2 Translated using Weblate (Latvian)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/lv/
2025-08-09 14:01:52 +02:00
Andrew Dolgov
4758c5f0bf Merge branch 'navigator-offline' into 'master'
check if backend is reachable when handling night mode change instead of...

See merge request tt-rss/tt-rss!172
2025-08-08 14:56:44 +03:00
Andrew Dolgov
d84260b59d check if backend is reachable when handling night mode change instead of relying on navigator.onLine 2025-08-08 14:36:52 +03:00
Andrew Dolgov
2ec0aa7cad Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!171
2025-08-08 14:23:18 +03:00
Edgars Andersons
91ac7c3f1f Translated using Weblate (Latvian)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/lv/
2025-08-08 09:02:29 +00:00
ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝)
6fc1ba172c Translated using Weblate (Latvian)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/lv/
2025-08-07 07:01:59 +02:00
Andrew Dolgov
75b035b93c Merge branch 'fix-youtube-nocookie' into 'master'
Fix youtube-nocookie support

See merge request tt-rss/tt-rss!170
2025-08-05 07:30:20 +03:00
vjkcxl
9fd54db30d Fix youtube-nocookie support 2025-08-04 22:55:28 -05:00
Andrew Dolgov
906063b1d1 Merge branch 'configurable-default-themes' into 'master'
make default light/dark themes configurable, add support for main application and login form

See merge request tt-rss/tt-rss!169
2025-08-04 20:09:08 +03:00
Andrew Dolgov
19fc3bff21 drop unused legacy (?) handler method OPML->import() 2025-08-04 20:05:59 +03:00
Andrew Dolgov
0d0745da44 add necessary plumbing for auto light/dark switch for the forgotpass handler 2025-08-04 19:54:12 +03:00
Andrew Dolgov
8eb340c3ca make default light/dark themes configurable, add support for main application and login form 2025-08-01 17:22:21 +03:00
Andrew Dolgov
851ddf4bbe Merge branch 'publishedcounter' into 'master'
implement special counter display when viewing by published, similar to marked

See merge request tt-rss/tt-rss!168
2025-07-30 20:23:50 +03:00
Andrew Dolgov
c7aa33fe8a fix publishedcounter not working for feed counters 2025-07-30 20:04:02 +03:00
Andrew Dolgov
aeb0d42f89 add missing count_published to get_cats() and fix some misleading indenting 2025-07-30 19:57:56 +03:00
Andrew Dolgov
bc312b1205 implement special counter display when viewing by published, similar to marked 2025-07-30 19:24:53 +03:00
wn
8b07dc8453 Merge branch 'retry-night-mode-switch' into 'master'
attempt to retry night mode switch if we're offline

See merge request tt-rss/tt-rss!167
2025-07-27 12:22:58 -05:00
Andrew Dolgov
524bdeb419 attempt to retry night mode switch if we're offline 2025-07-27 20:07:55 +03:00
Andrew Dolgov
bb39b34d72 Merge branch 'bugfix/media-thumbnail' into 'master'
Look for media thumbnails in more places.

See merge request tt-rss/tt-rss!166
2025-07-27 07:50:28 +03:00
wn_
f62aaa307b Look for media thumbnails in more places.
https://www.rssboard.org/media-rss#optional-elements
2025-07-26 20:00:25 +00:00
Andrew Dolgov
dea3f2dcb2 Merge branch 'no-more-666' into 'master'
set sane permissions on cache/lockfiles/feed-icons instead of hardcoding a+rwx

See merge request tt-rss/tt-rss!165
2025-07-12 14:35:03 +03:00
Andrew Dolgov
d9e2cd44ce set sane permissions on cache/lockfiles/feed-icons instead of hardcoding a+rwx 2025-07-12 08:33:51 +03:00
wn
d3599707ac Merge branch 'feature/article-display-url-skip-request' into 'master'
Use existing headline info for 'Article.displayUrl()'

See merge request tt-rss/tt-rss!164
2025-07-08 10:41:13 -05:00
wn_
5b99ccf662 Use existing headline info for 'Article.displayUrl()' 2025-07-08 10:54:10 +00:00
Andrew Dolgov
da4b886f08 Merge branch 'feature/feedparser-tweaks' into 'master'
Disallow subscribing if feed content is invalid

See merge request tt-rss/tt-rss!163
2025-07-07 07:38:21 +03:00
wn_
0cd788220d Separate feed type detection from init, don't subscribe on failures.
Also some FeedParser tweaks.
2025-07-06 02:33:18 +00:00
Andrew Dolgov
46e05583a9 Merge branch 'pidlock' into 'master'
Pidlock

See merge request tt-rss/tt-rss!162
2025-07-04 14:01:37 +03:00
Andrew Dolgov
dea41c6a3d Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2025-07-04 13:59:28 +03:00
Andrew Dolgov
b989b1cd5b stop checking for pidlock in yet another place 2025-07-04 13:59:22 +03:00
Andrew Dolgov
05a90d4ce1 Merge branch 'undying' into 'master'
Undying

See merge request tt-rss/tt-rss!161
2025-07-04 13:32:57 +03:00
Andrew Dolgov
775a6066f1 Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2025-07-04 13:31:30 +03:00
Andrew Dolgov
ec0a19c5a6 replace all instances of die() with print+exit because die() returns exit code 0 2025-07-04 13:31:15 +03:00
Andrew Dolgov
9b38a57570 Merge branch 'housekeeping' into 'master'
Housekeeping

See merge request tt-rss/tt-rss!160
2025-07-04 13:24:28 +03:00
Andrew Dolgov
b0dc82dc7e only exit with nonzero exit code if there was an error 2025-07-04 13:22:06 +03:00
Andrew Dolgov
79e0d6ecc2 run housekeeping tasks on task 0 if task id is passed, stop checking for pidlock 2025-07-04 13:18:50 +03:00
Andrew Dolgov
c1542671c1 return nonzero exit code when fatal error is triggered in on a CLI SAPI 2025-07-04 13:17:25 +03:00
Andrew Dolgov
0034cd69f8 Merge branch 'housekeeping_task' into 'master'
only run housekeeping on task 0 when --feeds is invoked with pidlock and task...

See merge request tt-rss/tt-rss!159
2025-07-04 13:02:54 +03:00
Andrew Dolgov
7b4f039651 Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2025-07-04 13:00:45 +03:00
Andrew Dolgov
2e62d27b9f only run housekeeping on task 0 when --feeds is invoked with pidlock and task id, similar to daemon-loop 2025-07-04 13:00:26 +03:00
Andrew Dolgov
ee2f556265 Merge branch 'scripts' into 'master'
add separate script that invokes update.php with args, add basic info blurbs to other scripts

See merge request tt-rss/tt-rss!158
2025-07-04 09:48:55 +03:00
Andrew Dolgov
629535329d add separate script that invokes update.php with args, add basic info blurbs to other scripts 2025-07-04 09:46:46 +03:00
Andrew Dolgov
50eff08fcb Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!157
2025-07-04 07:50:05 +03:00
தமிழ்நேரம்
7c34df1946 Translated using Weblate (Tamil)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ta/
2025-07-04 00:07:24 +02:00
தமிழ்நேரம்
f99ef00b0a Translated using Weblate (Tamil)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ta/
2025-07-04 00:07:22 +02:00
தமிழ்நேரம்
7ab3eb8431 Translated using Weblate (Tamil)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ta/
2025-07-04 00:07:20 +02:00
தமிழ்நேரம்
7ba4b40a63 Translated using Weblate (Tamil)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ta/
2025-07-04 00:07:18 +02:00
Andrew Dolgov
2643440aba Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2025-07-03 21:39:25 +03:00
Andrew Dolgov
fb219848b6 add same workaround to updater startup script 2025-07-03 21:39:17 +03:00
Andrew Dolgov
f57bb8ec24 Merge branch 'sslmode' into 'master'
add workaround for what possibly is a PDO bug related to tls connections to pgsql

See merge request tt-rss/tt-rss!156
2025-07-03 21:37:43 +03:00
Andrew Dolgov
bab03ea516 add workaround for what possibly is a PDO bug related to tls connections to pgsql 2025-07-03 21:30:55 +03:00
Andrew Dolgov
2d56105c93 Merge branch 'sslmode' into 'master'
add support for PG sslmode and set it to prefer encrypted connections by default

See merge request tt-rss/tt-rss!155
2025-07-03 19:24:04 +03:00
Andrew Dolgov
4088636865 add support for PG sslmode and set it to prefer encrypted connections by default 2025-07-03 19:04:32 +03:00
wn
18f8f55ce5 Merge branch 'search-by-tags' into 'master'
allow searching by tags (prefix tag:)

See merge request tt-rss/tt-rss!154
2025-06-23 11:17:52 -05:00
Andrew Dolgov
12ef981bfb allow searching by tags (prefix tag:) 2025-06-23 18:56:47 +03:00
Andrew Dolgov
664f832aac Merge branch 'feature/subscription-fail-info' into 'master'
Show more info when subscribing fails

See merge request tt-rss/tt-rss!153
2025-06-21 07:52:36 +03:00
wn_
09c11df764 Clean up displaying subscription error info, log more detailed info to the event log. 2025-06-19 20:37:49 +00:00
wn_
692c7a8949 Remove unused subscription return code 6 2025-06-17 18:02:57 +00:00
wn_
5b0d325733 Escape error content displayed when subscribing fails (as it might contain HTML). 2025-06-17 17:59:10 +00:00
wn_
ef1f3cbcef Show some HTML content as a hover tip when the 'feed URL' returned HTML without feeds.
Also tweak the 'Feeds::_subscribe()' documentation a bit.
2025-06-17 17:52:06 +00:00
Andrew Dolgov
4e47a39c2a Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!152
2025-06-09 20:19:28 +03:00
Yurt Page
8cfb96ed54 Translated using Weblate (Russian)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ru/
2025-06-09 17:02:42 +00:00
Yurt Page
f549459a5c Translated using Weblate (Russian)
Currently translated at 83.0% (584 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ru/
2025-06-08 18:07:22 +02:00
Yurt Page
c3a0968697 Translated using Weblate (Russian)
Currently translated at 82.7% (582 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ru/
2025-06-08 15:27:18 +00:00
Andrew Dolgov
b6f0724d51 Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!151
2025-06-08 17:32:25 +03:00
Yurt Page
1e35eb9b1b Translated using Weblate (Russian)
Currently translated at 82.3% (579 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ru/
2025-06-08 15:42:05 +02:00
Andrew Dolgov
3d43eecc50 Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!150
2025-06-04 08:11:32 +03:00
Dario Di Ludovico
2da58d7ff0 Translated using Weblate (Italian)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/it/
2025-06-03 19:38:43 +00:00
Andrew Dolgov
a1dcd06b3e Merge branch 'feature/syndicated-feed-improvements' into 'master'
Generated feed tweaks

See merge request tt-rss/tt-rss!149
2025-06-03 19:47:14 +03:00
wn_
3047b294a6 Minor CSS+style tweaks in 'Handler_Public#generate_syndicated_feed()'. 2025-06-03 15:17:27 +00:00
wn_
814ab48169 Use the current timestamp for feed-level 'updated' in 'Handler_Public#generate_syndicated_feed()'.
The last article's 'updated' value was not a good indicator of when the feed updated for various reasons, so we'll just use the current timestamp to represent the dynamic nature of the content.
2025-06-03 14:10:08 +00:00
wn_
446f9dcb23 Style tweaks in 'Handler_Public#generate_syndicated_feed()' 2025-06-03 14:10:08 +00:00
wn_
8255f71c2e Fail early in 'Handler_Public#generate_syndicated_feed()' on unrecognized format. 2025-06-03 14:10:08 +00:00
Andrew Dolgov
87fb1de91d Merge branch 'feature/json-mime-type' into 'master'
Use the official JSON MIME type of 'application/json'

See merge request tt-rss/tt-rss!146
2025-06-03 16:57:15 +03:00
wn_
34c7e11d84 Use the official JSON MIME type of 'application/json'. 2025-06-03 13:53:12 +00:00
Andrew Dolgov
2095052521 Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!148
2025-06-03 16:44:44 +03:00
Patrick Ahles
30bf1e9dbe Translated using Weblate (Dutch)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/nl/
2025-06-03 15:39:21 +02:00
Edgars Andersons
ca5ef48df2 Translated using Weblate (Latvian)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/lv/
2025-06-03 15:39:21 +02:00
Patrick Ahles
93a41aa282 Translated using Weblate (French)
Currently translated at 98.7% (694 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/fr/
2025-06-03 15:39:21 +02:00
Andrew Dolgov
18b67c837c Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Chinese (Traditional Han script))

See merge request tt-rss/tt-rss!147
2025-06-03 16:39:01 +03:00
Andrew Dolgov
e63fca3dec Merge branch 'master' into weblate-integration 2025-06-03 16:36:56 +03:00
Andrew Dolgov
227d45687f php8.3 -> php8.4 2025-06-03 10:34:45 +03:00
TonyRL
408ab99450 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hant/
2025-06-01 23:37:59 +00:00
Andrew Dolgov
516fcfee98 Merge branch 'feature/alpine-3.22' into 'master'
Bump to Alpine 3.22

See merge request tt-rss/tt-rss!144
2025-05-31 19:10:16 +03:00
Andrew Dolgov
166495ebbf Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!145
2025-05-31 19:09:57 +03:00
TonyRL
558327b3b5 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 99.0% (696 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hant/
2025-05-31 18:03:58 +02:00
wn_
0c8ec170f9 Bump to Alpine 3.22 2025-05-30 18:33:43 +00:00
Andrew Dolgov
6abd7fdc4c Merge branch 'weblate-integration' into 'master'
WIP: weblate-integration

See merge request tt-rss/tt-rss!143
2025-05-29 07:54:55 +03:00
Andrew Dolgov
0a6b41a3df let's use gitlab rss feed in phpunit for now 2025-05-29 07:48:45 +03:00
Achim Schumacher
fc6ce314d6 Translated using Weblate (German)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/de/
2025-05-28 18:03:10 +00:00
Andrew Dolgov
a26d12ba3b auto create MR on push into weblate-integration 2025-05-27 13:09:29 +03:00
Andrew Dolgov
0688e6dadd cache_starred_images - make scheduled tasks intervals configurable 2025-05-27 08:27:49 +03:00
Andrew Dolgov
0787af48ff add phpunit when running without /tt-rss 2025-05-24 16:48:20 +03:00
wn
c236560b70 Merge branch 'limit-web-root-to-nginx' into 'master'
remove APP_WEB_ROOT from fpm container because it expect a different value...

See merge request tt-rss/tt-rss!142
2025-05-24 08:29:41 -05:00
Andrew Dolgov
52267d2639 remove APP_WEB_ROOT from fpm container because it expect a different value from nginx container, replace with APP_INSTALL_BASE_DIR specific to fpm container 2025-05-24 13:10:28 +03:00
Andrew Dolgov
9d4e945386 Merge branch 'feature/drop-migrate_feed_icons' into 'master'
Drop legacy feed icon storage migration and unused 'Config::ICONS_DIR'.

See merge request tt-rss/tt-rss!141
2025-05-22 22:45:02 +03:00
wn_
25d8655214 Drop legacy feed icon storage migration and unused 'Config::ICONS_DIR'. 2025-05-22 18:05:02 +00:00
Andrew Dolgov
3783d987e5 Merge branch 'unprotected-phpunit' 2025-05-22 20:53:16 +03:00
Andrew Dolgov
a2a4e831cf set some needs 2025-05-22 20:51:50 +03:00
Andrew Dolgov
3e5889394a use generic push commit job 2025-05-22 20:50:46 +03:00
Andrew Dolgov
eb2ef63c46 Merge branch 'scheduler-env' into 'master'
make default task schedules configurable

See merge request tt-rss/tt-rss!140
2025-05-22 20:37:46 +03:00
Andrew Dolgov
dba83a639c fix wrong config param being used & add a link to cron syntax we support 2025-05-22 20:36:09 +03:00
Andrew Dolgov
581fbd762d try running integration tests without harbor 2025-05-22 20:23:28 +03:00
Andrew Dolgov
b25684a5a6 make default task schedules configurable 2025-05-22 20:01:00 +03:00
Andrew Dolgov
6695a81e3a drop legacy ci file 2025-05-22 19:57:05 +03:00
Andrew Dolgov
777e60f854 drop integration test include, drop legacy CI file 2025-05-22 19:56:36 +03:00
Andrew Dolgov
9b998b3069 fix typo 2025-05-22 14:58:57 +03:00
Andrew Dolgov
fa9da54d91 shorten services 2025-05-22 14:57:23 +03:00
Andrew Dolgov
0a3bec7201 switch selenium to services 2025-05-22 14:44:54 +03:00
Andrew Dolgov
91ed6e6f62 add app_fastcgi_pass to gitlab-ci 2025-05-22 14:34:37 +03:00
Andrew Dolgov
a51d5ac438 add APP_FASTCGI_PASS 2025-05-22 14:22:40 +03:00
Andrew Dolgov
b29de3eb7c add APP_WEB_ROOT to fpm container 2025-05-22 14:05:24 +03:00
Andrew Dolgov
819fde7318 try switching phpunit off helm 2025-05-22 13:54:58 +03:00
Andrew Dolgov
f91c19b040 Merge branch 'protected/selenium-standalone' 2025-05-22 12:45:35 +03:00
Andrew Dolgov
f21c0aa8f6 Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2025-05-22 12:45:31 +03:00
Andrew Dolgov
349d4c5931 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Chinese (Simplified Han script))

See merge request tt-rss/tt-rss!139
2025-05-22 12:44:57 +03:00
Andrew Dolgov
9c66b9b326 adjust timeouts again 2025-05-22 12:42:41 +03:00
Andrew Dolgov
80647fa4e8 fix typo 2025-05-22 12:40:23 +03:00
Andrew Dolgov
cb6fe8f974 adjust timeouts 2025-05-22 12:36:20 +03:00
Ptsa Daniel
45fc831243 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hans/
2025-05-22 08:59:04 +00:00
Andrew Dolgov
99caba74a9 also add driver implicit wait for element search 2025-05-22 11:48:40 +03:00
Andrew Dolgov
33047a86ba try adding selenium page timeout 2025-05-22 11:45:08 +03:00
Andrew Dolgov
b447cda515 increase selenium timeout/attempts 2025-05-22 11:23:01 +03:00
Andrew Dolgov
2294090778 switch selenium to standalone container 2025-05-22 11:17:30 +03:00
wn
b9b5c378b0 Merge branch 'feature/filter-action-refs' into 'master'
Improve naming when working with filter actions.

See merge request tt-rss/tt-rss!138
2025-05-21 15:00:31 -05:00
wn_
df28c71641 Improve naming when working with filter actions.
Also updated some related typing and documentation.
2025-05-21 18:34:16 +00:00
Andrew Dolgov
9ffb096e87 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (German)

See merge request tt-rss/tt-rss!137
2025-05-21 18:50:07 +03:00
Achim Schumacher
5cf787064d Translated using Weblate (German)
Currently translated at 98.2% (691 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/de/
2025-05-21 11:04:07 +02:00
Andrew Dolgov
eab69f8796 Merge branch 'feature/orm-and-misc' into 'master'
Use ORM in more places, deprecate const SUBSTRING_FOR_DATE, some minor fixes

See merge request tt-rss/tt-rss!135
2025-05-19 08:06:56 +03:00
Andrew Dolgov
f4973264d3 Merge branch 'feature/cache_starred_images-media' into 'master'
Improve media detection in 'cache_starred_images'.

See merge request tt-rss/tt-rss!136
2025-05-19 07:56:27 +03:00
wn_
a164790268 Improve media detection in 'cache_starred_images'.
This was mostly a copy from 'RSSUtils::cache_media()'.
2025-05-19 02:17:41 +00:00
wn_
ce36b27a0d Fix check for no articles found in 'RSSUtils::update_rss_feed()'.
FeedParser will always return an array.
2025-05-18 16:08:13 +00:00
wn_
2749c75b72 Minor ORM usage tweak in 'RSSUtils::update_rss_feed()'. 2025-05-18 16:06:44 +00:00
wn_
adf71f09a9 Use ORM in remaining parts of 'Pref_Users'. 2025-05-18 15:46:35 +00:00
wn_
cdd48bb1fa Use ORM in 'Counters::get_feeds()' (and simplify stuff). 2025-05-18 14:51:47 +00:00
wn_
2fa54cc627 Deprecate and remove use of the 'SUBSTRING_FOR_DATE' constant.
With MySQL support removed (b154bc7a10) this constant is unnecessary.
2025-05-18 14:26:05 +00:00
wn_
0acaac7115 Remove an outdated check for 'E_DEPRECATED' existence. 2025-05-18 14:09:27 +00:00
Andrew Dolgov
d859c56636 send content-length with cached files 2025-05-18 09:06:01 +03:00
Andrew Dolgov
895e29ec26 Merge branch 'feature/remove-past-comparison-qpart' into 'master'
Get rid of 'Db::past_comparison_qpart()'.

See merge request tt-rss/tt-rss!134
2025-05-18 07:50:05 +03:00
wn_
c472f00445 Get rid of 'Db::past_comparison_qpart()'.
With MySQL support dropped this function is just an unnecessary layer of abstraction.
2025-05-17 19:08:15 +00:00
Andrew Dolgov
868c1cadad API/getFeeds: return feed last_error & update_interval 2025-05-17 12:49:41 +03:00
Andrew Dolgov
aa58ab1ce0 drop gocomics and other inactive af_comics filters - third time the charm 2025-05-17 08:03:09 +03:00
Andrew Dolgov
2e50f96901 Revert "drop gocomics and other inactive af_comics filters"
This reverts commit 5f064b4477.
2025-05-17 08:02:42 +03:00
Andrew Dolgov
d0d90e4ec8 Merge branch 'master' of gitlab.fakecake.org:git/tt-rss/tt-rss 2025-05-17 08:02:11 +03:00
Andrew Dolgov
888bf821d6 drop gocomics and other inactive af_comics filters 2025-05-17 08:01:57 +03:00
Andrew Dolgov
5f064b4477 drop gocomics and other inactive af_comics filters 2025-05-17 08:00:04 +03:00
Andrew Dolgov
a931b91099 af_comics - fix penny arcade to new markup 2025-05-17 07:57:39 +03:00
Andrew Dolgov
8aac6f2d3d return standard Content-Length header for API responses in addition to nonstandard Api-Content-Length 2025-05-16 22:02:25 +03:00
Andrew Dolgov
191da49ab1 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Galician)

See merge request tt-rss/tt-rss!133
2025-05-16 18:31:10 +03:00
josé m
ea7e8404cb Translated using Weblate (Galician)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/gl/
2025-05-09 06:01:51 +02:00
Andrew Dolgov
0affed20d2 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Latvian)

See merge request tt-rss/tt-rss!132
2025-05-06 19:42:45 +00:00
Besnik Bleta
e68069d33e Translated using Weblate (Albanian)
Currently translated at 95.4% (671 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/sq/
2025-05-06 21:41:32 +02:00
Edgars Andersons
b0402aba34 Translated using Weblate (Latvian)
Currently translated at 100.0% (703 of 703 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/lv/
2025-05-06 21:41:31 +02:00
Andrew Dolgov
b73ab44e21 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Tamil)

See merge request tt-rss/tt-rss!131
2025-05-06 19:41:15 +00:00
Andrew Dolgov
8b3bd37549 Merge branch 'pg-optimize' into 'master'
drop some pointless queries now that we can use RETURNING for inserts

See merge request tt-rss/tt-rss!130
2025-05-06 04:44:03 +00:00
Andrew Dolgov
77e5deb9dd use RETURNING syntax when creating base filter record during OPML import 2025-05-06 05:06:17 +03:00
Andrew Dolgov
e91c49b747 use RETURNING syntax when creating article record in share anything 2025-05-06 05:04:47 +03:00
Andrew Dolgov
9735ff83cc use RETURNING syntax when creating base filter record 2025-05-06 05:01:23 +03:00
Andrew Dolgov
ea6f42dc61 switch insert query for base article record to named parameters and add previously missing ts_content stuff 2025-05-05 22:08:01 +03:00
Andrew Dolgov
677cd7453f drop some pointless queries now that we can use RETURNING for inserts 2025-05-05 21:55:38 +03:00
Andrew Dolgov
070585ac5e only open PDO transaction while performing CRUD operations on article 2025-05-05 20:45:40 +03:00
Hosted Weblate
f1df08ba20 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/
2025-05-05 18:06:49 +02:00
Besnik Bleta
9d7344dfa5 Translated using Weblate (Albanian)
Currently translated at 95.8% (666 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/sq/
2025-05-05 18:06:47 +02:00
தமிழ்நேரம்
2464153ffc Translated using Weblate (Tamil)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ta/
2025-05-05 18:06:47 +02:00
Andrew Dolgov
6d11acc713 Merge branch 'plugin-cringe' into 'master'
expose scheduled tasks to plugins, switch cache_starred_images plugin to use...

See merge request tt-rss/tt-rss!129
2025-05-04 17:40:13 +00:00
Andrew Dolgov
4cc40ddaa4 scheduler - only register built-in purge_orphaned_scheduled_tasks if running as default name 2025-05-04 20:25:29 +03:00
Andrew Dolgov
5263a07f61 record last cron expression (and stub owner_uid) used by scheduled task 2025-05-04 18:06:43 +03:00
Andrew Dolgov
fc059fc0fc expose scheduled tasks to plugins, switch cache_starred_images plugin to use them instead of housekeeping hook 2025-05-04 17:50:03 +03:00
Andrew Dolgov
d4faf2d369 Merge branch 'feature/move-purge_orphaned_scheduled_tasks' into 'master'
Move registration of 'purge_orphaned_scheduled_tasks' into Scheduler.

See merge request tt-rss/tt-rss!128
2025-05-04 14:21:33 +00:00
wn_
3ee0f331cc Move registration of 'purge_orphaned_scheduled_tasks' into Scheduler. 2025-05-04 14:20:17 +00:00
Andrew Dolgov
07eb34529f Merge branch 'feature/purge-orphaned-scheduled-tasks' into 'master'
Periodically purge orphaned scheduled task records

See merge request tt-rss/tt-rss!126
2025-05-04 14:10:56 +00:00
Andrew Dolgov
8f9f06e7c0 Merge branch 'feature/scheduled-task-no-update-users' into 'master'
Move DAEMON_UPDATE_LOGIN_LIMIT-related logging to a scheduled task, exclude disabled+readonly users

See merge request tt-rss/tt-rss!127
2025-05-04 14:09:32 +00:00
wn_
853864794a Move logging users excluded from updates to a daily scheduled task, exclude disabled or readonly users. 2025-05-04 13:28:09 +00:00
wn_
868385442a Periodically purge orphaned scheduled task records. 2025-05-04 12:57:58 +00:00
Andrew Dolgov
4ce5e6e8e1 rebase translations 2025-05-04 13:45:30 +03:00
Andrew Dolgov
fecab891fc add a basic prefs panel for scheduled task records 2025-05-04 13:44:08 +03:00
Andrew Dolgov
ec12a514a2 Revert "bring back cleanup of potentially sensitive environment variables but exclude CLI SAPI to prevent updater failures"
Breaks OIDC

This reverts commit 247efe3137.
2025-05-04 13:30:07 +03:00
Andrew Dolgov
5eba9fd116 Merge branch 'weblate-integration' into 'master'
Added translation using Weblate (Albanian)

See merge request tt-rss/tt-rss!125
2025-05-04 10:22:48 +00:00
Besnik Bleta
f9c0aacf72 Added translation using Weblate (Albanian) 2025-05-04 12:21:27 +02:00
Andrew Dolgov
bb0a136944 Merge branch 'cringe-jobs' into 'master'
Cringe jobs

See merge request tt-rss/tt-rss!124
2025-05-04 09:36:27 +00:00
Andrew Dolgov
01159fa6f8 error handler - dump caught exception/fatal error to debug log if running under CLI SAPI 2025-05-03 08:18:16 +03:00
Andrew Dolgov
4cda1da5c0 adjust scheduler logging to be somewhat more alike to feed updater 2025-05-03 07:55:45 +03:00
Andrew Dolgov
997c10437e reorder housekeeping tasks by interval 2025-05-02 23:26:13 +03:00
Andrew Dolgov
55bb464cc9 update static composer autoload 2025-05-02 23:24:09 +03:00
Andrew Dolgov
d5d15072e1 move scheduled tasks to a separate class, add some try-catches, improve/shorten logging and descriptions 2025-05-02 22:51:07 +03:00
Andrew Dolgov
5256edd484 schema - spaces to tabs 2025-05-02 21:51:07 +03:00
Andrew Dolgov
bc0da8edb6 Merge branch 'marked-hook' into 'master'
add plugin hooks invoked when articles get un/marked or un/published

See merge request tt-rss/tt-rss!123
2025-05-02 18:40:47 +00:00
Andrew Dolgov
3098dc0a16 rename article mark/publish hooks 2025-05-02 21:28:37 +03:00
Andrew Dolgov
b30f8c93a0 rename article mark/publish hooks 2025-05-02 21:27:50 +03:00
Andrew Dolgov
dc6ea08ca4 add workaround for due tasks because housekeeping is not run every minute, fix last_run not updated to NOW() in the db 2025-05-02 14:03:45 +03:00
Andrew Dolgov
247efe3137 bring back cleanup of potentially sensitive environment variables but exclude CLI SAPI to prevent updater failures 2025-05-02 13:37:08 +03:00
Andrew Dolgov
aeca30cb0c drop SIMPLE_UPDATE_MODE, limit housekeeping and updates to background processes 2025-05-02 13:26:58 +03:00
Andrew Dolgov
a51c1d5176 fix tasks_run never incremented 2025-05-02 13:18:48 +03:00
Andrew Dolgov
36f60b51d7 make digest sending a hourly cron job 2025-05-02 13:17:20 +03:00
Andrew Dolgov
44b5b33f3d remove synchronous usages of _purge_orphans() 2025-05-02 10:28:35 +03:00
Andrew Dolgov
a268f52de6 record task duration in seconds 2025-05-02 10:23:30 +03:00
Andrew Dolgov
6a40940ad6 split housekeeping jobs to separate scheduled tasks on longer cooldown intervals, add table to record task execution timestamps, bump schema 2025-05-02 10:17:13 +03:00
Andrew Dolgov
f22e32a26b import cron-expression 2025-05-02 08:53:38 +03:00
Andrew Dolgov
0520ca2226 deal with published hook in _create_published_article 2025-05-02 08:26:52 +03:00
Andrew Dolgov
5f70e41118 add plugin hooks invoked when articles get un/marked or un/published 2025-05-01 22:36:33 +03:00
wn
4ae17d0f1c Merge branch 'feature/phpstan-updates' into 'master'
PHPStan update and addressing findings

See merge request tt-rss/tt-rss!122
2025-04-30 16:34:52 +00:00
Andrew Dolgov
4cb8a84df4 Merge branch 'rip-mysql' into 'master'
initial attempt to remove mysql-related stuff from tt-rss

See merge request tt-rss/tt-rss!120
2025-04-28 04:48:01 +00:00
wn_
f097c5ed97 Remove an unnecessary session UID existence check in 'UserHelper::authenticate()'.
PHPStan 'if.alwaysTrue'
2025-04-27 15:13:15 +00:00
wn_
1c9fddd757 Add a PHPStan ignore for a non-issue in 'UrlHelper::fetch()'. 2025-04-27 15:05:28 +00:00
wn_
5c2c95a897 Remove unused 'PluginHost::HOOK_FORMAT_ARTICLE_CDM'.
PHPStan 'method.notFound'.
2025-04-27 14:53:53 +00:00
wn_
ae5394f6f9 Address 'method.resultUnused' in 'api/index.php'. 2025-04-27 14:50:02 +00:00
wn_
7ad1efed3e Bump PHPStan to 2.1.13 2025-04-27 14:41:39 +00:00
wn_
0961c8bd4c Remove a PHPStan ignore related to PHP < 8 2025-04-27 14:39:06 +00:00
Andrew Dolgov
f80187e05f Merge branch 'master' into rip-mysql 2025-04-25 18:54:29 +03:00
Andrew Dolgov
0e4b8bd653 add eslint-formatter-gitlab npm dependency 2025-04-25 18:53:55 +03:00
Andrew Dolgov
be82663ac9 cache_starred_images: disable chmod() on cache directory, it doesn't seem to be necessary anymore and breaks on S3 cache implementation 2025-04-17 17:25:13 +03:00
Andrew Dolgov
75556e2f3d Merge branch 'master' into rip-mysql 2025-04-17 14:07:26 +03:00
Andrew Dolgov
d2ccdaf400 Merge branch 'fix-schema' into 'master'
* fix 148 migration for pgsql not setting default value of

See merge request tt-rss/tt-rss!121
2025-04-17 10:43:50 +00:00
Andrew Dolgov
f7199a47c2 * fix 148 migration for pgsql not setting default value of ttrss_feeds.auth_pass breaking OPML import
* replace no-op migrations for mysql with 'select 1'
2025-04-17 11:51:47 +03:00
Andrew Dolgov
8cf3059951 more type hints 2025-04-14 15:31:06 +03:00
Andrew Dolgov
945690fffc add some type hints 2025-04-14 15:28:57 +03:00
Andrew Dolgov
3c138a71a1 add deprecation notice for sql_random_function() 2025-04-14 15:27:58 +03:00
Andrew Dolgov
54e8ab7e3d update DB_TYPE deprecation notice 2025-04-14 15:24:34 +03:00
Andrew Dolgov
7e403aae92 further mysql/DB_TYPE related cleanup 2025-04-14 15:21:10 +03:00
Andrew Dolgov
b154bc7a10 initial attempt to remove mysql-related stuff from tt-rss 2025-04-14 12:59:00 +03:00
fox
60606aaa97 Merge branch 'mysql-noop' into 'master'
no-op auth_pass varchar to text migration for mysql

See merge request git/tt-rss/tt-rss!8
2025-04-14 09:39:11 +00:00
Andrew Dolgov
561d922e78 no-op auth_pass varchar to text migration for mysql 2025-04-14 12:36:13 +03:00
Andrew Dolgov
50e614499b Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Chinese (Traditional Han script))

See merge request tt-rss/tt-rss!119
2025-04-14 06:24:42 +00:00
Andrew Dolgov
f9e8911727 Revert "cleanup environment variables related to global configuration after instantiating config object"
This reverts commit e4f1480453.
2025-04-08 18:38:10 +03:00
Andrew Dolgov
44e23469a0 Merge branch 'less-leaks' into 'master'
cleanup environment variables related to global configuration after instantiating config object

See merge request tt-rss/tt-rss!118
2025-04-08 11:16:22 +00:00
Andrew Dolgov
e4f1480453 cleanup environment variables related to global configuration after instantiating config object 2025-04-08 14:09:15 +03:00
Andrew Dolgov
008c518d5d Merge branch 'session-encryption' into 'master'
add optional encryption for stored session data using Sodium library

See merge request tt-rss/tt-rss!117
2025-04-08 10:54:24 +00:00
Andrew Dolgov
17b4e98249 spaces to tabs 2025-04-08 13:52:00 +03:00
Andrew Dolgov
597971f238 we no longer directly modify schema_version in migrations 2025-04-08 09:48:44 +03:00
Andrew Dolgov
f00d9a18f8 if possible, automatically encrypt stored plaintext password for feed on update 2025-04-08 09:43:03 +03:00
Andrew Dolgov
eedc1460e5 support transparent encryption for feed passwords, bump schema to drop length limit of ttrss_feeds.auth_pass 2025-04-08 09:36:04 +03:00
Andrew Dolgov
25d3ce4ee8 drop SESSION-specific stuff and move encrypt/decrypt helpers to a separate class; add a command line flag to generate encryption keys 2025-04-08 08:55:44 +03:00
TonyRL
aa552ef057 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hant/
2025-04-08 00:41:40 +00:00
Andrew Dolgov
58677fc791 rename SODIUM_ENCRYPTION_KEY to SESSION_ENCRYPTION_KEY and move related stuff to Sessions class 2025-04-07 20:28:35 +03:00
Andrew Dolgov
026d68fc2d add optional encryption for stored session data using Sodium library 2025-04-07 20:09:31 +03:00
wn
bb2c4b3801 Merge branch 'allow-session-cookies' into 'master'
allow setting lifetime to 0 for session cookies

See merge request tt-rss/tt-rss!116
2025-04-07 06:58:33 +00:00
Andrew Dolgov
20ba3c67cc allow setting lifetime to 0 for session cookies 2025-04-07 07:14:01 +03:00
Andrew Dolgov
eaacca5792 Merge branch 'bugfix/hook-feed-basic-info-result' into 'master'
Only use valid feed basic info from plugins.

See merge request tt-rss/tt-rss!115
2025-04-04 18:02:15 +00:00
wn_
e1256b06ea Only use valid feed basic info from plugins. 2025-04-04 17:53:47 +00:00
wn
f70cd0d149 Merge branch 'bugfix/gocomics-changes-2' into 'master'
Use the correct suffix for GoComics permalinks.

See merge request tt-rss/tt-rss!114
2025-04-04 17:42:31 +00:00
wn_
7cef3a5ac2 Use the correct suffix for GoComics permalinks. 2025-04-04 17:40:24 +00:00
Andrew Dolgov
7bb6ebb356 Merge branch 'bugfix/gocomics-changes' into 'master'
Handle changes to GoComics.

See merge request tt-rss/tt-rss!113
2025-04-04 17:35:51 +00:00
wn_
c4788023a4 Handle changes to GoComics. 2025-04-04 17:33:05 +00:00
Andrew Dolgov
8df250f2eb Merge branch 'feature/search-in-hash' into 'master'
Include the search query+language as part of the URL hash

See merge request tt-rss/tt-rss!112
2025-03-31 04:57:52 +00:00
Andrew Dolgov
90942d9ccf Merge branch 'better-filter-tests' into 'master'
filter test dialog improvements:

See merge request tt-rss/tt-rss!111
2025-03-31 04:25:19 +00:00
wn_
fdf9a08197 Bump ESLint ecmaVersion to 2020.
It wasn't aware of optional chaining.
2025-03-30 18:12:55 +00:00
wn_
ca751c10e1 Include the search query+language as part of the URL hash. 2025-03-30 18:05:53 +00:00
Andrew Dolgov
2d041f7d28 use server-side localized formatting for matching rule to display as a tooltip (for now) 2025-03-30 20:41:50 +03:00
Andrew Dolgov
b4962b670d stop sending matched content twice for the tooltip, use smaller objects containing only regular expression and rule type 2025-03-30 20:21:06 +03:00
wn_
10c488e1d0 Strip '[\r\n\t]' from entry content during filter test.
This is to help get the content and regex match strings a bit closer.
2025-03-29 10:36:08 +00:00
Andrew Dolgov
043162b0eb enforce maximum length on resulting rule regexp match, highlight based on strings with stripped tags 2025-03-28 18:36:11 +03:00
Andrew Dolgov
42ea2ebec0 * fix filter test not returning anything for filters set for specific
feed ID
 * show content preview after first matched rule, not article beginning
 * show meaningful preview for filters matching on article link, tags,
   and author
2025-03-28 16:51:15 +03:00
Andrew Dolgov
8986a3e7ee add limited highlighting of filter test results based on matched rules 2025-03-28 07:59:46 +03:00
Andrew Dolgov
49766ab01f filter test dialog improvements:
- properly return results for filter rules matching specific feeds or
   categories
 - fix test results never returned for Uncategorized
 - show tooltip with specific word match and matched rule on resulting
   item hover
2025-03-27 22:22:34 +03:00
Andrew Dolgov
c1e6a5ff63 enable ta translation 2025-03-26 09:40:54 +03:00
Andrew Dolgov
4b677f10e4 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Tamil)

See merge request tt-rss/tt-rss!110
2025-03-26 06:39:42 +00:00
Andrew Dolgov
d04fc6f26d add integration runner tag 2025-03-26 09:24:41 +03:00
Edgars Andersons
ee81566f5e Translated using Weblate (Latvian)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/lv/
2025-03-21 17:11:23 +01:00
தமிழ்நேரம்
b8b475cdf6 Translated using Weblate (Tamil)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ta/
2025-03-20 20:24:42 +01:00
தமிழ்நேரம்
e01dc2e0d8 Translated using Weblate (Tamil)
Currently translated at 55.2% (384 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ta/
2025-03-19 18:49:07 +01:00
Andrew Dolgov
5dcb8db933 allow setting update interval in subcribe to feed dialog 2025-03-19 12:47:10 +03:00
Andrew Dolgov
5d69120056 Merge branch 'feature/search-to-sql-phrases' into 'master'
Support phrases in searches (PostgreSQL only)

See merge request tt-rss/tt-rss!109
2025-03-19 09:33:56 +00:00
wn_
434da183e7 Support using phrases in searches (PostgreSQL only). 2025-03-18 00:31:23 +00:00
Andrew Dolgov
6f9429f405 Merge branch 'feature/search-to-sql-tweaks' into 'master'
Some improvements in 'Feeds::_search_to_sql()'.

See merge request tt-rss/tt-rss!108
2025-03-17 17:28:44 +00:00
wn_
4053af899f Some improvements in 'Feeds::_search_to_sql()'.
* Pass in the profile so some preferences can be retrieved correctly.
* Consistently use the owner UID that gets passed in (previously some session var uses).
2025-03-15 22:37:17 +00:00
Andrew Dolgov
28cb97ddc5 Merge branch 'weblate-integration' into 'master'
Added translation using Weblate (Tamil)

See merge request tt-rss/tt-rss!107
2025-03-14 15:41:16 +00:00
Andrew Dolgov
1dc0c98c51 allow app passwords via auth_internal even if DISABLE_LOGIN_FORM is set 2025-03-14 11:57:48 +03:00
Andrew Dolgov
405cae963b Merge branch 'protected/DISABLE_LOGIN_FORM' into 'master'
add Config::DISABLE_LOGIN_FORM to allow limiting logins to SSO providers

See merge request tt-rss/tt-rss!106
2025-03-14 08:44:17 +00:00
Andrew Dolgov
d373c1f978 add Config::DISABLE_LOGIN_FORM to allow limiting logins to SSO providers 2025-03-14 11:43:25 +03:00
தமிழ்நேரம்
3563a517a6 Added translation using Weblate (Tamil) 2025-03-09 22:43:32 +01:00
Andrew Dolgov
1fc4eed6cd Merge branch 'feature/time-comparison-gen' into 'master'
Add and use 'Db::past_comparison_qpart()'.

See merge request tt-rss/tt-rss!105
2025-03-08 11:58:11 +00:00
Andrew Dolgov
9983954bf1 Merge branch 'bugfix/user-language' into 'master'
Get rid of the 'language' session variable.

See merge request tt-rss/tt-rss!104
2025-03-04 19:11:53 +00:00
wn_
89b0332d38 Add and use 'Db::now_comparison_qpart()'.
This introduces a helper to build a query part comparing a field against a past datetime (determined by '$now - $some_interval'), eliminating certain boilerplate code.
2025-03-04 18:34:35 +00:00
wn_
7e335de7b8 Get rid of the 'language' session variable.
It had issues (no profile usage, only set at login), so it's cleanest to just replace its one usage with 'Prefs::get()'.
2025-03-04 02:27:28 +00:00
Andrew Dolgov
532570ca17 Merge branch 'feature/favicon-mime-type-detection' into 'master'
Use the fileinfo module for favicon validation in 'RSSUtils::update_favicon()'.

See merge request tt-rss/tt-rss!103
2025-02-26 04:33:39 +00:00
wn_
f8198933b1 Use the fileinfo module for favicon validation in 'RSSUtils::update_favicon()'. 2025-02-25 20:23:17 +00:00
Andrew Dolgov
bfdfea88b9 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Czech)

See merge request tt-rss/tt-rss!102
2025-02-20 14:37:03 +00:00
Andrew Dolgov
85929b5d67 Merge branch 'feature/efficient-filter-testing' into 'master'
Only continue filter testing when there are likely more entries to check.

See merge request tt-rss/tt-rss!101
2025-02-20 13:13:51 +00:00
wn_
777c2b4c97 Move filter test results HTML building to the frontend. 2025-02-15 20:39:35 +00:00
wn_
e0d9ffcbc1 Only continue filter testing when there are likely more entries to check.
Prior to this, a filter test could needlessly result in up to 100 backend requests (limit 100, max_offset 10000) when the filter's associated feeds+categories have fewer than 10000 entries.
2025-02-15 16:51:25 +00:00
Patrik Coch
b85330096e Translated using Weblate (Czech)
Currently translated at 98.2% (683 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/cs/
2025-02-11 22:02:00 +01:00
Andrew Dolgov
169ff6de34 Merge branch 'feature/filter-test-cleanup' into 'master'
Clean up 'Pref_Filters::testFilterDo()'.

See merge request tt-rss/tt-rss!100
2025-02-08 17:19:20 +00:00
wn_
708563acd4 Indicate filters might have matched more than the test found.
Due to the limits set when testing a filter it's possible more results (some of which the user may be expecting to see) aren't displayed.
2025-02-08 16:55:39 +00:00
wn_
a34927d184 Clean up 'Pref_Filters::testFilterDo()'.
Use ORM, drop the '5 rule' limit, etc.
2025-02-08 16:53:12 +00:00
Andrew Dolgov
59b94a9e45 Merge branch 'feed-icons-misc' into 'master'
Feed icons cleanup

See merge request tt-rss/tt-rss!99
2025-01-27 05:39:13 +00:00
wn_
d361c1c65d Remove now-unused 'Config::ICONS_URL'. 2025-01-26 17:21:12 +00:00
wn_
7618101e33 Reduce use of legacy 'Config::ICONS_DIR'.
Also some minor cleanup in 'API#_get_config()'.
2025-01-26 17:19:12 +00:00
wn_
117d210c8a Include 'cache/feed-icons' in the default backup process.
This is worthwhile since custom icons might've been uploaded and post-restore icon fetches might take a while.
2025-01-26 16:56:34 +00:00
Andrew Dolgov
0eb4571c19 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Finnish)

See merge request tt-rss/tt-rss!98
2025-01-22 19:20:39 +00:00
Andrew Dolgov
f1b1320438 fix extra comma 2025-01-22 22:18:20 +03:00
Andrew Dolgov
f0687060d7 shorten_expanded: add simple event debounce 2025-01-22 22:14:33 +03:00
Ricky Tigg
ffe47821e0 Translated using Weblate (Finnish)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/fi/
2025-01-17 07:51:59 +01:00
Andrew Dolgov
a071edaa9d Merge branch 'feature/copy-filter' into 'master'
Add the ability to clone an existing filter.

See merge request tt-rss/tt-rss!97
2025-01-09 08:58:44 +00:00
wn_
5a93056c1c Fix setting a custom title when cloning a single filter. 2025-01-06 16:04:27 +00:00
wn_
e546e73914 Use 'clone' wording for filter duplication. 2025-01-06 15:49:40 +00:00
wn_
2eb3c150c2 Fix a return type warning in 'Pref_Filters'.
'Pref_Filters#get_details()' gets passed the ID of an existing filter, so we don't need to handle some edge case of it not existing.
2025-01-06 14:30:57 +00:00
wn_
0da9ef81bd Prompt for the new filter name when only copying one.
This also reworks 'Pref_Filters' a bit so it's easier to retrieve filter titles.
2025-01-06 14:17:38 +00:00
wn_
91496a0d24 Add the ability to copy an existing filter. 2025-01-06 00:33:18 +00:00
Andrew Dolgov
93e00d5aab Merge branch 'af-comics-dumbingofage' into 'master'
[af_comics] Add processing of dumbingofage.com images

See merge request tt-rss/tt-rss!96
2025-01-05 19:55:12 +00:00
Nathan Neulinger
ebe080dfe4 Add processing of dumbingofage.com images 2025-01-05 12:27:37 -06:00
Andrew Dolgov
d85cfb5c56 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Chinese (Simplified Han script))

See merge request tt-rss/tt-rss!95
2024-12-30 20:09:07 +00:00
Dario Di Ludovico
b076eb0005 Translated using Weblate (Italian)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/it/
2024-12-29 10:00:36 +00:00
Mikalai Daronin
83ae3ce619 Translated using Weblate (Belarusian)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/be/
2024-12-29 10:00:34 +00:00
qx100
eac64efe53 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hans/
2024-12-27 09:00:40 +01:00
Andrew Dolgov
fc89d2e633 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Greek)

See merge request tt-rss/tt-rss!94
2024-12-22 16:39:29 +00:00
cuhsy
a147e36a47 Translated using Weblate (Greek)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/el/
2024-12-22 17:00:39 +01:00
Andrew Dolgov
7b72715678 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Spanish)

See merge request tt-rss/tt-rss!93
2024-12-20 05:58:29 +00:00
Patrick Ahles
9e12c9d8e9 Translated using Weblate (Dutch)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/nl/
2024-12-17 07:00:33 +00:00
Edgars Andersons
b8dc4794c6 Translated using Weblate (Latvian)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/lv/
2024-12-17 07:00:31 +00:00
josé m
52b8e0f562 Translated using Weblate (Galician)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/gl/
2024-12-17 07:00:28 +00:00
gallegonovato
4fed050215 Translated using Weblate (Spanish)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/es/
2024-12-17 07:00:27 +00:00
Andrew Dolgov
472058e06d Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Polish)

See merge request tt-rss/tt-rss!92
2024-12-16 08:15:47 +00:00
Anarion Dunedain
b172ed039b Translated using Weblate (Polish)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/pl/
2024-12-16 07:38:51 +01:00
Andrew Dolgov
09f32efa3b Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Latvian)

See merge request tt-rss/tt-rss!91
2024-12-16 06:38:45 +00:00
Andrew Dolgov
eb47047351 maybe fix integration tests failing by always initializing apitest fields 2024-12-16 09:34:14 +03:00
Andrew Dolgov
e990a3c00f Merge branch 'feature/php-misc' into 'master'
More native typing, use some new PHP stuff

See merge request tt-rss/tt-rss!88
2024-12-16 06:29:28 +00:00
Edgars Andersons
912d1d6b1b Translated using Weblate (Latvian)
Currently translated at 79.1% (550 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/lv/
2024-12-16 05:41:15 +01:00
Andrew Dolgov
d1c0ba5944 Merge branch 'weblate-integration' into 'master'
Update translation files

See merge request tt-rss/tt-rss!90
2024-12-16 04:41:09 +00:00
Andrew Dolgov
13804c4beb Merge branch 'feature/smart_date_time_never' into 'master'
Consistently "smart display" the Unix epoch as "Never", clean up calls to `TimeHelper::make_local_datetime()`

See merge request tt-rss/tt-rss!89
2024-12-16 04:39:54 +00:00
wn_
f9b2291c28 Don't bother passing unused arguments to 'TimeHelper::make_local_datetime()'.
There's no point in passing '$long' unless '$no_smart_dt' is set to 'true'.
2024-12-15 18:39:03 +00:00
wn_
119c7f13dc Consistently handle the 'smart' display of default/never dates. 2024-12-15 17:47:27 +00:00
wn_
6f8f1b30d5 minor PHPDoc cleanup in PluginHost 2024-12-15 16:41:45 +00:00
wn_
cfbbb9d714 Clean up some virtual feed stuff in PluginHost.
Among other things, this makes 'PluginHost->add_feed()' return false if the feed was not added.
2024-12-15 16:09:42 +00:00
Edgars Andersons
d47e8957de Translated using Weblate (Latvian)
Currently translated at 67.0% (466 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/lv/
2024-12-15 17:01:08 +01:00
Dario Di Ludovico
662164334f Translated using Weblate (Italian)
Currently translated at 100.0% (695 of 695 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/it/
2024-12-15 17:01:06 +01:00
wn_
62a6191f04 Deal with FeedEnclosure property accessed before initialization. 2024-12-15 13:43:49 +00:00
wn_
18b17cbc83 Revert some stuff based upon feedback 2024-12-15 13:39:54 +00:00
wn_
57dd754e07 Use a native DNF type for 'PluginHost->get_feed_handler()'. 2024-12-14 12:26:59 +00:00
wn_
a1bd6cea1b Use native typing in more places and clean up 'FeedEnclosure' a bit. 2024-12-14 12:26:59 +00:00
wn_
333bab90a7 Remove use of 'ReturnTypeWillChange'.
'ReturnTypeWillChange' was a workaround needed until we reached PHP 8.0, which introduced union types and allowed alignment with 'SessionHandlerInterface'.
2024-12-14 12:26:59 +00:00
wn_
1742fb65c5 Use the spread operator instead of 'array_merge' in more places.
PHP 8.1 introduced support for merging string-key arrays (last array with a wins).
2024-12-14 12:26:59 +00:00
Hosted Weblate
688ab74eb6 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/
2024-12-14 10:18:06 +01:00
Andrew Dolgov
d5b1258d29 rebase translations 2024-12-14 12:17:51 +03:00
Andrew Dolgov
899d7d258c pref feed tree - switch to flexbox for row layout, remove floating param 2024-12-14 12:17:22 +03:00
Andrew Dolgov
e473d8ecc5 show amount of stored article in prefs feed tree 2024-12-14 11:37:58 +03:00
Andrew Dolgov
0caf502b79 fix correct font-family not applying for multiple select element 2024-12-13 08:04:29 +03:00
Andrew Dolgov
13f32bb62e Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Estonian)

See merge request tt-rss/tt-rss!87
2024-12-13 05:03:51 +00:00
Priit Jõerüüt
e858fc45fd Translated using Weblate (Estonian)
Currently translated at 0.2% (2 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/et/
2024-12-12 13:00:32 +00:00
Andrew Dolgov
0ef2dd7175 Merge branch 'feature/alpine_3.21-and-php_8.4' into 'master'
Bump to Alpine 3.21 and PHP 8.4, raise the minimum to PHP 8.2

See merge request tt-rss/tt-rss!85
2024-12-10 05:22:37 +00:00
wn_
3860435cba Add the 'ca-certificates' package to provide 'update-ca-certificates'.
By default only the 'ca-certificates-bundle' package is installed.

* https://pkgs.alpinelinux.org/contents?file=&path=&name=ca-certificates&branch=v3.21&repo=main&arch=x86_64
* https://pkgs.alpinelinux.org/contents?file=&path=&name=ca-certificates-bundle&branch=v3.21&repo=main&arch=x86_64
2024-12-09 19:29:22 +00:00
wn_
65bb110770 minor package installation formatting tweak 2024-12-09 19:21:56 +00:00
wn_
133e8ea2a4 Centralize the PHP version suffix used for packages and paths. 2024-12-09 18:54:02 +00:00
wn_
f6a8facfd4 Bump 'spomky-labs/otphp' to 11.3.x.
This is mainly for PHP 8.4 compatibility.
2024-12-09 17:58:28 +00:00
wn_
cd2c10f9f7 Bump the minimum required PHP version to 8.2.0.
Discussion: https://gitlab.tt-rss.org/tt-rss/tt-rss/-/merge_requests/85
2024-12-09 17:53:23 +00:00
Andrew Dolgov
f15db7b961 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Spanish)

See merge request tt-rss/tt-rss!86
2024-12-09 14:19:46 +00:00
Ricky Tigg
50259a1bc1 Translated using Weblate (Finnish)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/fi/
2024-12-06 10:00:33 +01:00
wn_
096f803f2f Bump to Alpine 3.21 and PHP 8.4.
* https://alpinelinux.org/posts/Alpine-3.21.0-released.html
* https://www.php.net/releases/8.4/en.php
2024-12-05 15:59:30 +00:00
Anarion Dunedain
791c9f287c Translated using Weblate (Polish)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/pl/
2024-12-03 11:00:30 +01:00
Edgars Andersons
efb16289c6 Translated using Weblate (Latvian)
Currently translated at 58.2% (404 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/lv/
2024-11-30 23:00:20 +01:00
Patrick Ahles
30ab6a6046 Translated using Weblate (Dutch)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/nl/
2024-11-29 22:00:25 +01:00
josé m
d2f4e123be Translated using Weblate (Galician)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/gl/
2024-11-29 22:00:24 +01:00
gallegonovato
9340f3fd88 Translated using Weblate (Spanish)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/es/
2024-11-29 22:00:23 +01:00
Andrew Dolgov
4dfa7a666a Merge branch 'weblate-integration' into 'master'
Update translation files

See merge request tt-rss/tt-rss!84
2024-11-28 16:03:16 +00:00
Hosted Weblate
04c0f2a9b9 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/
2024-11-28 06:15:51 +01:00
Andrew Dolgov
ca61521f42 rebase translations 2024-11-28 08:15:31 +03:00
Andrew Dolgov
3dd71e41a1 also deal with The Oatmeal posts, not just comics 2024-11-26 20:15:34 +03:00
Andrew Dolgov
b045da0e5e add af_comics filter for The Oatmeal 2024-11-26 20:08:44 +03:00
Andrew Dolgov
8c42b3a3bf Merge branch 'bugfix/feedparser-rdf-type' into 'master'
Fix RDF feed support in FeedParser.

See merge request tt-rss/tt-rss!83
2024-11-26 16:51:43 +00:00
wn_
b5777b5a7c Add+use FeedParser::FEED_UNKNOWN 2024-11-26 16:50:51 +00:00
wn_
d85bdcfd78 Fix RDF feed support in FeedParser. 2024-11-26 16:45:03 +00:00
Andrew Dolgov
aaeabbc961 API: allow catchup for search results (bumps api level to 22) 2024-11-25 19:47:28 +03:00
Andrew Dolgov
103bafd90a Merge branch 'feature/php8-str-funcs' into 'master'
Use PHP 8 'str_' functions.

See merge request tt-rss/tt-rss!81
2024-11-24 14:06:57 +00:00
wn_
667528d5b9 Use PHP 8 'str_' functions.
A few more characters in some places, but helps with readability.
2024-11-24 13:59:29 +00:00
Andrew Dolgov
53fee911e6 Merge branch 'feature/php8-match' into 'master'
Use match expressions in some places.

See merge request tt-rss/tt-rss!82
2024-11-24 13:55:00 +00:00
wn_
9b0baf9b32 Use match expressions in some places. 2024-11-24 13:45:26 +00:00
Andrew Dolgov
43e8864ead allow nullable password in auto_create_user for backwards compatibility 2024-11-24 12:55:59 +03:00
Andrew Dolgov
b089d67e26 Merge branch 'getfiltertree-eldritch-horrors' into 'master'
getfiltertree: switch to ORM and simplify code

See merge request tt-rss/tt-rss!79
2024-11-24 06:37:31 +00:00
Andrew Dolgov
7892b41234 Merge branch 'feature/php8-union-types' into 'master'
Use native union types in most places.

See merge request tt-rss/tt-rss!80
2024-11-24 06:36:58 +00:00
Andrew Dolgov
aad69b7ca6 make filter search properly case-insensitive 2024-11-24 09:24:20 +03:00
wn_
abcd0e8ba2 Use native union types in most places. 2024-11-23 17:43:24 +00:00
Andrew Dolgov
1ce1aee40f simplify resulting tree root object 2024-11-23 18:15:18 +03:00
Andrew Dolgov
6bc9097b0f getfiltertree: switch to ORM and simplify code 2024-11-23 18:13:09 +03:00
Andrew Dolgov
d4636716fb collapse actions summary list in filter tree if 'toggle rule display' is disabled, remove label-specific icon display, simplify markup 2024-11-23 17:43:20 +03:00
Andrew Dolgov
81ccfed4b4 Merge branch 'filter-fixes' into 'master'
get filter action descriptions on pref_filters class init instead of doing it...

See merge request tt-rss/tt-rss!78
2024-11-23 14:10:56 +00:00
Andrew Dolgov
4dc0e8cd29 fix text-muted being set to default text foreground color in light mode, adjust styling of filter actions list 2024-11-23 17:08:33 +03:00
Andrew Dolgov
417065b8f5 show cumulative score adjustment (if any) and up to 3 actions total in filter tree 2024-11-23 16:54:22 +03:00
Andrew Dolgov
c2d0c5a5c9 update usage of deprecated function 2024-11-23 15:59:53 +03:00
Andrew Dolgov
648024eb2e bump minimum required php version to 8.0 & remove some deprecated code 2024-11-23 15:54:40 +03:00
Andrew Dolgov
7be6484fee use select_many() without _expr 2024-11-23 15:49:50 +03:00
Andrew Dolgov
1fc19d6e50 adjust indent 2024-11-23 11:54:44 +03:00
Andrew Dolgov
64d9a77fde switch filters _get_rules_list() to ORM 2024-11-23 11:54:24 +03:00
Andrew Dolgov
c23b76eb72 pass resulting action description through gettext 2024-11-23 11:21:18 +03:00
Andrew Dolgov
ce5a96cb30 set type hint for $action_descriptions 2024-11-23 11:14:39 +03:00
Andrew Dolgov
bbc28e626a fix _get_name failing on filters without any rules because of wrong type of JOIN 2024-11-23 11:11:19 +03:00
Andrew Dolgov
31ca090c63 get filter action descriptions on pref_filters class init instead of doing it all the time, use ORM in _get_action_name 2024-11-23 11:07:50 +03:00
Andrew Dolgov
f28df34fec fix action params not hiding in edit action dialog if param-less action was initially selected 2024-11-23 10:47:41 +03:00
Andrew Dolgov
987936f57a pref_filters - refactor _get_name to use ORM, show cumulative score in tree filter description 2024-11-23 10:26:12 +03:00
Andrew Dolgov
9c1fb45d73 Merge branch 'feature/php-qrcode-5.0.x' into 'master'
Bump 'chillerlan/php-qrcode' to 5.0.x.

See merge request tt-rss/tt-rss!77
2024-11-21 17:36:54 +00:00
wn_
64a36970d6 Bump 'chillerlan/php-qrcode' to 5.0.x.
* Maintains PHP `7.4` compatibility and adds PHP `8.4` compatibility
  * The `4.4.x` branch does the same, but I didn't see any reason not to go to `5.0.x`.
* https://github.com/chillerlan/php-qrcode/releases
2024-11-21 17:34:32 +00:00
Andrew Dolgov
1e14fc0fd9 Merge branch 'wn-master-patch-27216' into 'master'
Fix array key warning in 'Feeds::_get_headlines()'.

See merge request tt-rss/tt-rss!76
2024-11-20 01:50:14 +00:00
wn
486c92240a Fix array key warning in 'Feeds::_get_headlines()'. 2024-11-19 23:13:10 +00:00
Andrew Dolgov
4bdd926a1c Merge branch 'feature/replace-get_pref-and-set_pref' into 'master'
Eliminate use of deprecated 'get_pref()' and 'set_pref()'.

See merge request tt-rss/tt-rss!75
2024-11-19 16:46:48 +00:00
wn_
154abc61a0 Eliminate use of deprecated 'get_pref()' and 'set_pref()'. 2024-11-18 21:59:45 +00:00
Andrew Dolgov
394d606fe9 Merge branch 'feature/phpstan-2.0.x' into 'master'
PHPStan 2.0.x

See merge request tt-rss/tt-rss!74
2024-11-13 18:38:32 +00:00
wn_
859ce4d7f6 Deal with an error showing up in Gitlab PHPStan runs 2024-11-13 02:11:58 +00:00
wn_
2dda9f9ab5 Minor @var cleanup in Counters 2024-11-12 04:14:51 +00:00
wn_
76b9cd8274 Make the 'requireOnce.fileNotFound' PHPStan error ignore more specific. 2024-11-12 03:59:52 +00:00
wn_
5a200755b8 Move 'IVirtualFeed' checks into 'PluginHost::get_feed_handler()'. 2024-11-12 03:49:58 +00:00
wn_
dca2ae60a1 Remove some PHPStan ignores and make others rule-specific. 2024-11-12 03:38:45 +00:00
wn_
a784305cc7 Address PHPStan findings as of 2.0.1 2024-11-12 03:15:53 +00:00
wn_
e4c57769e0 Upgrade PHPStan to 2.0.1 2024-11-12 01:26:43 +00:00
Andrew Dolgov
6273e26ea4 Merge branch 'feature/search-improvements' into 'master'
Search improvements

See merge request tt-rss/tt-rss!72
2024-11-11 04:36:47 +00:00
Andrew Dolgov
42ebdb027e fix get_self_url() misbehaving in plugins/ 2024-11-04 13:59:02 +03:00
Andrew Dolgov
d8718b7574 Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2024-11-01 11:02:09 +03:00
Andrew Dolgov
b12b2afcc1 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Arabic (Saudi Arabia))

See merge request tt-rss/tt-rss!73
2024-11-01 08:01:47 +00:00
Andrew Dolgov
eb841b761c af_comics - fix for Danby Draws and maybe other similar wodpress-based sites 2024-11-01 10:59:57 +03:00
Younes
710e42cfd8 Translated using Weblate (Arabic (Saudi Arabia))
Currently translated at 59.2% (411 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ar_SA/
2024-10-24 14:15:51 +00:00
Andrew Dolgov
68da94cc36 move sql patch file stuff after all initialization tasks 2024-10-24 11:35:05 +03:00
Andrew Dolgov
dba6a39d2a drop RESTORE_SCHEMA, add simple mechanism to apply SQL patch files after migrations 2024-10-24 11:08:20 +03:00
Andrew Dolgov
9f0eb4d7fc support TTRSS_DB_PORT in container startup scripts 2024-10-21 08:59:55 +03:00
wn_
842e9af4cf Feeds::_search_to_sql(): updates for clarity and SQL quoting. 2024-10-20 17:57:32 +00:00
wn_
142ca20cb0 Fix keyword searches with a quoted string value.
Before this change curly braces wrapped the keyword and its value, making the pair get treated as leftover words.

Also make the search query modification and CSV parsing a bit clearer with some comments and minor refactoring.
2024-10-20 16:08:20 +00:00
Andrew Dolgov
5ea96397c0 properly support search queries in viewfeed debugger, improve some debugging messages and output 2024-10-15 19:58:52 +03:00
Andrew Dolgov
a58a0cd888 launch gulp from local node_modules 2024-10-15 19:36:26 +03:00
Andrew Dolgov
468f464b48 Merge branch 'feature/guzzle-7.9.2' into 'master'
Update Guzzle to 7.9.2

See merge request tt-rss/tt-rss!70
2024-10-10 16:01:54 +00:00
Andrew Dolgov
7fafad2ac2 Merge branch 'weblate-integration' into 'master'
Added translation using Weblate (Estonian)

See merge request tt-rss/tt-rss!71
2024-10-10 15:57:04 +00:00
wn_
124c4e2542 Update Guzzle to 7.9.2
https://github.com/guzzle/guzzle/releases
2024-10-07 20:22:01 +00:00
Priit Jõerüüt
946337fd5d Added translation using Weblate (Estonian) 2024-10-04 23:46:37 +02:00
Andrew Dolgov
df489df309 Merge branch 'drop-opentelemetry' into 'master'
drop php-http/guzzle7-adapter

See merge request tt-rss/tt-rss!69
2024-10-01 18:50:08 +00:00
Andrew Dolgov
2ea888fdc6 drop php-http/guzzle7-adapter 2024-10-01 18:10:28 +03:00
Andrew Dolgov
df33ddaea1 Merge branch 'drop-opentelemetry' into 'master'
drop opentelemetry

See merge request tt-rss/tt-rss!68
2024-10-01 14:54:29 +00:00
Andrew Dolgov
7e0f5f295c drop OPENTELEMETRY_ global config entries 2024-10-01 17:22:37 +03:00
Andrew Dolgov
e1bac5d855 composer dump-autoload --optimize 2024-10-01 17:21:28 +03:00
Andrew Dolgov
884fd92f13 drop opentelemetry 2024-10-01 16:00:34 +03:00
Andrew Dolgov
8fcc68baf5 Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2024-09-25 12:33:13 +03:00
Andrew Dolgov
b3489fa2a7 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Spanish)

See merge request tt-rss/tt-rss!67
2024-09-25 09:32:58 +00:00
Andrew Dolgov
4ec871eee4 disable absolute redirects & redirect port in phpdoc image 2024-09-25 12:31:14 +03:00
gallegonovato
e11bc1cd1c Translated using Weblate (Spanish)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/es/
2024-09-23 13:15:37 +02:00
Andrew Dolgov
8a8ce06965 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Galician)

See merge request tt-rss/tt-rss!65
2024-09-21 14:21:55 +00:00
Andrew Dolgov
5de5361703 phpdoc - switch to nginx unprivileged 2024-09-21 17:18:28 +03:00
josé m
2636d4dace Translated using Weblate (Galician)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/gl/
2024-09-19 09:02:10 +02:00
Andrew Dolgov
0163884ef6 add another test for self url, split regex into two parts - one for plugins, one for everything else 2024-09-15 07:17:48 +03:00
Andrew Dolgov
9c44af354f Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Greek)

See merge request tt-rss/tt-rss!63
2024-09-14 08:00:17 +00:00
Andrew Dolgov
78c903cb7f fix Config::get_self_url() invoked from plugin context, better deal with multiple trailing slashes in URL, update phpunit image path 2024-09-14 10:53:40 +03:00
Nikolas
0f0aa30746 Translated using Weblate (Greek)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/el/
2024-09-13 22:08:03 +02:00
Nikolas
ca75ac8a56 Translated using Weblate (Spanish)
Currently translated at 99.8% (693 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/es/
2024-09-13 22:06:37 +02:00
Nikolas
c457b08070 Translated using Weblate (Greek)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/el/
2024-09-13 22:06:36 +02:00
Andrew Dolgov
3619ee97c5 don't try to publish phpdoc image without creds 2024-09-12 21:44:01 +03:00
Andrew Dolgov
ec0b4306a9 Revert "Update .gitlab-ci.yml file"
This reverts commit 0d3c5049c1.
2024-09-12 21:11:59 +03:00
Andrew Dolgov
0d3c5049c1 Update .gitlab-ci.yml file 2024-09-12 18:11:19 +00:00
Andrew Dolgov
dff0a8ccbb Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Spanish)

See merge request tt-rss/tt-rss!62
2024-09-12 04:13:44 +00:00
Andrew Dolgov
16dd42c3d6 Merge branch 'path_info_update' into 'master'
Enable PATH_INFO for plugins to use

See merge request tt-rss/tt-rss!61
2024-09-12 04:10:15 +00:00
Eric Pierce
d91b7c339d spacing fix 2024-09-11 21:22:40 +00:00
Eric Pierce
48db6beca7 Update nginx.conf to move the currently unused PATH_INFO configuration into a separate location rule for plugins to leverage 2024-09-11 21:13:50 +00:00
gallegonovato
913ba654bd Translated using Weblate (Spanish)
Currently translated at 99.8% (693 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/es/
2024-09-09 22:09:23 +02:00
gallegonovato
d5b4e5ff72 Translated using Weblate (Spanish)
Currently translated at 89.3% (620 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/es/
2024-09-06 12:09:21 +02:00
Andrew Dolgov
1fb202b258 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Galician)

See merge request tt-rss/tt-rss!59
2024-08-31 07:44:08 +00:00
josé m
11acb07def Translated using Weblate (Galician)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/gl/
2024-08-31 06:09:11 +02:00
Andrew Dolgov
d5d47b8e50 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Spanish)

See merge request tt-rss/tt-rss!58
2024-08-29 15:25:11 +00:00
Andrew Dolgov
1570fb4f15 Merge branch 'feature/search-title-link' into 'master'
Make a search query 'feed title' a link to get back to the search modal.

See merge request tt-rss/tt-rss!57
2024-08-29 15:24:58 +00:00
Andrew Dolgov
bce2722b12 Merge branch 'feature/php84-csv-escape-deprecation' into 'master'
Set 'str_getcsv' escape param to empty string to avoid PHP 8.4 deprecation message.

See merge request tt-rss/tt-rss!56
2024-08-29 15:22:51 +00:00
wn_
89489f622d Make a search query 'feed title' a link to get back to the search modal. 2024-08-26 19:55:34 +00:00
gallegonovato
fd19bfd0dc Translated using Weblate (Spanish)
Currently translated at 81.7% (567 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/es/
2024-08-26 18:09:19 +02:00
gallegonovato
e07f8dc7db Translated using Weblate (Spanish)
Currently translated at 78.5% (545 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/es/
2024-08-24 18:09:18 +00:00
wn_
207d3dd48d Set 'str_getcsv' escape param to empty string to avoid PHP 8.4 deprecation message.
The escape param is in the process of being eliminated, with PHP 8.4 deprecating passing anything but an empty string as its value.
For some reason they're leaving the default value (a backslash) as-is, meaning the default will cause a deprecation message.
This commit avoids that by setting the escape param to an empty string (see references below).

* https://wiki.php.net/rfc/deprecations_php_8_4#deprecate_proprietary_csv_escaping_mechanism
* https://www.php.net/manual/en/function.str-getcsv.php
* https://old.reddit.com/r/PHP/comments/1eyum8c/new_deprecation_notices_in_php84_with_csv/
* https://nyamsprod.com/blog/csv-and-php8-4/
2024-08-24 14:22:12 +00:00
Andrew Dolgov
a10f45cf67 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Galician)

See merge request tt-rss/tt-rss!55
2024-08-22 06:53:09 +00:00
Andrew Dolgov
8423183e2f Merge branch 'feature/php-version-check-and-link-stuff' into 'master'
Remove extra PHP version checks, fix some links

See merge request tt-rss/tt-rss!54
2024-08-22 06:50:27 +00:00
wn_
d17f90b96f Fix some broken links and make minor wording tweaks. 2024-08-21 17:59:35 +00:00
wn_
94c43fe979 Remove extra PHP version checks.
Since PHP 7.4.0 is the current minimum, we should be fine just using the check in 'Config::sanity_check()'.
2024-08-21 17:58:18 +00:00
josé m
d94e21c884 Translated using Weblate (Galician)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/gl/
2024-08-21 11:44:50 +02:00
Andrew Dolgov
b8cbb167d4 enforce lowercase usernames while keeping backwards-compatibility for authentication 2024-08-16 14:28:20 +03:00
Andrew Dolgov
99e444d1d2 fix build: directive missing in dev compose file for updater 2024-08-16 14:27:59 +03:00
Andrew Dolgov
b626a61461 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (German)

See merge request tt-rss/tt-rss!53
2024-08-14 12:10:22 +00:00
Andrew Dolgov
8cceaa873a Merge branch 'feature/cleanup-dashboard-feed-code' into 'master'
Remove unused 'dashboard feed' code.

See merge request tt-rss/tt-rss!52
2024-08-14 12:08:32 +00:00
wn_
d167d5803f Remove unused 'dashboard feed' code.
Displaying auxiliary info when there's nothing to load is being handled in 'Feeds::_format_headlines_list()'.
2024-08-14 01:05:11 +00:00
tarte
10a431b8b5 Translated using Weblate (German)
Currently translated at 99.8% (693 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/de/
2024-08-09 00:09:33 +02:00
Andrew Dolgov
3496402686 Merge branch 'feature/isset-to-null-coalescing-op' into 'master'
Replace basic 'isset()' cases with the null coalescing operator.

See merge request tt-rss/tt-rss!51
2024-08-04 16:10:12 +00:00
wn_
9dd4102c7f Replace basic 'isset()' cases with the null coalescing operator. 2024-08-04 15:42:11 +00:00
Andrew Dolgov
6b521b5ed1 Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2024-08-03 23:11:40 +03:00
Andrew Dolgov
3c53a0b5dc Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Spanish)

See merge request tt-rss/tt-rss!50
2024-08-03 15:05:43 +00:00
gallegonovato
b98ed32e95 Translated using Weblate (Spanish)
Currently translated at 78.2% (543 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/es/
2024-07-30 15:09:23 +02:00
Andrew Dolgov
29cc61d1e3 set proper stages for phpdoc jobs 2024-07-21 11:07:06 +03:00
Andrew Dolgov
19c0909337 copy docs to subpath 2024-07-21 10:54:49 +03:00
Andrew Dolgov
0caf1ef850 set phpdoc image name 2024-07-21 10:51:26 +03:00
Andrew Dolgov
1346767dfb add phpdoc dockerfile 2024-07-21 10:46:27 +03:00
Andrew Dolgov
8f448685be exp - use phpdoc container 2024-07-21 10:45:01 +03:00
Andrew Dolgov
9d37158d8c Merge branch 'bugfix/optional-alpine-mirror' into 'master'
Fix breakage when 'ALPINE_MIRROR' is not provided

See merge request tt-rss/tt-rss!49
2024-07-19 18:00:02 +00:00
wn_
fd9eabdfdb Fix breakage when 'ALPINE_MIRROR' is not provided.
Related to 7f3129d4f3
2024-07-19 17:57:19 +00:00
Andrew Dolgov
7f3129d4f3 support optional mirror for alpine 2024-07-19 09:45:22 +03:00
Andrew Dolgov
71c0d319aa Merge branch 'bugfix/add-sessions-to-autoloader' into 'master'
Add the 'Sessions' class to the autoloader.

See merge request tt-rss/tt-rss!48
2024-07-18 16:47:11 +00:00
wn_
6e715bc154 Add the 'Sessions' class to the autoloader.
Generated using 'composer dump-autoload --optimize'.
2024-07-17 16:43:20 +00:00
Andrew Dolgov
8f20c1a7ca Merge branch 'feature/php84-session_set_save_handler' into 'master'
Switch to the non-deprecated form of 'session_set_save_handler'.

See merge request tt-rss/tt-rss!44
2024-07-17 06:56:55 +00:00
Andrew Dolgov
dc0d1b93d2 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Chinese (Traditional))

See merge request tt-rss/tt-rss!46
2024-07-17 06:52:47 +00:00
Andrew Dolgov
5ca874aa9b update CONTRIBUTING 2024-07-17 09:50:29 +03:00
ssantos
c3013d04e5 Translated using Weblate (Portuguese (Portugal))
Currently translated at 77.2% (536 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/pt_PT/
2024-07-14 12:09:11 +00:00
TonyRL
6ed766c7fe Translated using Weblate (Chinese (Traditional))
Currently translated at 99.5% (691 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hant/
2024-07-12 17:09:16 +02:00
wn_
44257b8016 Move side effects out of the 'Sessions' constructor. 2024-07-12 01:18:53 +00:00
wn_
c7cc3c92ba Add and use the 'Sessions' class. 2024-07-11 12:16:00 +00:00
Andrew Dolgov
8fe28cfcb8 retry selenium tests several times 2024-07-11 10:01:55 +03:00
Andrew Dolgov
a758d287ff Merge branch 'bugfix/decoded-srcset-result' into 'master'
Don't reuse the '$matches' array in 'RSSUtils::decode_srcset()'.

See merge request tt-rss/tt-rss!43
2024-07-10 19:59:38 +00:00
Andrew Dolgov
f2db34a707 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Chinese (Traditional))

See merge request tt-rss/tt-rss!45
2024-07-10 19:52:47 +00:00
oopzzozzo
b560ef7d05 Translated using Weblate (Chinese (Traditional))
Currently translated at 99.5% (691 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hant/
2024-07-09 16:09:26 +02:00
wn_
acf3748621 Switch to the non-deprecated form of 'session_set_save_handler'.
As of PHP 8.4 the form with more than 2 arguments is deprecated.

This also does some initial work to make the functions behave closer to what SessionHandlerInterface describes.

* https://php.watch/versions/8.4/session_set_save_handler-alt-signature-deprecated
* https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures
* https://www.php.net/manual/en/class.sessionhandlerinterface.php
2024-07-05 16:39:16 +00:00
wn_
0ce4ae3ece Don't reuse the '$matches' array in 'RSSUtils::decode_srcset()'.
This causes the size of the array to be incorrectly doubled due to the original regex match items being combined with the custom items (i.e. the ones with just 'url' and 'size' keys).

Also rework 'RSSUtils::encode_srcset()' a bit so it looks similar to 'RSSUtils::decode_srcset()'.
2024-07-05 03:17:11 +00:00
Andrew Dolgov
59cf218144 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Polish)

See merge request tt-rss/tt-rss!42
2024-07-02 09:58:44 +00:00
Anarion Dunedain
42051f3bad Translated using Weblate (Polish)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/pl/
2024-06-30 09:44:19 +02:00
Andrew Dolgov
d5eec31d6b Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Turkish)

See merge request tt-rss/tt-rss!41
2024-06-25 19:17:42 +00:00
Oğuz Ersen
c416fdff07 Translated using Weblate (Turkish)
Currently translated at 80.6% (560 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/tr/
2024-06-24 21:09:34 +02:00
Andrew Dolgov
44906b8268 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (German)

See merge request tt-rss/tt-rss!40
2024-06-22 06:32:38 +00:00
TonyRL
1463d28cb6 Translated using Weblate (Chinese (Traditional))
Currently translated at 97.9% (680 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hant/
2024-06-21 20:10:03 +02:00
Patrick Ahles
4a399915a5 Translated using Weblate (Dutch)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/nl/
2024-06-21 20:10:02 +02:00
Patrick Ahles
4f581e53bf Translated using Weblate (French)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/fr/
2024-06-21 20:10:00 +02:00
Patrick Ahles
855da3f87f Translated using Weblate (German)
Currently translated at 99.8% (693 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/de/
2024-06-21 20:09:59 +02:00
Andrew Dolgov
2df5c79f68 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Galician)

See merge request tt-rss/tt-rss!39
2024-06-20 14:02:24 +00:00
Chih-Hsuan Yen
6baea4cbcf Translated using Weblate (Chinese (Traditional))
Currently translated at 97.1% (674 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hant/
2024-06-20 16:00:50 +02:00
josé m
2b5ec065a7 Translated using Weblate (Galician)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/gl/
2024-06-20 16:00:50 +02:00
Andrew Dolgov
342d62da66 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Galician)

See merge request tt-rss/tt-rss!38
2024-06-20 14:00:41 +00:00
Andrew Dolgov
49ef5a929b add some time-related debugging output for feeds and users 2024-06-19 09:12:11 +03:00
josé m
ea06455b3f Translated using Weblate (Galician)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/gl/
2024-06-19 04:09:46 +00:00
Andrew Dolgov
df8403be08 set DAEMON_FEED_LIMIT to 50 by default and use it consistently between forking daemon and any other update methods 2024-06-18 21:54:22 +03:00
Andrew Dolgov
db3e67b3fe * pass arbitrary CLI arguments to update daemon via updater.sh entrypoint
* add configurable log level for update daemon (DAEMON_LOG_LEVEL)
 * when daemon log level is set to LOG_EXTENDED (2) log queries for feed
   update selection
2024-06-18 21:47:05 +03:00
Andrew Dolgov
d7a6f74ae5 add gitlab CR publish jobs 2024-06-17 17:09:29 +03:00
Andrew Dolgov
88806115a6 wip split image build/push 2024-06-17 15:43:13 +03:00
Andrew Dolgov
b0bcddd6da Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Italian)

See merge request tt-rss/tt-rss!37
2024-06-16 18:53:36 +00:00
Ptsa Daniel
00b0f90238 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hans/
2024-06-16 17:09:33 +00:00
Dario Di Ludovico
907c380c12 Translated using Weblate (Italian)
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/it/
2024-06-16 17:09:33 +00:00
Andrew Dolgov
611910e181 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Chinese (Simplified))

See merge request tt-rss/tt-rss!36
2024-06-15 15:45:46 +00:00
Ptsa Daniel
e3acfb90b0 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (694 of 694 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hans/
2024-06-15 17:44:21 +02:00
Andrew Dolgov
9993e90da6 Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Chinese (Simplified))

See merge request tt-rss/tt-rss!35
2024-06-15 13:51:27 +00:00
Hosted Weblate
83f05888c7 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/
2024-06-15 15:45:10 +02:00
Ptsa Daniel
09230bcded Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (682 of 683 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hans/
2024-06-15 15:45:08 +02:00
Andrew Dolgov
fa18ecf58f update messages.pot 2024-06-15 14:03:57 +03:00
Andrew Dolgov
9d50d95fa3 fix untranslated strings 2024-06-15 14:03:33 +03:00
Andrew Dolgov
b3c085972b Merge branch 'weblate-integration' into 'master'
Translated using Weblate (Galician)

See merge request tt-rss/tt-rss!34
2024-06-15 06:34:00 +00:00
josé m
99ea9d2c25 Translated using Weblate (Galician)
Currently translated at 100.0% (701 of 701 strings)

Translation: TinyTinyRSS/webui
Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/gl/
2024-06-15 08:29:25 +02:00
Andrew Dolgov
cc51487f08 Merge branch 'master' into weblate-integration 2024-06-14 13:47:14 +03:00
Andrew Dolgov
9e6684e927 don't ask rsync to set ownership when copying to persistent volume in a container 2024-06-12 09:47:21 +03:00
Andrew Dolgov
bcbbd53a8f Merge branch 'feature/alpine-3.20' into 'master'
Bump to Alpine 3.20.

See merge request tt-rss/tt-rss!33
2024-06-07 15:20:26 +00:00
wn_
4137e7f473 Bump to Alpine 3.20.
This is to keep receiving updates for things from the Alpine community repo (e.g. PHP).
2024-05-22 14:55:24 +00:00
Andrew Dolgov
8b037e81d8 Merge branch 'feature/debug-feeds-with-update-errors' into 'master'
Add option to debug feeds in 'Feeds with update errors' dialog.

See merge request tt-rss/tt-rss!32
2024-05-16 16:07:47 +00:00
wn_
5e7713a658 Add option to debug feeds in 'Feeds with update errors' dialog.
Also, prevent opening that dialog from modifying the URL.
2024-05-16 15:48:21 +00:00
Andrew Dolgov
e2e161dece Merge branch 'feature/support-feed-link-in-body' into 'master'
Check 'head' and 'body' when searching HTML for feed links.

See merge request tt-rss/tt-rss!30
2024-05-09 06:44:16 +00:00
wn_
7a5ea2a2b9 Check 'head' and 'body' when searching HTML for feed links.
YouTube, for some reason, puts theirs in 'body'.
2024-05-09 00:00:51 +00:00
Andrew Dolgov
d832907125 Merge branch 'feature/composer-autoload-functions' into 'master'
Move common 'include/functions.php' require into Composer autoloader.

See merge request tt-rss/tt-rss!28
2024-04-21 16:16:50 +00:00
wn_
16b89cc293 Move 'include/functions.php' require into Composer autoloader.
Autoloader regenerated with 'composer dump-autoload --optimize'.
2024-04-20 16:25:33 +00:00
Andrew Dolgov
b1e3d660e4 Merge branch 'feature/php84-explicit-nullable-params-2' into 'master'
Switch 2 more implicitly nullable params to explicitly nullable.

See merge request tt-rss/tt-rss!27
2024-04-16 14:29:02 +00:00
wn_
ac55a15c84 Switch 2 more implicitly nullable params to explicitly nullable.
Missed in https://gitlab.tt-rss.org/tt-rss/tt-rss/-/merge_requests/26 .

https://wiki.php.net/rfc/deprecate-implicitly-nullable-types
2024-04-16 14:23:07 +00:00
Andrew Dolgov
ae5e7568f5 force-set absolute path for local cache if CACHE_DIR config value is relative 2024-04-13 00:39:37 +03:00
Andrew Dolgov
435c321caa Merge branch 'feature/php84-explicit-nullable-params' into 'master'
Make implicitly nullable parameters explicitly nullable.

See merge request tt-rss/tt-rss!26
2024-03-26 17:11:56 +00:00
wn_
de00a09538 Make implicit nullable parameters explicitly nullable.
This is to address a deprecation planned for PHP 8.4.

https://wiki.php.net/rfc/deprecate-implicitly-nullable-types
2024-03-26 16:43:39 +00:00
Andrew Dolgov
fea3089bde Merge branch 'feature/prefs-search-submit' into 'master'
Support doing a prefs page search via Enter.

See merge request tt-rss/tt-rss!25
2024-03-23 15:26:43 +00:00
wn_
9743f0efcd Support doing a prefs page search via Enter. 2024-03-23 15:19:58 +00:00
Andrew Dolgov
66db7dc0ac drop es_LA, move es_ES to es 2024-03-06 15:24:16 +03:00
Andrew Dolgov
536085c764 Update CONTRIBUTING.md 2024-03-05 18:53:06 +00:00
Andrew Dolgov
81f3139992 add HOOK_VALIDATE_SESSION 2024-02-21 22:13:23 +03:00
Andrew Dolgov
fc95c988cf use update job template 2024-02-18 19:43:35 +03:00
Andrew Dolgov
6a51afcfcb fix demo values file, drop .helm 2024-02-17 12:43:47 +03:00
Andrew Dolgov
ea732aa55f add demo job back 2024-02-17 12:37:34 +03:00
Andrew Dolgov
d19729157b more prod helm stuff 2024-02-17 12:31:49 +03:00
Andrew Dolgov
7602038264 unquote commit hash & add files to commit 2024-02-17 12:20:33 +03:00
Andrew Dolgov
e85b27a61c maybe fix placeholder git name 2024-02-17 12:15:38 +03:00
Andrew Dolgov
924496e148 set placeholder git email/name 2024-02-17 12:09:34 +03:00
Andrew Dolgov
78ce18b0e8 set image 2024-02-17 12:07:29 +03:00
Andrew Dolgov
dad3646876 wip: update-prod 2024-02-17 12:02:07 +03:00
Andrew Dolgov
244146fac7 Merge branch 'bugfix/backup-script-perms' into 'master'
Ensure correct permissions on the backup script.

See merge request tt-rss/tt-rss!24
2024-02-15 04:51:57 +00:00
wn_
05da9ca742 Ensure correct permissions on the backup script. 2024-02-14 22:56:10 +00:00
Andrew Dolgov
373a2fec3a Merge branch 'bugfix/hook-fetch-feed-auth' into 'master'
Fix passing auth credentials to plugins for HOOK_FETCH_FEED.

See merge request tt-rss/tt-rss!23
2024-02-06 15:09:55 +00:00
wn_
1dbc4dc475 Fix passing auth credentials to plugins for HOOK_FETCH_FEED. 2024-02-06 12:50:26 +00:00
Andrew Dolgov
528fad51fb implement above changes for 3 panel view, add basic tooltip 2024-02-02 07:01:53 +03:00
Andrew Dolgov
a5b19e5ff5 make headline elements with feed title lead to originating site while RSS icon elements lead to the feed within tt-rss UI 2024-02-02 06:53:55 +03:00
Andrew Dolgov
a56e935deb fix unfunctional rss icon in grouped-by-feed heading 2024-02-02 06:46:05 +03:00
Andrew Dolgov
3d70fb21f7 use OCI for integration tests 2024-02-01 18:56:27 +03:00
Andrew Dolgov
19563f23af fix missing include 2024-02-01 18:54:47 +03:00
Andrew Dolgov
a994db2b5a switch to template helm ci 2024-02-01 18:53:31 +03:00
Andrew Dolgov
c7e36e1a0c Merge branch 'feature/rssutils-use-feedenclosure' into 'master'
Use FeedEnclosure throughout RSSUtils.

See merge request tt-rss/tt-rss!22
2024-01-28 11:34:03 +00:00
wn_
21aebd8ff1 Use FeedEnclosure throughout RSSUtils. 2024-01-20 17:37:10 +00:00
Andrew Dolgov
a86df7eac8 Merge branch 'bugfix/web-nginx-healthcheck' into 'master'
Use APP_BASE in the web-nginx health check URL.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

See merge request tt-rss/tt-rss!9
2023-08-06 16:19:16 +00:00
Andrew Dolgov
adba0aa8d2 add .dockerignore 2023-08-06 19:02:38 +03:00
Andrew Dolgov
ba6a912abd use non-deprecated variant of get_schema_version() 2023-08-03 07:24:48 +03:00
Andrew Dolgov
bd95325f8d phpstan: exclude psr/log 2023-08-03 07:24:29 +03:00
Andrew Dolgov
1d788eddf8 * logger: add optional HTML output
* feed debugger: add checkbox to dump feed XML
2023-08-02 09:10:05 +03:00
Andrew Dolgov
3d255d861c use nginx envsubst to make tt-rss root configurable 2023-07-28 21:23:57 +03:00
Andrew Dolgov
dc25a9cf68 disable app passwords in the UI if auth_internal is not loaded 2023-06-14 20:19:18 +03:00
Andrew Dolgov
a9d8fd8bdc move af_redditimgur to a separate repo 2023-06-10 08:40:23 +03:00
Andrew Dolgov
d43d6f7dff keep two sets of content-insert size cookies for wide & normal mode 2023-06-01 08:31:10 +03:00
Andrew Dolgov
a28d9582e8 public/getUnread: fix PHP8 warning if fresh optional parameter is not given 2023-05-28 21:27:33 +03:00
Andrew Dolgov
718af52a1b Merge branch 'fix-version' into 'master'
Fix version string for unsupported installations

See merge request tt-rss/tt-rss!6
2023-05-27 06:24:35 +00:00
Chih-Hsuan Yen
d26309b1e5 Fix version string for unsupported installations
For unsupported installations, $version['commit'] may not be defined,
leading to a warning:

E_WARNING (2)

Undefined array key "commit"
1. classes/config.php(316): ttrss_error_handler(Undefined array key "commit", classes/config.php)
2. prefs.php(173): get_version_html()
2023-05-27 10:58:11 +08:00
Andrew Dolgov
3468317bd3 Merge branch 'master' into 'master'
Update API

See merge request tt-rss/tt-rss!5
2023-05-21 05:21:49 +00:00
defkev
af3e9eb4a0 Forgot delimiter 2023-05-21 06:16:36 +02:00
defkev
5bfd18d3e6 Update API
Add site_url property for 9e169dc3aa7c7e30c11d7d3d1bbc4bc66fa39760
2023-05-21 04:55:25 +02:00
Andrew Dolgov
a4543de3ac Merge branch 'feature/php8.2' into 'master'
Bump to Alpine 3.18, PHP 8.2

See merge request tt-rss/tt-rss!4
2023-05-15 14:56:45 +00:00
wn_
b1187f0db6 Bump to Alpine 3.18, PHP 8.2 2023-05-15 14:08:11 +00:00
Andrew Dolgov
11946f0148 Update CONTRIBUTING.md 2023-05-09 08:09:20 +00:00
Andrew Dolgov
3de09b44f2 _order_to_override_query: fix custom sort plugins overriding each other 2023-05-06 14:07:10 +03:00
Andrew Dolgov
0578bf8025 add Headlines.default_move_on_expand tunable 2023-04-13 20:15:23 +03:00
Andrew Dolgov
1e90feef0e fix 881f8805bd behaving improperly 2023-04-13 06:20:04 +03:00
Andrew Dolgov
c314bd8742 add APCu & opcache 2023-04-10 20:28:02 +03:00
Andrew Dolgov
103fdd5e60 long year -> short year 2023-04-10 20:11:26 +03:00
Andrew Dolgov
7a54154d45 we don't need BUILD_TIMESTAMP either 2023-04-10 20:06:52 +03:00
Andrew Dolgov
27bd226f2b move branch to version tooltip 2023-04-10 20:04:38 +03:00
Andrew Dolgov
15c9dbe270 use short sha CI envvar for version.json compatibility 2023-04-10 19:55:20 +03:00
Andrew Dolgov
2420feb91f no more pointless txt files for version 2023-04-10 19:53:49 +03:00
Andrew Dolgov
8ccea1712e a bit more diskcache tracing 2023-04-10 18:23:26 +03:00
Andrew Dolgov
f3f2e7d043 don't try to pass array to span tags 2023-04-10 18:06:14 +03:00
Andrew Dolgov
6920c44587 better static version 2023-04-10 18:05:13 +03:00
Andrew Dolgov
566d164053 add CI_COMMIT_TIMESTAMP to version-static file 2023-04-10 17:49:42 +03:00
Andrew Dolgov
06aabdf60b create version_static.txt on build 2023-04-10 17:43:39 +03:00
Andrew Dolgov
4e17fac8b7 fix typo 2023-04-10 07:24:01 +03:00
Andrew Dolgov
7dc83961bd make jaeger service name configurable 2023-04-10 07:18:36 +03:00
Andrew Dolgov
b0fc248c05 add tracing to UrlHelper 2023-04-09 23:26:51 +03:00
Andrew Dolgov
6ec01203a1 add php81-sockets 2023-04-09 23:19:21 +03:00
Andrew Dolgov
44137342a6 a bit more tracing 2023-04-09 22:36:37 +03:00
Andrew Dolgov
fd5e0f98c4 even more tracing 2023-04-09 22:31:42 +03:00
Andrew Dolgov
e18295a364 more tracing 2023-04-09 22:15:16 +03:00
Andrew Dolgov
d68c736e47 Tracer: rework options to tags 2023-04-09 21:37:17 +03:00
Andrew Dolgov
6418157ccf add icatchall 2023-04-09 21:30:04 +03:00
Andrew Dolgov
d7c070b22b make phpstan happy 2023-04-09 21:29:16 +03:00
Andrew Dolgov
c1b3c99667 some tracer class fixes / unhardcode jaeger IP 2023-04-09 21:20:35 +03:00
Andrew Dolgov
8f3646a9c9 exp: jaeger tracing 2023-04-09 20:50:33 +03:00
Andrew Dolgov
a37eab2610 Revert "docker: point xaccel plugin url to gitlab"
This reverts commit c4edc93182.
2023-04-09 15:27:03 +03:00
Andrew Dolgov
431b8778ed bump dojo to 1.16.5 2023-04-09 11:14:46 +03:00
Andrew Dolgov
c4edc93182 docker: point xaccel plugin url to gitlab 2023-04-09 10:37:41 +03:00
Andrew Dolgov
028bb846d9 Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2023-04-09 10:31:20 +03:00
Andrew Dolgov
31ef788e02 add window.requestIdleCallback polyfill for safari 2023-04-09 10:29:42 +03:00
fox
ae27d51197 Merge branch 'master' into 'master'
Update config.php: $ttrss_version in private function _get_version() replaced by $this->version

See merge request tt-rss/tt-rss!2
2023-04-06 19:58:03 +00:00
Jan Pieter Kunst
a60c833ee4 Update config.php: $ttrss_version in private function _get_version() replaced by $this->version 2023-04-06 18:36:36 +00:00
Andrew Dolgov
0fcc2d1d66 CI: phpstan: check for PHPDOC_DEPLOY_SSH_KEY 2023-04-06 20:27:04 +03:00
Andrew Dolgov
152545b3c9 phpstan task: analyze only modified files 2023-04-06 20:25:48 +03:00
Andrew Dolgov
881f8805bd filters: allow matching on tags if there are no tags 2023-04-06 20:25:24 +03:00
Andrew Dolgov
53bd56894d make phpstan happy 2023-04-06 15:55:00 +03:00
Andrew Dolgov
af5c64045b add simple autocompleter for tags 2023-04-06 15:51:09 +03:00
Andrew Dolgov
0fcc715069 add branch targets 2023-04-05 08:51:35 +03:00
Andrew Dolgov
fd684dae29 remove .gitea workflows 2023-04-03 17:48:42 +03:00
Andrew Dolgov
58450486a0 dockerfile cleanup 2023-04-03 17:47:03 +03:00
Andrew Dolgov
4a60652be9 fix eslint task extending .phpunit 2023-04-02 12:32:28 +03:00
Andrew Dolgov
521ac622e4 Merge branch 'master' of gitlab.tt-rss.org:tt-rss/tt-rss 2023-04-02 12:31:00 +03:00
Andrew Dolgov
f0e0f7d5f5 use common CI template 2023-04-02 12:30:33 +03:00
fox
c2d8ba5973 Merge branch 'bugfix/editfeed-focus' into 'master'
Make the edit feed dialog's 'remove icon' button a regular button.

See merge request tt-rss/tt-rss!1
2023-04-01 19:13:13 +00:00
wn_
807f914338 Make edit feed dialog's 'remove icon' button a regular button.
Previously it was of type 'submit', and hitting Enter anywhere in the modal triggered its action (rather than the other submit button for saving).
2023-04-01 14:22:33 +00:00
Andrew Dolgov
fd98d6d117 add branch tasks 2023-04-01 12:52:45 +03:00
Andrew Dolgov
92185933f9 more pipeline tweaks 2023-04-01 12:36:37 +03:00
Andrew Dolgov
4fd0d13b64 phpstan: depend on phpunit 2023-04-01 12:03:59 +03:00
Andrew Dolgov
e2a02f1f4b add some manual job triggers 2023-04-01 12:03:20 +03:00
Andrew Dolgov
00d2cb0f93 remove docs stage 2023-04-01 09:51:44 +03:00
Andrew Dolgov
2b01786124 CI: set some job filters 2023-04-01 09:50:32 +03:00
Andrew Dolgov
088dd049b5 fix phpdoc stage 2023-04-01 09:35:54 +03:00
Andrew Dolgov
28a911a2a8 add phpdoc job 2023-04-01 09:34:59 +03:00
Andrew Dolgov
066b9a29d7 WIP: gitlab-ci 2023-04-01 08:58:54 +03:00
fox
269c0f53b8 Merge pull request 'isLoggedIn adds a message to the system log when it returns false, fix for php8+' (#106) from rodneys_mission/tt-rss:master into master
Reviewed-on: https://gitea.tt-rss.org/tt-rss/tt-rss/pulls/106
2023-04-01 06:36:57 +03:00
Rodney Stromlund
80bd26b3b1 isLoggedIn adds a message to the system log when it returns false, fix for php8+, removed empty test for bool conversion. 2023-03-31 20:26:36 -05:00
Andrew Dolgov
3ca3e54251 lint job: remove useless cache hash 2023-03-29 17:19:57 +03:00
Rodney Stromlund
7795c415ab isLoggedIn adds a message to the system log when it returns false, fix for php8+ 2023-03-29 08:20:52 -05:00
Andrew Dolgov
cf656125b9 good idea to checkout code before trying to document it 2023-03-28 18:02:26 +03:00
Andrew Dolgov
740d249aba phpdoc job: verbosity 2023-03-28 18:00:00 +03:00
Andrew Dolgov
dfffa20e78 simplify pipeline setup 2023-03-28 17:57:17 +03:00
Andrew Dolgov
d25440f051 fix ssh for phpdoc pipeline 2023-03-28 17:55:10 +03:00
Andrew Dolgov
da620ef5d8 add phpdoc job 2023-03-28 17:52:59 +03:00
Andrew Dolgov
ecedc51162 add docker build cache 2023-03-27 09:34:02 +03:00
Andrew Dolgov
33a3672eab use action for image meta 2023-03-26 20:48:07 +03:00
Andrew Dolgov
a0b52caced we don't need to do npm install either 2023-03-26 19:19:11 +03:00
Andrew Dolgov
37014d78fd remove node_modules after eslint pass 2023-03-26 19:18:03 +03:00
Andrew Dolgov
0c42d99a93 shorten pipeline 2023-03-26 17:25:11 +03:00
Andrew Dolgov
c41c1bd353 update FROM links for harbor 2023-03-26 10:28:33 +03:00
Andrew Dolgov
d40b6c655f enable multiarch 2023-03-26 00:09:24 +03:00
Andrew Dolgov
6cf4ebbabd bring back nginx_xaccel 2023-03-25 23:05:06 +03:00
Andrew Dolgov
723323d746 add gitea-CI docker builder 2023-03-25 22:41:47 +03:00
Andrew Dolgov
d305532bce commit package-lock.json, add eslint task 2023-03-25 18:10:26 +03:00
Andrew Dolgov
903b9dbb8b CI: add on PR 2023-03-25 11:26:53 +03:00
Andrew Dolgov
8e490af01c jenkins lint -> gitea workflow 2023-03-25 10:58:02 +03:00
Andrew Dolgov
ca86a0e239 add lint workflow 2023-03-25 09:56:59 +03:00
Andrew Dolgov
563675de09 * auth_internal OTP form: fix double-urlencode
* post-login redirect: handle ?return in a less idiotic fashion
2023-03-23 20:05:03 +03:00
Andrew Dolgov
0f9488ace0 combined mode: prevent left part of footer pushing right part out of viewport 2023-03-20 18:33:29 +03:00
fox
cddbf5bf5a Merge pull request 'Replace special feed and category numbers with constants.' (#104) from wn/tt-rss:feature/special-feed-and-cat-consts into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/104
2023-03-07 20:13:10 +03:00
wn_
b14a8a76eb Change 'FEED_NOTHING' to 'FEED_DASHBOARD'. 2023-03-07 15:45:07 +00:00
fox
c4026b408b Merge pull request 'Bump PHPStan to 1.10.3, address some new warnings' (#103) from wn/tt-rss:feature/bump-phpstan into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/103
2023-03-07 16:50:13 +03:00
wn_
c923fda8c9 Also use friendly names for special feed+cat IDs in the frontend. 2023-03-05 20:06:48 +00:00
wn_
fe08299ec4 Replace special feed and category numbers with constants. 2023-03-05 19:16:48 +00:00
wn_
029cb8f442 Revert 7ed4fa4c1d and use @var instead.
PHPStan had trouble recognizing that ['items'] might have elements added.
2023-03-05 16:29:51 +00:00
wn_
42b287e964 Remove unused 'Prefs::_delete()'.
Related to dabb85c7dd.
2023-03-05 15:30:12 +00:00
wn_
dabb85c7dd Address PHPStan warning about unused private method 'Prefs::_delete()'. 2023-03-05 14:20:19 +00:00
wn_
7ed4fa4c1d Tweak to appease PHPStan in 'Pref_Feeds::_makefeedtree()'.
PHPStan flagged the 'count()' below this with: Comparison operation '>' between 0 and 0 is always false.
2023-03-05 14:20:19 +00:00
wn_
c4b16ca608 Address PHPStan 'right side always true' in 'PluginHost::lookup_command()'.
Since 'PluginHost::add_command()' is currently the only way to add to this private array, and it always sets an array, this is reasonably safe.
2023-03-05 14:20:19 +00:00
wn_
c48dd6a3c4 Address PHPStan 'right side always true' in FeedItem_RSS. 2023-03-05 14:20:19 +00:00
wn_
d34d01fd72 Bump PHPStan from 1.8.2 to 1.10.3
* https://phpstan.org/blog/phpstan-1-9-0-with-phpdoc-asserts-list-type
* https://phpstan.org/blog/phpstan-1-10-comes-with-lie-detector
2023-03-05 14:20:19 +00:00
Andrew Dolgov
d210ae50ad API:
- sharedToPublished: add optional sanitize parameter (defaults to true)
   if disabled, allows inserting HTML into shared article content;
 - clean() already invokes strip_tags() so it's pointless to do both;
2023-03-05 08:07:55 +03:00
Andrew Dolgov
b7a6c948d0 tags display: instead of limiting to 5 tags, limit by container width % 2023-03-01 21:41:52 +03:00
Andrew Dolgov
04c2fa9f15 Merge branch 'master' of git.tt-rss.org:tt-rss/tt-rss 2023-02-25 19:31:07 +03:00
Andrew Dolgov
4d825fa6a6 require PHP to have support for flock() 2023-02-25 19:30:41 +03:00
fox
33c20d42df Merge pull request 'add override links to utility views' (#102) from levito/tt-rss:add-override-links-to-util-views into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/102
2023-02-24 07:10:28 +03:00
Veit Lehmann
aa2b770e30 add override links to utility views
This enables `local-overrides.css` and `local-overrides.js` for all utility views, for example to add polyfills, enable responsive styling or to adjust styles globally.
2023-02-24 00:46:40 +01:00
Andrew Dolgov
a2af3a6bb4 API: add getFeedIcon endpoint, bump version 2023-02-23 18:00:18 +03:00
fox
fcfcb69a2e Merge pull request 'Handle fetch issues in 'RSSUtils::update_basic_info'.' (#101) from wn/tt-rss:bugfix/handle-failed-basic-info-fetch into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/101
2023-02-19 12:03:49 +03:00
wn_
fd55e492c3 Handle fetch issues in 'RSSUtils::update_basic_info'. 2023-02-17 12:10:51 +00:00
fox
7bbe71b818 Merge pull request 'Fix calculating average color for *.ico' (#100) from yan12125/tt-rss:fix-ico-avg-color into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/100
2023-02-13 20:43:04 +03:00
Chih-Hsuan Yen
40afee5d12 Fix calculating average color for *.ico
Currently colorPalette() always fails for *.ico due to a logic error.
It's a regression from 8f749fe61b.
2023-02-10 18:20:42 +08:00
fox
0cd4abe4eb Merge pull request 'Attempt calculating custom favicon avg color, only try calculating once' (#99) from wn/tt-rss:feature/custom-favicon-detect-color into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/99
2023-02-04 10:56:21 +03:00
wn_
1646aba944 Minor tweak to favicon avg color debug log message. 2023-02-03 01:30:35 +00:00
wn_
b28d339bf2 Don't set 'favicon_avg_color' on feed obj unless it's valid. 2023-02-03 01:28:24 +00:00
wn_
f484988967 Fix logging favicon-related bools in 'RSSUtils::update_rss_feed()'. 2023-02-03 01:17:53 +00:00
wn_
380624a484 Persist failure to detect favicon average color.
Previously, an empty string returned by '\Colors\calculate_avg_color()' would be set as the 'favicon_avg_color' value, resulting in always reattempting average color calculation.
2023-02-03 01:02:42 +00:00
wn_
f0f7a5f958 Ensure custom favicon color detection happens. 2023-02-03 00:45:04 +00:00
Andrew Dolgov
c30b24d09f deal with type errors in batch feed editor properly, un-deprecate PDO wrapper functions and document them for posterity 2022-12-30 19:51:34 +03:00
Andrew Dolgov
5c0a5da88c batch feed editor: silence some more php8.1 undefined field warnings 2022-12-30 19:10:41 +03:00
Andrew Dolgov
a16acd65fc batch feed editor:
- fix some field changes not applying because of DB type errors
 - rework to use bound vars instead of sql query concatenation
deprecate: checkbox_to_sql_bool(), bool_to_sql_bool()
2022-12-30 19:07:15 +03:00
Andrew Dolgov
2be8d58509 CI: use local registry php image 2022-12-30 09:48:34 +03:00
Andrew Dolgov
9c0ead3640 show full commit timestamp with version information in prefs footer 2022-12-30 09:46:01 +03:00
Andrew Dolgov
4c94139113 Merge branch 'weblate-integration' 2022-12-28 12:35:07 +03:00
Eike
b64ec219b9 Translated using Weblate (German)
Currently translated at 99.2% (696 of 701 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2022-12-28 09:33:28 +00:00
fox
22f8a22748 Merge pull request 'Fix getting active feeds with errors.' (#97) from wn/tt-rss:bugfix/feeds-with-errors-default-interval into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/97
2022-12-25 08:42:44 +03:00
wn_
371af1a39c Fix getting active feeds with errors.
fb4bc2615e incorrectly excluded feeds using the default update interval.  This change ignores the unlikely scenario where someone has the default update interval set to 'disabled'.
2022-12-24 21:22:16 +00:00
fox
fb4bc2615e Merge pull request 'Only count updating (i.e. enabled) feeds when determining active feeds with errors.' (#96) from wn/tt-rss:feature/only-warn-for-updating-feeds into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/96
2022-12-22 00:13:04 +03:00
wn_
46e2635869 Only count updating (i.e. enabled) feeds when determining active feeds with errors.
This excludes feeds that had errors and currently have updating disabled (e.g. disabled due to the site being down for a while, getting compromised, etc.).

Disabled / non-updating feeds' error states are still visible when viewed in the feed tree.
2022-12-21 21:05:59 +00:00
fox
423b26afc5 Merge pull request 'Only touch on send for expirable cache files.' (#95) from wn/tt-rss:bugfix/local-cache-feed-icons-ts into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/95
2022-12-20 08:22:21 +03:00
wn_
8b129626cd Only touch on send for expirable cache files.
With d373b7b452 feed icon modification times get used for cache-busting, but 'Cache_Local' updates that
value on each send.  This change makes it so the modification time only gets updated on files in expirable caches, keeping the value
consistent between sends for files in non-expiring caches.

Also, marking 'Cache_Local::send_local_file()' private since it's unique to that adapter.
2022-12-20 02:16:47 +00:00
Andrew Dolgov
c6d21b3196 make phpstan happy 2022-12-19 21:42:34 +03:00
Andrew Dolgov
d373b7b452 * bring back cache-busting for feed icons based on timestamp
* DiskCache: use singleton pattern to create less cache object instances
 * DiskCache: implement ETag
2022-12-19 21:36:50 +03:00
Andrew Dolgov
20d6aaa9ab limit tree expando white color to prefs 2022-12-19 21:19:28 +03:00
Andrew Dolgov
8ea537123d move af_readability out of master tree 2022-12-13 20:08:43 +03:00
fox
313f12ae93 Merge pull request 'Bump af_readability 'html5-php' dependency to latest.' (#94) from wn/tt-rss:feature/bump-af_readability-masterminds-html5 into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/94
2022-12-13 19:45:50 +03:00
wn_
457553eeac Add af_readability 'html5-php' Jenkinsfile.
Got missed when updating that dependency.
2022-12-12 22:38:18 +00:00
wn_
0317828847 Bump af_readability 'html5-php' dependency to latest.
This is to add a couple more 'ReturnTypeWillChange' ( https://dev.tt-rss.org/main/html5-php/pulls/1 ).

Composer 2.4.4 (latest release) also updated some of its files.
2022-12-12 22:31:14 +00:00
Andrew Dolgov
72e64bdb78 phpstan: exclude tests in lib/ 2022-12-11 22:06:49 +03:00
Andrew Dolgov
fa9c614ff1 Merge branch 'master' of git.tt-rss.org:tt-rss/tt-rss 2022-12-02 07:35:11 +03:00
Andrew Dolgov
824addbc9d fix cleanup_feed_icons unlinking nonexistant files, limit it to actual feed icons 2022-12-02 07:34:51 +03:00
fox
292ca86665 Merge pull request 'Consistently get the self URL.' (#92) from wn/tt-rss:bugfix/config-self-url-consistency into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/92
2022-11-28 20:43:10 +03:00
wn_
a355221e7f Consistently get the self URL.
This ensures all uses of the self URL get the same normalized/sanitized value.
2022-11-28 17:40:42 +00:00
Andrew Dolgov
94c49399cc get_self_url: strip all trailing slashes 2022-11-28 19:24:12 +03:00
Andrew Dolgov
52180c9f8f DiskCache: enforce basename() on filenames passed to cache adapter 2022-11-26 14:15:45 +03:00
Andrew Dolgov
3212c51ce8 migrate favicons directly to new cache 2022-11-24 23:43:46 +03:00
Andrew Dolgov
a30b9bb649 rework favicon storage to use DiskCache 2022-11-24 23:31:33 +03:00
Andrew Dolgov
be6bc72a74 DiskCache: tweak how expiration is invoked 2022-11-24 18:49:36 +03:00
Andrew Dolgov
3180b35807 deprecate DiskCache->touch() 2022-11-24 08:16:56 +03:00
Andrew Dolgov
9732d8fc9f update_rss_feed: use DiskCache to store feed data 2022-11-23 22:09:04 +03:00
Andrew Dolgov
10a1dd35e3 * split local cache implementation into a separate class
* allow custom implementations provided by plugins
2022-11-23 21:18:40 +03:00
Andrew Dolgov
30c04adfa6 Merge branch 'weblate-integration' 2022-11-23 17:27:42 +03:00
xosé m
cb2f1ac2d9 Translated using Weblate (Galician)
Currently translated at 100.0% (701 of 701 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/gl/
2022-11-23 11:05:12 +00:00
fox
9a0dcdd6cc Merge pull request 'Address upcoming string interpolation deprecation (PHP 8.2)' (#90) from wn/tt-rss:feature/php82-str-intrp-deprecation into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/90
2022-11-12 20:24:52 +03:00
wn_
d376cd6142 Address upcoming string interpolation deprecation.
https://wiki.php.net/rfc/deprecate_dollar_brace_string_interpolation
2022-11-12 16:20:59 +00:00
Andrew Dolgov
602e868425 Merge branch 'master' of git.tt-rss.org:tt-rss/tt-rss 2022-10-15 13:46:02 +03:00
Andrew Dolgov
f56a049641 fix some PHP8 warnings generated while dragging feed tree items around 2022-10-15 13:44:02 +03:00
Andrew Dolgov
b702761941 fix tree expando being invisible on selected tree nodes 2022-10-15 13:43:42 +03:00
fox
7eb711eefa Merge pull request 'Return true in custom error handler for proper suppression' (#89) from mechnich/tt-rss:fix-error-handling into master
Reviewed-on: https://dev.tt-rss.org/tt-rss/tt-rss/pulls/89
2022-10-11 18:57:09 +03:00
jmechnich
017bf9777f Return true in custom error handler for proper suppression 2022-10-10 14:56:28 +02:00
Andrew Dolgov
bbb47b5d62 Jenkins: set discarder 2022-10-02 19:20:10 +03:00
Andrew Dolgov
68dee45782 remove docker-on-docker hacks from Jenkinsfile 2022-10-01 22:22:06 +03:00
Andrew Dolgov
c654f02a53 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2022-10-01 19:01:33 +03:00
Andrew Dolgov
6cbdbd261e add Jenkinsfile to enable separate CI for pull requests 2022-10-01 19:01:22 +03:00
fox
54942b7664 Merge pull request 'Fix handling of suppressed errors' (#85) from mechnich/tt-rss:master into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/85
2022-10-01 12:31:22 +03:00
fox
0ac143a29b Merge pull request 'Fix PHP8 strtime warning if argument is null (addendum)' (#86) from mechnich/tt-rss:more-strtotime-fixes into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/86
2022-10-01 12:28:15 +03:00
jmechnich
560caf8377 Fix PHP8 strtime warning if argument is null (addendum) 2022-10-01 11:05:12 +02:00
jmechnich
504d0afd35 Fix handling of suppressed errors 2022-10-01 10:28:36 +02:00
Andrew Dolgov
42bc1620b8 make phpstan happy 2022-09-29 20:02:59 +03:00
fox
3545d3ba83 Update 'CONTRIBUTING.md' 2022-09-29 17:23:40 +03:00
fox
9437b45569 Merge pull request 'Added support for api plugins virtual feeds' (#84) from Shemi/tt-rss:master into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/84
2022-09-29 17:13:37 +03:00
Shemi
f0a20a62c7 Merge branch 'master' into master 2022-09-29 11:18:41 +03:00
Shemi
e2f9a3b9a4 Added support for api plugins virtual feeds 2022-09-29 08:37:56 +03:00
fox
051fc29b55 Merge pull request 'Fix PHP8 strtime warning if argument is null' (#83) from mechnich/tt-rss:master into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/83
2022-09-28 19:34:43 +03:00
jmechnich
359f0af2e7 Fix PHP8 strtime warning if argument is null 2022-09-28 12:29:57 +02:00
fox
d47b8c8494 Merge pull request 'Set user related sessions for single user mode' (#82) from powerivq/tt-rss:language-session into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/82
2022-09-03 08:28:56 +03:00
powerivq
96595ca4c5 Set user related sessions for single user mode 2022-08-31 14:52:42 -07:00
fox
5fea1a7ea9 Merge pull request 'Fix PHP8 empty param warning' (#79) from powerivq/tt-rss:php8compat into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/79
2022-08-31 18:50:39 +03:00
fox
a2c8c92f62 Merge pull request 'Add last_login_update session to single user mode' (#80) from powerivq/tt-rss:last_login into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/80
2022-08-31 18:50:16 +03:00
powerivq
f0f44c6ea5 Add last_login to single user mode 2022-08-31 00:41:57 -07:00
powerivq
f490bdd17a Fix PHP8 empty param problem 2022-08-31 00:36:49 -07:00
xosé m
413d824f23 Translated using Weblate (Galician)
Currently translated at 100.0% (701 of 701 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/gl/
2022-08-25 06:49:25 +00:00
TonyRL
e8b3cdcf4a Translated using Weblate (Chinese (Traditional))
Currently translated at 97.8% (686 of 701 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2022-08-25 06:49:24 +00:00
Andrew Dolgov
c0e77241d3 update gl translation label (2) 2022-08-22 19:10:06 +03:00
Andrew Dolgov
70f500bee9 update gl translation label 2022-08-22 19:08:59 +03:00
Andrew Dolgov
aca16a3448 enable gl translation (Galician) 2022-08-21 18:42:06 +03:00
Andrew Dolgov
3f97b8fdb9 Merge branch 'weblate-integration' 2022-08-21 15:36:43 +03:00
Patrick Ahles
1358746100 Translated using Weblate (Dutch)
Currently translated at 100.0% (701 of 701 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nl/
2022-08-21 07:45:49 +00:00
Marek Pavelka
a49e3af55a Translated using Weblate (Czech)
Currently translated at 100.0% (701 of 701 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/cs/
2022-08-21 07:45:49 +00:00
fox
60658be5bc Merge pull request 'Use PHP 7.4 features' (#77) from wn/tt-rss:feature/php-7.4-stuff into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/77
2022-08-17 19:38:15 +03:00
Andrew Dolgov
ec764f97e7 generate_syndicated_feed: add source section to JSON-formatted output 2022-08-16 20:02:28 +03:00
wn_
0dbed700ef Merge remote-tracking branch 'origin/master' into feature/php-7.4-stuff 2022-08-15 10:43:14 +00:00
Andrew Dolgov
7d77edd1fb amend logic flow to fix phpstan warning in previous 2022-08-15 07:59:24 +03:00
fox
3b7174788d Merge pull request 'Handle no response body, file_get_contents() failure in UrlHelper::fetch()' (#78) from wn/tt-rss:feature/handle-no-response-body into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/78
2022-08-15 07:27:23 +03:00
wn_
830a20debf Handle 'file_get_contents()' failure in 'UrlHelper::fetch()'. 2022-08-14 16:52:44 +00:00
wn_
57e31fe5a7 Handle valid HTTP responses with no response body. 2022-08-14 16:39:03 +00:00
wn_
c301053965 Use the null coalescing assignment operator in various places. 2022-08-12 18:21:38 +00:00
wn_
3487c922b3 Replace use of 'array_merge' with the spread operator and 'array_push' in various places.
This isn't supported for arrays with string keys until PHP 8.1.

https://wiki.php.net/rfc/spread_operator_for_array
2022-08-12 17:58:38 +00:00
wn_
a63c949a55 Use arrow functions in some places. 2022-08-12 14:41:21 +00:00
wn_
6e01d5d930 minor: remove a PHP >= 5.6 check in 'af_redditimgur' 2022-08-12 14:18:43 +00:00
wn_
7567676ed8 Remove a PHP < 7.1 branch in UrlHelper. 2022-08-12 14:16:40 +00:00
wn_
93fd85df6f Switch to direct type declarations of class properties. 2022-08-12 14:13:26 +00:00
Andrew Dolgov
ed2cbeffcc disable composer platform check 2022-08-01 20:38:16 +03:00
Andrew Dolgov
26c67dba77 update phpstan to 1.8.2 2022-07-31 13:55:09 +03:00
Andrew Dolgov
d5c043e846 rework phpstan task to use inotifywait 2022-07-31 11:13:17 +03:00
Andrew Dolgov
ff18453205 enable phpstan task to run in background 2022-07-31 09:49:09 +03:00
Andrew Dolgov
ff7e99b986 readability: import fixed html5-php 2022-07-31 09:42:00 +03:00
Andrew Dolgov
a8b0bce008 add vscode task for phpstan 2022-07-31 09:39:56 +03:00
Andrew Dolgov
7187ab859d fork masterminds html5-php 2022-07-31 09:15:00 +03:00
Andrew Dolgov
4aefbd628e properly check for baseline required PHP version (7.4) 2022-07-29 06:34:20 +03:00
Andrew Dolgov
cbf710161d af_redditimgur: absolutize links before working on them (again) 2022-07-27 07:24:47 +03:00
Andrew Dolgov
e507d006fd Revert "af_redditimgur: absolutize links before working on them"
This reverts commit 6a68ed0208.
2022-07-27 07:19:59 +03:00
Andrew Dolgov
6a68ed0208 af_redditimgur: absolutize links before working on them 2022-07-27 07:16:57 +03:00
Andrew Dolgov
4e02dc0ab5 af_redditimgur: don't try to check if null domain is blacklisted 2022-07-27 07:14:32 +03:00
Andrew Dolgov
7c45b3f789 * add HOOK_LOGINFORM_ADDITIONAL_BUTTONS
* allow plugins to inject JS code into login form
2022-07-24 16:33:28 +03:00
Andrew Dolgov
c0385c2098 public: allow system plugins to expose public methods 2022-07-24 15:51:56 +03:00
Andrew Dolgov
74d7f88fae make_self_url: properly strip out GET params 2022-07-24 14:50:03 +03:00
Andrew Dolgov
8cf421e1fc readability: depend on psr/http-factory 2022-07-24 14:09:22 +03:00
Andrew Dolgov
5006c754c4 readability: add missing dependencies 2022-07-24 14:03:04 +03:00
Andrew Dolgov
f7b3c50828 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2022-07-24 11:52:46 +03:00
Andrew Dolgov
7da7de6e7a use correct namespace for forked readability-php 2022-07-24 11:51:56 +03:00
fox
8f19423c22 Merge pull request 'Fix an error when disabling all user plugins' (#76) from yan12125/tt-rss:fix-setPlugins-error into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/76
2022-07-16 17:55:59 +03:00
Chih-Hsuan Yen
f3aceb4648 Fix an error when disabling all user plugins
If I enabled some user plugins via Preferences -> Plugins and then
disabled all of them, an error occurred:

Jul 16 22:34:20 php[100]: PHP Fatal error:  Uncaught TypeError: array_filter(): Argument #1 ($array) must be of type array, null given in /usr/share/webapps/tt-rss/classes/pref/prefs.php:1027
Jul 16 22:34:20 php[100]: Stack trace:
Jul 16 22:34:20 php[100]: #0 /usr/share/webapps/tt-rss/classes/pref/prefs.php(1027): array_filter()
Jul 16 22:34:20 php[100]: #1 /usr/share/webapps/tt-rss/backend.php(136): Pref_Prefs->setplugins()
Jul 16 22:34:20 php[100]: #2 {main}
Jul 16 22:34:20 php[100]:   thrown in /usr/share/webapps/tt-rss/classes/pref/prefs.php on line 1027

Apparently the issue was elevated from a warning to an error in PHP 8.0
[1].

[1] https://php.watch/versions/8.0/internal-function-exceptions
2022-07-16 22:50:16 +08:00
Andrew Dolgov
b8c1d622a7 add missing files for forked idiorm 2022-07-16 16:30:46 +03:00
Andrew Dolgov
fdd1c43612 downgrade phpstan to 1.1.2 2022-07-16 11:10:19 +03:00
Andrew Dolgov
96f704d157 af_redditimgur: absolutize links before processing them 2022-07-13 07:51:09 +03:00
Andrew Dolgov
5c70d26b7e some very minor php8.1 warnings fixed 2022-07-13 07:08:31 +03:00
Andrew Dolgov
80d3db1dcf upgrade idiorm to php8.1-patched version (aaronpk/idiorm) 2022-07-12 22:26:21 +03:00
Chih-Hsuan Yen
4b61618920 Update php-qrcode and php-settings-container for PHP 8.1
By running the following command after updating composer.json

```
composer update chillerlan/php-qrcode chillerlan/php-settings-container
```

This change fixes a deprecation warning from Preferences ->
Personal data / Authentication -> Authenticator (OTP).

```
Return type of chillerlan\Settings\SettingsContainerAbstract::jsonSerialize() should either be compatible with JsonSerializable::jsonSerialize(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice
1. vendor/chillerlan/php-settings-container/src/SettingsContainerAbstract.php(19): ttrss_error_handler(Return type of chillerlan\Settings\SettingsContainerAbstract::jsonSerialize() should either be compatible with JsonSerializable::jsonSerialize(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice, vendor/chillerlan/php-settings-container/src/SettingsContainerAbstract.php)
2. vendor/composer/ClassLoader.php(571): include(/usr/share/webapps/tt-rss/vendor/chillerlan/php-settings-container/src/SettingsContainerAbstract.php)
3. vendor/composer/ClassLoader.php(428): Composer\Autoload\includeFile(/usr/share/webapps/tt-rss/vendor/composer/../chillerlan/php-settings-container/src/SettingsContainerAbstract.php)
4. vendor/chillerlan/php-qrcode/src/QROptions.php(59): loadClass(chillerlan\Settings\SettingsContainerAbstract)
5. vendor/composer/ClassLoader.php(571): include(/usr/share/webapps/tt-rss/vendor/chillerlan/php-qrcode/src/QROptions.php)
6. vendor/composer/ClassLoader.php(428): Composer\Autoload\includeFile(/usr/share/webapps/tt-rss/vendor/composer/../chillerlan/php-qrcode/src/QROptions.php)
7. vendor/chillerlan/php-qrcode/src/QRCode.php(113): loadClass(chillerlan\QRCode\QROptions)
8. classes/pref/prefs.php(958): __construct()
9. classes/pref/prefs.php(469): _get_otp_qrcode_img()
10. classes/pref/prefs.php(541): index_auth_2fa()
11. backend.php(136): index_auth()
```

The issue is fixed in php-settings-container 2.1.1 [1] Here I use the
latest php-qrcode version for another PHP 8.1 fix [2].

[1] 68bc5019c8 (diff-359c7f7a6d32d9935951e1b0742eb116fb654f4a932c8d40328bb5dcab2fa111L162)
[2] https://github.com/chillerlan/php-qrcode/issues/97
2022-07-12 22:23:48 +03:00
Chih-Hsuan Yen
d9861038bc Update beberlei/assert for PHP 8 compatibility
Run `composer update beberlei/assert` using composer 2.3.8 on PHP 8.1.7

Updating other packages without updating this fails with:

```
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - beberlei/assert v3.2.2 requires php ^7 -> your php version (8.1.7) does not satisfy that requirement.
    - spomky-labs/otphp v10.0.1 requires beberlei/assert ^3.0 -> satisfiable by beberlei/assert[v3.2.2].
    - spomky-labs/otphp is locked to version v10.0.1 and an update of this package was not requested.
```
2022-07-12 22:23:30 +03:00
fox
f8fe5e02f1 Merge pull request 'fix: lower-case remote usernames before validation' (#75) from disconn3ct/tt-rss:fix/auth-remote into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/75
2022-07-08 17:44:06 +03:00
disconn3ct
7e5453b3aa fix: lower-case remote usernames before validation
Fixes a bug where users are saved lowercase but compared mixed-case. Only applies to upstreams that send non-lowercase usernames. No obvious security impact; it results in a unique key violation and not a successful login.
2022-07-08 16:31:15 +03:00
fox
d9ae4204ce Merge pull request 'Fix MySQL search Queries' (#74) from DJ_TBX/tt-rss:master into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/74
2022-07-04 06:27:31 +03:00
DJ_TBX
3d55db6a53 Merge pull request 'Fix MySQL search Queries' (#1) from mysql-search-queries into master
Reviewed-on: https://dev.tt-rss.org/DJ_TBX/tt-rss/pulls/1
2022-07-04 00:18:39 +03:00
DJ_TBX
9d69fd2a56 Fix MySQL search Queries
Add the missing space between "AND" and "MATCH" in MySQL search queries
2022-07-04 00:17:01 +03:00
Andrew Dolgov
b148d2f515 schema: don't use 'create index if not exists' syntax because mysql doesn't support it 2022-06-20 21:49:12 +03:00
Andrew Dolgov
0bb72fbb26 experimental, af_psql_trgm: attempt to pseudo-normalize returned mysql score values by dividing by match length 2022-06-19 23:04:10 +03:00
Andrew Dolgov
50f014e52d implement native fulltext search on mysql 2022-06-19 22:21:54 +03:00
Andrew Dolgov
6d98cc6c80 schema: add fulltext indexes for mysql to support af_psql_trgm and possibly future fulltext search 2022-06-19 21:53:57 +03:00
Andrew Dolgov
9428e2c571 af_psql_trgm: add support for querying against mariadb FULLTEXT indexes 2022-06-19 21:36:33 +03:00
Andrew Dolgov
7e36f6e4c4 Merge branch 'weblate-integration' 2022-06-19 13:46:18 +03:00
Андрій Жук
747899c211 Translated using Weblate (Ukrainian)
Currently translated at 99.8% (700 of 701 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/uk/
2022-06-19 10:42:47 +00:00
Andrew Dolgov
59b0ae8af2 Merge branch 'weblate-integration' 2022-06-15 13:02:47 +03:00
Ptsa Daniel
53bc39fc20 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (699 of 701 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2022-06-15 06:42:41 +00:00
Andrew Dolgov
184efcf3f5 fix rendering of articles with titles containing extremely long words 2022-06-13 14:23:08 +03:00
Andrew Dolgov
c2f7044485 userhelper: fix optional parameter being declared before a required one 2022-06-13 08:37:39 +03:00
Andrew Dolgov
d4be821825 UserHelper, CLI: add a method to check user password 2022-06-10 22:16:48 +03:00
Andrew Dolgov
8632c39eb2 phpstan: limit concurrency 2022-06-10 17:41:38 +03:00
Andrew Dolgov
65f341fbf4 CLI: properly deal with --force-yes on schema update 2022-06-10 16:16:12 +03:00
Andrew Dolgov
25b71b90b2 CLI: exit with error status when operation has failed 2022-06-10 15:39:02 +03:00
Andrew Dolgov
cf1eaeedf3 * add UserHelper methods to manipulate user database (add, modify, delete)
* expose said methods via CLI (update.php)
 * fix several invocations of deprecated functions
 * set stricter type hints on several method arguments
2022-06-10 13:39:00 +03:00
xosé m
f82085ea9b Translated using Weblate (Galician)
Currently translated at 100.0% (701 of 701 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/gl/
2022-06-09 13:15:36 +00:00
Dario Di Ludovico
b7a7673d24 Translated using Weblate (Italian)
Currently translated at 100.0% (701 of 701 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2022-06-09 13:15:35 +00:00
Andrew Dolgov
2975c7297b throttle updates if received HTTP 429 (Too Many Requests) 2022-06-09 09:06:52 +03:00
Glandos
6555ee811d Translated using Weblate (French)
Currently translated at 100.0% (701 of 701 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fr/
2022-06-08 11:20:39 +00:00
Andrew Dolgov
7cd26272fa Revert "minor: Support html content in mailer.php"
This reverts commit b91ffae292.
2022-06-06 21:05:24 +03:00
Andrew Dolgov
8151295829 Revert "trivia: coding style"
This reverts commit 9e557501fa.
2022-06-06 21:05:15 +03:00
Andrew Dolgov
8ef816d8f8 feeds-tree: move external onClick dojo/method to PrefFeedTree class 2022-06-06 09:31:37 +03:00
Andrew Dolgov
6436dd16f9 filter-tree: move external dojo/method to PrefFilterTree class 2022-06-06 09:29:16 +03:00
Weblate
c9c9e4a9ea Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/
2022-06-05 08:52:40 +00:00
Andrew Dolgov
082b3386e9 rebase translations 2022-06-05 11:49:50 +03:00
Andrew Dolgov
dd983e5de1 prefs: move external filter tree onload method (which checks for inactive feeds, etc.) to FilterTree class 2022-06-05 11:47:21 +03:00
Andrew Dolgov
fc84712135 pref-filters: add a button to hide or show rules in the filter list 2022-06-05 11:41:28 +03:00
Andrew Dolgov
185234bc67 gulp task: add flatpak node16 sdk to path because it doesn't inherit launch environment 2022-06-05 11:23:01 +03:00
Andrew Dolgov
9457bb090a fix PHP8 undefined array key warning when resetting prefs to defaults 2022-06-05 11:14:42 +03:00
Andrew Dolgov
d391a01de7 bookmarklets: fix wiki URL 2022-05-30 11:50:25 +03:00
Andrew Dolgov
5adedcd3d0 fix custom-set site URLs never used while updating feeds 2022-05-29 08:02:12 +03:00
Andrew Dolgov
484ab26a3b Merge branch 'weblate-integration' 2022-05-28 23:04:12 +03:00
Andrew Dolgov
b0059d3f88 when determining feed-specific favicon, instead of using first match or generic fallback, go through entire list of determined favicon URLs 2022-05-28 22:27:59 +03:00
fox
09fb2273e6 Merge pull request 'minor: Support html content in mailer.php' (#72) from hardway/tt-rss:master into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/72
2022-05-24 16:49:26 +03:00
Hardway Hou
9e557501fa trivia: coding style 2022-05-24 20:49:01 +08:00
Hardway Hou
b91ffae292 minor: Support html content in mailer.php 2022-05-24 10:09:46 +08:00
Andrew Dolgov
1b3e655f89 use CURLAUTH_BASIC by default for password-protected feeds, keeping
CURLAUTH_ANY as a fallback in case we got a 403.
2022-05-23 08:43:04 +03:00
fox
1152b2454e Merge pull request 'Fix xml parsing error' (#70) from Sie/tt-rss:parsing-fix into master
Reviewed-on: https://dev.tt-rss.org/fox/tt-rss/pulls/70
2022-05-22 12:34:45 +03:00
Siemenskun
3406a16025 Fix typo 2022-05-22 02:02:56 +03:00
Siemenskun
d33d026b12 Fix xml parsing error
Move re-requesting logic before parsing response body, otherwise it puts HTTP headers into XML body
2022-05-22 01:46:46 +03:00
fox
659ad8537a Update 'CONTRIBUTING.md' 2022-05-21 21:21:32 +03:00
Andrew Dolgov
68e49203d1 make phpstan stfu about unmatcher ignored errors (seriously) 2022-05-11 11:12:45 +03:00
Andrew Dolgov
d781354539 add some more phpstan excludes 2022-05-11 07:56:29 +03:00
Mikalai
715ff2145b Translated using Weblate (Belarusian)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/be/
2022-04-18 19:08:29 +00:00
Andrew Dolgov
b17b4a4b9e fix be locale label 2022-04-18 13:04:29 +03:00
Andrew Dolgov
ddb6f4316c enable Belarusian translation (proper UI label pending) 2022-04-18 10:49:18 +03:00
Andrew Dolgov
06940cdd6e Merge branch 'weblate-integration' 2022-04-18 10:21:39 +03:00
Mikalai
c9de9f55bc Translated using Weblate (Belarusian)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/be/
2022-04-17 16:50:10 +00:00
Mikalai
f41ab8e0c8 Translated using Weblate (Belarusian)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/be/
2022-04-17 16:34:49 +00:00
Mikalai
d3160af52d Translated using Weblate (Belarusian)
Currently translated at 48.5% (323 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/be/
2022-04-17 11:57:52 +00:00
Andrew Dolgov
1b1236dc49 Added translation using Weblate (Belarusian) 2022-04-17 06:55:31 +00:00
Andrew Dolgov
2654b3c6be disable some pointless startup sanity checks when running under docker 2022-04-03 19:39:34 +03:00
Andrew Dolgov
41a52c8333 update CONTRIBUTING.md re: gitea registration 2022-03-31 16:35:18 +03:00
Andrew Dolgov
585f37e418 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2022-03-29 13:52:39 +03:00
Andrew Dolgov
4250386ba5 set last_login_update session variable immediately when logging in 2022-03-29 13:52:22 +03:00
fox
ea4bbd7a46 Merge pull request 'Fix af_comics for explosm after site changes' (#68) from ekalin/tt-rss:fix_explosm into master
Reviewed-on: https://git-gitea.tt-rss.org/fox/tt-rss/pulls/68
2022-03-28 20:33:14 +03:00
Eduardo M KALINOWSKI
fdd1831c5d Fix af_comics for explosm after site changes 2022-03-28 12:32:30 -03:00
Andrew Dolgov
385da287d8 rewrite_relative: deal with undefined path warning 2022-03-22 19:43:32 +03:00
Andrew Dolgov
0345e9d3f6 rewrite_relative: use isset() to check for relative path 2022-03-22 16:18:22 +03:00
Andrew Dolgov
ee54904274 add phpunit configuration 2022-03-22 14:34:05 +03:00
Andrew Dolgov
e35a4a1306 tests: add stub autoloader, add a few more rewrite_relative tests 2022-03-22 14:32:32 +03:00
Andrew Dolgov
1c4f7ab3b8 * add phpunit as a dev dependency
* add some basic tests for UrlHelper::rewrite_relative()
 * fix UrlHelper::rewrite_relative() to work better on non-absolute
   relative URL paths
2022-03-22 12:24:31 +03:00
Andrew Dolgov
7116629487 af_readability: ask readability-php library to fix relative URLs 2022-03-22 11:26:22 +03:00
Andrew Dolgov
1703850215 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2022-03-10 14:45:44 +03:00
Andrew Dolgov
6f14a5b261 Merge branch 'weblate-integration' 2022-03-10 14:45:19 +03:00
Pascal Borkenhagen
451556b171 Translated using Weblate (German)
Currently translated at 98.4% (655 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2022-03-10 09:59:26 +00:00
fox
95431890be Merge pull request 'Support PHP 8.1 while keeping compatibility with 7.x' (#67) from yan12125/tt-rss:php8.1 into master
Reviewed-on: https://git-gitea.tt-rss.org/fox/tt-rss/pulls/67
2022-03-08 20:15:01 +03:00
David Edler
de1e218a83 various fixes vor php 8.1 compatibility
Cherry-picked from https://git-gitea.tt-rss.org/fox/tt-rss/pulls/56,
while excluding changes in vendor/ (causes compatiblity issues with
PHP<8 [1]) and strftime-related ones (already re-applied in
https://git-gitea.tt-rss.org/fox/tt-rss/pulls/66).

[1] https://community.tt-rss.org/t/support-for-php-8-1/5089/9
2022-03-09 00:46:15 +08:00
Andrew Dolgov
a395574516 actions dropdown: add context-sensitive UI layout labels 2022-02-25 18:45:00 +03:00
Andrew Dolgov
39c0bd378a getAllCounters: set default value if frontend doesn't pass label or feed id count 2022-02-25 12:41:53 +03:00
Andrew Dolgov
806b46d0c4 * add actions dropdown to toggle combined mode
* hide 'toggle widescreen' menu item when in combined mode
 * unify some mode toggling code in App
2022-02-24 21:07:53 +03:00
Andrew Dolgov
bd0af3ae9e inline attachment links: break long words to prevent horizontal scrolling 2022-02-23 22:16:59 +03:00
Andrew Dolgov
9a5c21630b update.php: better error reporting if invoked with PHP SAPI other than CLI 2022-02-20 16:35:32 +03:00
Andrew Dolgov
f7e2f62022 fix Feeds::_get_counters() used improperly as a replacement for
getFeedUnread()
2022-02-20 12:48:38 +03:00
Andrew Dolgov
77f39d65b5 * Feeds::_get_counters - fix retrieving unread for tags
* mark several symbols as @deprecated properly
 * replace uses of (deprecated) getFeedUnread() with Feeds::_get_counters()
2022-02-20 11:04:40 +03:00
Andrew Dolgov
168dc6fe57 rewrite_relative: prevent php warning when checking for unset content type in EXTRA_SCHEMES_BY_CONTENT_TYPE 2022-02-18 16:44:03 +03:00
Andrew Dolgov
74a247fc5c rewrite_relative: whitelist specific schemes for URLs with 'known' content-types i.e. specified for enclosures 2022-02-17 22:38:38 +03:00
Andrew Dolgov
89ef98e57e allow running as root in a container environment 2022-02-17 17:32:02 +03:00
Andrew Dolgov
079f6dfdd0 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2022-02-05 11:50:02 +03:00
Andrew Dolgov
6738f5c86b note: use proper hook to set click handlers 2022-02-05 11:49:33 +03:00
xosé m
9282804b35 Translated using Weblate (Galician)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/gl/
2022-02-04 12:18:26 +00:00
fox
a7d6ead956 Merge pull request 'Replace deprecated strftime' (#66) from dxbi/tt-rss:replace-strftime into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/66
2022-02-03 18:34:10 +03:00
Felix Eckhofer
cc30198b3d Replace deprecated strftime 2022-02-03 16:13:01 +01:00
Andrew Dolgov
4e35c44add Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2022-02-01 13:17:11 +03:00
Andrew Dolgov
6077175c57 plugins/note: allow editing note by clicking on it 2022-02-01 13:16:23 +03:00
xosé m
f2d71d62b7 Translated using Weblate (Galician)
Currently translated at 69.6% (463 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/gl/
2022-01-30 07:18:15 +00:00
fox
b59bde7b45 Merge pull request 'Add workaround for boolean values being intergers with MySQL/PHP 8.1' (#65) from Schrottfresse/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/65
2022-01-28 10:43:34 +03:00
Schrottfresse
931e33c381 Add workaround for boolean values being intergers with MySQL/PHP 8.1 2022-01-28 08:37:29 +01:00
xosé m
14b1a037ff Translated using Weblate (Galician)
Currently translated at 49.7% (331 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/gl/
2022-01-26 08:41:22 +00:00
Andrew Dolgov
478c9b64a9 make sure notification is closed when saving/creating filter 2022-01-25 22:39:18 +03:00
Andrew Dolgov
c57ebf2c10 fix filter last_triggered not updating 2022-01-25 22:33:13 +03:00
xosé m
b4684ed462 Translated using Weblate (Galician)
Currently translated at 39.5% (263 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/gl/
2022-01-23 14:41:07 +00:00
Andrew Dolgov
fc432f95f7 Added translation using Weblate (Galician) 2022-01-21 15:58:10 +00:00
Andrew Dolgov
56fd06d611 make sure #headline-spacer click to open prompt is not obscured when
mark as read on scroll is enabled

https://community.tt-rss.org/t/click-to-open-next-unread-feed-hidden-by-scrolling/5190
2022-01-21 17:41:11 +03:00
Андрій Жук
765dff83f2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/uk/
2022-01-19 09:41:30 +00:00
fox
49d93989af Merge pull request 'Fix starred images not being deleted' (#64) from klempin/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/64
2022-01-17 20:14:29 +03:00
Philip Klempin
a769ccc51c Fix starred images not being deleted 2022-01-16 22:44:46 +00:00
Andrew Dolgov
3cdbd4422c share plugin: fix previous 2022-01-14 18:08:33 +03:00
Andrew Dolgov
f1759786d7 rework several instances of translated strings being used with single quotes as HTML element attribute values 2022-01-14 18:03:50 +03:00
fox
d3f26cc4a6 Merge pull request '[RFC] update_rss_feed: juxtapose pdo and ORM commit on timestamp update' (#63) from rtollert/tt-rss:update-rss-deadlock1 into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/63
2022-01-14 10:24:57 +03:00
Richard Tollerton
aaccf89501 update_rss_feed: juxtapose pdo and ORM commit on timestamp update
If for whatever reason $pdo holds a DDL lock on ttrss_entries, it could
block ORM's save, leading to a deadlock. To work around this, call
$pdo->commit() before ORM::for_table()->save().
2022-01-13 23:39:49 -06:00
Andrew Dolgov
420782418d Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2022-01-13 13:59:41 +03:00
Andrew Dolgov
304845f380 Merge branch 'master' of git.fakecake.org:fox/tt-rss 2022-01-13 13:59:36 +03:00
Andrew Dolgov
8cf9c451dc Headlines: fix multiple article ids not passed to setScore as an array 2022-01-13 13:59:21 +03:00
fox
4a4928ea25 Merge pull request 'af_readability: use data-src for images if available' (#62) from woodoo/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/62
2022-01-09 14:54:18 +03:00
Eugene Molotov
ac6a34f097 af_readability: use data-src for images if available
data-src is popular attribute to store original images for lazy loading via javascript
2022-01-09 14:51:36 +05:00
fox
f1607902e6 Merge pull request 'themes: Fix incorrect blur and opacity interaction' (#61) from suraia/tt-rss:blur-opacity into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/61
2022-01-06 10:38:42 +03:00
Andrew Dolgov
c3482fbe6b generate a warning if plugin-generated content of HOOK_ARTICLE_BUTTON or _LEFT_BUTTON can't be parsed as valid XML 2022-01-06 10:37:03 +03:00
Michael Kuhn
1ff52bff81 themes: Fix incorrect blur and opacity interaction
Chrome sometimes seems to have problems when using a blur
backdrop-filter in combination with opacity. On Linux, this often
results in the blur being completely ignored. This also seems to apply
to other systems, though. See the following issue for more details:
https://bugs.chromium.org/p/chromium/issues/detail?id=1129838

Making the background opaque using rgba seems to fix the problem.
2022-01-05 20:42:21 +01:00
Andrew Dolgov
97baf3e8b9 process gallery media in the correct order 2021-12-23 17:33:04 +03:00
Andrew Dolgov
1818fc11a5 fetch: return HTTP code when no curl_error() is available 2021-12-23 17:32:44 +03:00
Andrew Dolgov
6971ca08b2 remove deprecated LOG_ constants 2021-12-23 17:32:27 +03:00
Andrew Dolgov
7aeaa1b039 rssutils: rewrite several invocations of (deprecated) rewrite_relative_url() to UrlHelper::rewrite_relative() 2021-12-20 08:03:30 +03:00
Andrew Dolgov
40b2356be2 filters:
* add filter action to ignore feed-provided tags
 * simplify handling of various filter-provided tags
 * bump schema to 146
2021-12-20 07:56:16 +03:00
Andrew Dolgov
92747b1d21 af_redditimgur: don't generate a warning if fallback video is not found for hosted:video post 2021-12-16 09:15:05 +03:00
Andrew Dolgov
720b318796 * fox.form.Select: add several properties allowing it to better
imitate other controls like DropDownButton, etc.
 * rework several main toolbar items to use fox.form.Select instead of
other controls
 * replace HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM with
HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2 because of markup change (option
instead of menuitem)
 * PluginHost: add some explicit typecasts to make intellephense shut up
2021-12-14 21:53:45 +03:00
Andrew Dolgov
8a645892a6 PluginHost: add run_until() and HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2 2021-12-14 21:43:02 +03:00
fox
3ef9bb5b58 Merge pull request 'Handle the admin user not having any entries in 'Feeds::_get_global_unread'.' (#60) from wn/tt-rss:bugfix/get-global-unread-admin-null into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/60
2021-12-14 15:51:36 +03:00
wn_
0726a9d820 Handle another potential 'SUM()' null situation in Feeds. 2021-12-14 12:50:53 +00:00
wn_
ddc81b2c89 Add a note on why ed74c43f18 was needed. 2021-12-14 12:47:25 +00:00
wn_
ed74c43f18 Handle the admin user not having any entries in 'Feeds::_get_global_unread'. 2021-12-14 12:06:32 +00:00
Patrick Ahles
7109871452 Translated using Weblate (Dutch)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nl/
2021-12-08 14:40:29 +00:00
Andrew Dolgov
471f97ca82 Merge branch 'weblate-integration' 2021-12-07 21:58:15 +03:00
Andrew Dolgov
53061d1508 * add HOOK_POST_LOGOUT
* auth_remote: add config option AUTH_REMOTE_POST_LOGOUT_URL
2021-12-06 13:20:18 +03:00
Andrew Dolgov
57b0413a3a af_comics: add Powerup Comics and Danby Draws 2021-12-06 08:14:29 +03:00
fox
6a70f5e92c Merge pull request 'Prevent "Undefined index: version" events for git version with open_basedir after 9dabfbfa11' (#57) from ltGuillaume/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/57
2021-12-03 07:25:06 +03:00
fox
78e82b6043 Merge pull request 'Fixes declaration of Pref_Prefs::csrf_ignore to match IHandler::csrf_ignore' (#58) from jbaldus/tt-rss:jbaldus-fix-pref-prefs into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/58
2021-12-03 07:23:33 +03:00
jbaldus
987870074b Fixes declaration of Pref_Prefs::csrf_ignore to match IHandler::csrf_ignore 2021-12-02 20:57:19 +03:00
ltGuillaume
0269c7ce32 Prevent "Undefined index: version" events for git version with open_basedir after 9dabfbfa11 2021-12-02 18:55:08 +01:00
Andrew Dolgov
5df8dacf9f api, getHeadlines: properly accept feed_id 0 2021-12-01 19:04:42 +03:00
Andrew Dolgov
be94a3a791 combined mode: add a hack (?) for .titleWrap width 2021-12-01 14:02:43 +03:00
Andrew Dolgov
a201e10ee0 Revert "various fixes vor php 8.1 compatibility"
This reverts commit 14027ae04e.
2021-12-01 13:37:35 +03:00
Andrew Dolgov
aaebe55456 Revert "replace strftime with date"
This reverts commit 72e21f89ce.
2021-12-01 13:37:25 +03:00
fox
a72e24343b Merge pull request 'Fixes for php 8.1 compatibility' (#56) from magicDave/tt-rss:php8.1-fixes into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/56
2021-12-01 10:52:12 +03:00
David Edler
72e21f89ce replace strftime with date 2021-11-30 22:07:11 +01:00
David Edler
14027ae04e various fixes vor php 8.1 compatibility 2021-11-30 21:50:09 +01:00
Andrew Dolgov
df7b2e7984 combined mode: limit feed title element to 25% width 2021-11-29 13:39:58 +03:00
Andrew Dolgov
409c63dcf8 remove mixed type hints from function arguments because we still support PHP7 2021-11-29 12:30:33 +03:00
Andrew Dolgov
28fb571dca * fix showing headlines for tag-based virtual feeds
* API: allow retrieving headlines for tag-based feeds (bump api level to 18)
2021-11-29 10:20:13 +03:00
Marek Pavelka
d0cf7a3d97 Translated using Weblate (Czech)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/cs/
2021-11-24 07:41:30 +00:00
Andrew Dolgov
831648e3c8 af_redditimgur: check content-type before downloading data for og:image and imgur pages 2021-11-24 08:27:10 +03:00
Andrew Dolgov
204f92b926 urlhelper: add debugging output for download attempts 2021-11-24 08:19:04 +03:00
Andrew Dolgov
a109e89983 mark clean() return value as nullable 2021-11-23 17:01:08 +03:00
Andrew Dolgov
a6cad5cbca api: don't try to pass null login/password when subscribing to feed 2021-11-23 16:55:21 +03:00
Andrew Dolgov
9dabfbfa11 _get_version:
- don't bother with git if open_basedir is enabled
 - check for SCRIPT_ROOT instead of TTRSS_.. anything because that would be set regardless of install method
2021-11-23 14:46:24 +03:00
TonyRL
17addd1237 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2021-11-21 17:41:59 +00:00
Andrew Dolgov
7cfc30ac25 format_backtrace: revise previous to only try truncating/adding actual strings 2021-11-20 16:15:10 +03:00
Andrew Dolgov
3323ae78ce * sql_bool_to_bool: make parameter nullable
* errorhandler: don't try to truncate null strings
 * UrlHelper::rewrite_relative: fix undefined offset warnings for URLs
that lack schema/host (data: etc)
2021-11-20 16:11:44 +03:00
Andrew Dolgov
e7111e4f14 sanitize: make force_remove_images nullable 2021-11-20 14:02:37 +03:00
Andrew Dolgov
2b4ba59a79 feeds/add: force cast category id to integer 2021-11-19 17:02:25 +03:00
fox
0a3a464def Merge pull request 'Consistently handle param string to bool conversions in handlers.' (#53) from wn/tt-rss:feature/consistent-param-to-bool into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/53
2021-11-19 07:36:15 +03:00
fox
3070933f64 Merge pull request 'Fix Undefined array key "order_by"' (#54) from klempin/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/54
2021-11-19 07:35:20 +03:00
fox
e5ec69f45a Merge pull request 'Don't type DiskCache's $mimeMap.' (#55) from wn/tt-rss:bugfix/diskcache-mimemap-type into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/55
2021-11-19 07:34:26 +03:00
wn_
59a09790eb Don't type DiskCache's $mimeMap.
PHP typing of class properties is only supported on PHP 7.4+.
2021-11-18 23:51:36 +00:00
Philip Klempin
739c3fe760 Fix Undefined array key "order_by" 2021-11-18 22:33:03 +00:00
wn_
4a891b20f0 Fix 'view_mode' default in API#getHeadlines() 2021-11-18 21:31:52 +00:00
wn_
d532eb773b Switch from null to false as the default for missing bool params. 2021-11-18 18:25:04 +00:00
wn_
cd71292610 Actually, always clean in Handler._param_to_bool() 2021-11-18 18:18:49 +00:00
wn_
16a7208893 Clean string params in Handler._param_to_bool() 2021-11-18 18:16:50 +00:00
wn_
2422aae577 Consistently handle param string to bool conversions in handlers. 2021-11-18 18:09:47 +00:00
Andrew Dolgov
57d9a5e925 config: use phpdoc comments for global options, etc 2021-11-18 20:51:11 +03:00
Andrew Dolgov
d6f604c06c API/catchupFeed: properly pass is_cat as bool 2021-11-18 20:02:24 +03:00
Andrew Dolgov
1ea177491f * base plugin class: correct description of hook_house_keeping()
* cache_starred_images: keep status files in a separate cache directory
2021-11-18 19:54:42 +03:00
Andrew Dolgov
b2ffc8c2e3 _format_headlines_list: fix phpstan warning properly 2021-11-18 11:03:26 +03:00
Andrew Dolgov
9ac67c7973 API: fix unexpected null being passed to _order_to_override_query 2021-11-18 11:01:46 +03:00
Andrew Dolgov
b77f6c9a6b API: force methods to return bool to make wrap() mistakes easier to track 2021-11-18 10:46:06 +03:00
Andrew Dolgov
9e469b1642 api: a few more returns in login 2021-11-18 09:04:08 +03:00
Andrew Dolgov
10a1d4d879 api: don't return errors on login success 2021-11-18 09:01:44 +03:00
Andrew Dolgov
63ec5a8965 Merge branch 'wip-phpstan-level6' 2021-11-18 07:32:28 +03:00
wn_
2d830c6281 Minor correction to RSSUtils::cache_enclosures() $enclosures param type.
All FeedEnclosure values are currently strings, even though the numeric things get converted to int before getting inserted in 'ttrss_enclosures'.
2021-11-17 20:45:41 +00:00
wn_
fb1e85baaf Switch FeedParser back to described behavior for setting 'error'.
Also some formatting.
2021-11-17 19:29:54 +00:00
Andrew Dolgov
aeb4137cbd document a few more plugin hooks 2021-11-17 18:30:32 +03:00
Andrew Dolgov
3e273ea527 add descriptions for some plugin hooks 2021-11-17 15:56:58 +03:00
Andrew Dolgov
34f294414f schema: remove default subscription of admin to the forums rss feed 2021-11-17 11:35:40 +03:00
Andrew Dolgov
bd66eff7cc better check for docker 2021-11-17 10:52:37 +03:00
Andrew Dolgov
938f7db482 correctly show non-docker git installs as unsupported 2021-11-17 10:36:04 +03:00
Andrew Dolgov
5980b3d2cb pluginhost: set stricter @params 2021-11-16 18:35:13 +03:00
Andrew Dolgov
10d1a8c05a adjust phpdoc tags for hook definitions/constants (make them reference each other) 2021-11-16 16:31:40 +03:00
Andrew Dolgov
ad30d39e2a not dead: Article.assigntolabel etc are exported methods called by frontend (Headlines.js) 2021-11-16 15:45:35 +03:00
Andrew Dolgov
4166628c36 Merge branch 'wip-phpstan-level6' of git.tt-rss.org:fox/tt-rss into wip-phpstan-level6 2021-11-16 09:19:19 +03:00
Andrew Dolgov
6a8030fd76 mailer: don't crash if php mail() fails with no reported errors 2021-11-16 09:19:12 +03:00
wn_
d78ba7b3a9 Minor fix in 'classes/articles.php'.
It looks like these functions are dead code, though.  Adding comments for future review.
2021-11-16 02:14:31 +00:00
Andrew Dolgov
b2952843f5 * DiskCache: add download() helper
* Af_Comics_Gocomics_FarSide: cache linked images because it seems to
be required anyway
2021-11-15 23:22:21 +03:00
Andrew Dolgov
3a3fde1a2e when uninstall plugins, refresh plugins index instead of reloading entire prefs pane 2021-11-15 20:18:50 +03:00
Andrew Dolgov
8cd69fe15c when uninstall plugins, refresh plugins index instead of reloading entire prefs pane 2021-11-15 20:18:37 +03:00
Andrew Dolgov
ef1e2a8b2f update dojo/dijit to 1.16.4 2021-11-15 18:48:47 +03:00
Andrew Dolgov
1db1be7a81 update phpstan to 1.1.2 (composer files) 2021-11-15 18:44:24 +03:00
Andrew Dolgov
4c37fa4b41 update phpstan to 1.1.2; update php-qrcode to 3.4.1 2021-11-15 18:33:35 +03:00
wn_
109b702ed0 Minor fix to DOMNodeList#item() potential type (null vs false) 2021-11-15 12:24:38 +00:00
Andrew Dolgov
85b974af32 auth_internal: limit password throttling to failed login attempts not using OTP 2021-11-15 13:16:49 +03:00
Andrew Dolgov
26f47c0694 remove themes/Makefile (obsolete, replaced with gulp) 2021-11-15 08:27:49 +03:00
Andrew Dolgov
aa924d9ee7 deal with several DOMElement-related errors 2021-11-15 08:26:02 +03:00
Andrew Dolgov
a92070da06 unignore several phpstan errors 2021-11-15 08:20:56 +03:00
Andrew Dolgov
2493c9cddd set better matching type hint on virtual feed object 2021-11-15 08:19:44 +03:00
Andrew Dolgov
676c5787e7 require virtual feed plugins to implement IVirtualFeed 2021-11-15 07:11:29 +03:00
Andrew Dolgov
8dfefe7968 add IVirtualFeed interface for plugins implementing virtual feeds 2021-11-15 07:08:52 +03:00
Andrew Dolgov
3bd13b91c8 add IVirtualFeed interface for plugins implementing virtual feeds 2021-11-15 07:08:41 +03:00
Andrew Dolgov
07ea364189 Merge branch 'wip-phpstan-level6' of git.tt-rss.org:fox/tt-rss into wip-phpstan-level6 2021-11-15 06:54:04 +03:00
Andrew Dolgov
edc7998851 revise phpdoc annotations for hook_search() 2021-11-15 06:53:55 +03:00
wn_
fb208bb136 Fix a PHPStan warning in 'UrlHelper::rewrite_relative()'. 2021-11-15 03:28:17 +00:00
wn_
41b4eef504 Address PHPStan warnings for FeedEnclosure. 2021-11-15 02:46:19 +00:00
wn_
78acf18b70 Address PHPStan warnings in FeedItem classes. 2021-11-15 02:40:45 +00:00
wn_
8943604aad Change the param type for UserHelper::hash_password() $algo to appease PHPStan.
PHPStan was complaining in 'plugins/auth_internal/init.php' due to UserHelper::hash_password() being passed a string, rather than a UserHelper::HASH_ALGO_* constant.  Just switching the param to string for now.
2021-11-14 22:44:48 +00:00
wn_
324d926eb4 Also fix the param signature for Plugin#hook_hotkey_info() 2021-11-14 22:16:16 +00:00
wn_
12f9df1066 Fix the return signature for Plugin#hook_hotkey_info() 2021-11-14 22:11:27 +00:00
wn_
8f749fe61b Address PHPStan warnings in 'include/colors.php'.
Also some formatting for readability.
2021-11-14 21:06:06 +00:00
wn_
6d438c5a77 Address PHPStan warning in 'classes/pref/users.php'. 2021-11-14 20:27:17 +00:00
wn_
5632d75a45 Address PHPStan warning in 'classes/pref/system.php'. 2021-11-14 20:26:21 +00:00
wn_
abab2a94e8 Address PHPStan warning in 'classes/pref/prefs.php'.
Also update 'select_hash' and 'select_tag' values param, which can have int or string keys.
2021-11-14 20:13:09 +00:00
wn_
b8f0627a0e Address PHPStan warning in 'classes/pref/labels.php'. 2021-11-14 20:13:09 +00:00
Andrew Dolgov
56cf425e45 revise prototype for hook_enclosure_imported 2021-11-14 23:03:25 +03:00
Andrew Dolgov
c3fbf56984 deal with most of warnings in plugins/af_readability 2021-11-14 21:25:56 +03:00
Andrew Dolgov
242cf916ef deal with phpstan warnings in plugins/note, nsfw, and share 2021-11-14 21:20:59 +03:00
Andrew Dolgov
5f808051b2 deal with phpstan warnings in auto_assign_labels and bookmarklets 2021-11-14 21:14:21 +03:00
Andrew Dolgov
f537502fce deal with (most of) phpstan warnings in auth_internal and auth_remote 2021-11-14 21:09:53 +03:00
Andrew Dolgov
67a89e861d Merge branch 'wip-phpstan-level6' of git.tt-rss.org:fox/tt-rss into wip-phpstan-level6 2021-11-14 21:01:25 +03:00
wn_
9326ed605f Address PHPStan warning in 'classes/pref/filters.php'. 2021-11-14 17:59:57 +00:00
wn_
812f5f532e Address PHPStan warning in 'classes/mailer.php'. 2021-11-14 17:59:57 +00:00
Andrew Dolgov
91c9a73532 deal with phpstan warnings in plugins/cache_starred_images.php 2021-11-14 20:59:49 +03:00
Andrew Dolgov
931a7533ce adjust some return types in urlhelper 2021-11-14 20:53:30 +03:00
Andrew Dolgov
80291ffe0c deal with phpstan warnings in plugins/af_redditimgur.php 2021-11-14 20:51:22 +03:00
Andrew Dolgov
cfc31fc692 set annotations/types in af_psql_trgm 2021-11-14 20:36:55 +03:00
Andrew Dolgov
d17b79311e set missing annotations in af_comics 2021-11-14 20:33:37 +03:00
Andrew Dolgov
afdb4b0072 set phpdoc annotations for auth_base 2021-11-14 20:26:05 +03:00
Andrew Dolgov
6bd6a14c20 revise phpdoc annotations for hook_sanitize() 2021-11-14 20:19:12 +03:00
wn_
f5c881586b Handle potentially null link, title, etc. in FeedParser. 2021-11-14 16:59:21 +00:00
Andrew Dolgov
7988c79bd4 plugin.php: add some minor method phpdoc corrections 2021-11-14 18:05:31 +03:00
Andrew Dolgov
1b5c61ac85 userhelper: add a phpdoc variable class hint 2021-11-14 18:02:20 +03:00
Andrew Dolgov
01b39d985c deal with the rest of warnings in plugin.php 2021-11-14 18:00:03 +03:00
Andrew Dolgov
dd7299b6d0 deal with a few more phpstan warnings re: base plugin class 2021-11-14 17:19:35 +03:00
Andrew Dolgov
55729b4bbd fix HOOK_QUERY_HEADLINES being invoked with different argument lists, add some more phpdoc comments for base plugin class 2021-11-14 17:07:47 +03:00
Andrew Dolgov
af2f4460ce * deal with some phpstan warnings in base plugin class
* arguably better hack for incompatible plugins causing E_COMPILE_ERROR
2021-11-14 16:49:10 +03:00
Andrew Dolgov
c3ffa08807 deal with phpstan warnings in update.php 2021-11-14 16:15:31 +03:00
Andrew Dolgov
98af46addd prefs: properly report failures when loading plugin list 2021-11-14 16:13:06 +03:00
Andrew Dolgov
cf93371607 show safe mode warning dialog in prefs 2021-11-14 16:12:27 +03:00
Andrew Dolgov
8b743f7249 xhr.json: properly pass failure callback to xhr.post() 2021-11-14 16:08:01 +03:00
Andrew Dolgov
d3d3bceec9 xhr.json: properly pass failure callback to xhr.post() 2021-11-14 16:07:52 +03:00
Andrew Dolgov
9b5d199ad9 xhr.json: don't call resolve() on failed to parse data 2021-11-14 15:57:56 +03:00
Andrew Dolgov
fe1feca009 xhr.json: don't call resolve() on failed to parse data 2021-11-14 15:57:43 +03:00
Andrew Dolgov
15af164f69 pluginhost: add a hack to not crash on an incompatible plugin more than once (per login) - UGLY 2021-11-14 11:50:55 +03:00
Andrew Dolgov
0a2dcacbcf normalize some mismatching hook function definitions to match base Plugin class 2021-11-14 11:11:49 +03:00
Andrew Dolgov
81a10f69bc deal with phpstan warnings related to base authentication modules 2021-11-14 10:48:32 +03:00
wn_
5a50c333b2 Address PHPStan warnings in 'classes/pref/filters.php'. 2021-11-14 00:59:19 +00:00
Andrew Dolgov
fe5ada7250 set some annotations on Plugin hook methods 2021-11-13 20:07:13 +03:00
Andrew Dolgov
5e34fe17a7 experimental: bring back plugin hooks to Plugin base class once (to be improved/fixed with annotations later) 2021-11-13 20:03:28 +03:00
Andrew Dolgov
618e96b793 deal with some warnings in plugins/trgm,readability and base plugin class 2021-11-13 19:55:30 +03:00
Andrew Dolgov
03d0692268 no need to duplicate annotations 2021-11-13 19:52:47 +03:00
Andrew Dolgov
68d7cf44f9 phpstan: deal with plugins/share 2021-11-13 19:49:37 +03:00
Andrew Dolgov
37827427a2 rework previous Plugin changes as phpdoc annotations 2021-11-13 19:41:50 +03:00
Andrew Dolgov
9845d5fd15 revert all plugin base class related changes to keep compatibility with extant plugins (for the time being) 2021-11-13 19:36:26 +03:00
wn_
edd476e7fe minor: correct $cat type in Pref_Feeds#calculate_children_count() 2021-11-13 16:00:59 +00:00
wn_
a18473e4c0 Address PHPStan warnings in 'classes/pref/feeds.php'. 2021-11-13 15:56:31 +00:00
wn_
b37a03fb31 Fix the type of Labels::update_cache() 2021-11-13 15:56:31 +00:00
Andrew Dolgov
f2323bda81 fix phpstan warnings in classes/plugin-template.php 2021-11-13 18:26:11 +03:00
Andrew Dolgov
70051742af experimental: also don't keep base plugin template as a non-analyzed file 2021-11-13 18:21:04 +03:00
Andrew Dolgov
b381e95792 experimental: auto-generate and add all plugin hook methods to Plugin class 2021-11-13 18:18:05 +03:00
Andrew Dolgov
8a83f061bf fix phpstan warnings in classes/sanitizer.php 2021-11-13 17:52:03 +03:00
Andrew Dolgov
a7983d475e fix phpstan warnings in classes/api.php 2021-11-13 17:51:26 +03:00
Andrew Dolgov
77b8dc7386 fix phpstan warnings in classes/feedparser.php 2021-11-13 17:48:52 +03:00
Andrew Dolgov
45431170b6 fix phpstan warnings in classes/db/migrations.php 2021-11-13 17:31:13 +03:00
wn_
3ba8d964b6 Address PHPStan warnings in 'classes/api.php'. 2021-11-13 14:15:20 +00:00
wn_
1ec003ce35 Typing IHandler methods, typing Handler_Public, fix type of $feed_id (might be tag). 2021-11-13 14:05:48 +00:00
wn_
25775bb407 Fix type of 'check_first_id' in Feeds '_format_headlines_list'. 2021-11-13 04:16:36 +00:00
wn_
d3a81f598b Switch class properties from PHP typing to PHPDoc for compatibility with PHP < 7.4.0 2021-11-12 21:17:31 +00:00
wn_
2c41bc7fbc Address PHPStan warnings in 'classes/mailer.php', 'classes/opml.php', and 'classes/pluginhandler.php'. 2021-11-12 06:16:18 +00:00
wn_
9db5e402a0 Address PHPStan warnings in 'classes/rpc.php'.
Also a couple minor fixes in 'classes/article.php' and 'classes/labels.php'.
2021-11-12 05:42:55 +00:00
wn_
011c941e7c Fix some PHPStan warnings in 'classes/db/migrations.php', 'classes/db/prefs.php', and 'classes/debug.php'. 2021-11-12 05:24:02 +00:00
wn_
b0eb347839 Fix a warning in 'classes/counters.php'. 2021-11-12 05:04:55 +00:00
wn_
f0ad5881c0 PHPStan warning fix in 'backend.php'. 2021-11-12 04:53:53 +00:00
wn_
734be4ebd1 Minor PHPStand warning fix in 'update.php'. 2021-11-12 04:51:35 +00:00
wn_
763515de79 Address PHPStan warnings in 'classes/feeds.php'.
Also some minor related tweaks in other classes.
2021-11-12 04:48:06 +00:00
wn_
5606e38bff Update signature of handler 'csrf_ignore' to include types. 2021-11-12 02:01:31 +00:00
wn_
57bf56f794 Address PHPStan warnings in 'classes/article.php'.
Also related changes in some other classes.
2021-11-12 01:50:40 +00:00
wn_
a0f37c3206 Address PHPStan warnings in 'classes/pluginhost.php'. 2021-11-12 00:06:00 +00:00
wn_
95277fd099 Address PHPStan warnings in 'classes/labels.php'. 2021-11-11 22:28:13 +00:00
wn_
2d5603b196 Address PHPStan warnings in 'classes/diskcache.php'. 2021-11-11 22:07:32 +00:00
wn_
50997df57a Address PHPStan warnings in 'inclasses/digest.php'. 2021-11-11 21:46:44 +00:00
wn_
cc220058e0 Address PHPStan warnings in 'include/functions.php'. 2021-11-11 21:37:34 +00:00
wn_
728a71150a Fix 'TimeHelper::make_local_datetime()' (null is allowed). 2021-11-11 21:33:12 +00:00
wn_
58ea0d4339 Address PHPStan warnings in 'classes/debug.php'. 2021-11-11 21:02:06 +00:00
wn_
00b86bac39 Address PHPStan warnings in 'include/errorhandler.php'. 2021-11-11 20:47:29 +00:00
wn_
e4b8e2d063 Address PHPStan warnings in 'include/controls_compat.php'. 2021-11-11 20:47:20 +00:00
wn_
d2ccbecea6 Address PHPStan warnings in 'include/controls.php'. 2021-11-11 20:47:12 +00:00
wn_
2e3a9098b9 Address PHPStan warnings in 'classes/userhelper.php'. 2021-11-11 20:25:13 +00:00
wn_
f704d25ab1 Address PHPStan warnings in 'classes/timehelper.php'. 2021-11-11 20:12:47 +00:00
wn_
03495c11ed Address PHPStan warnings in 'classes/sanitizer.php'.
This also includes some minor tweaks to things that call 'Sanitizer::sanitize()'.
2021-11-11 19:59:25 +00:00
wn_
3f8aaffd34 Address PHPStan warnings in 'classes/rssutils.php'.
This also includes a minor tweak in 'update.php' to account for 'getopt()' potentially returning false (indicating failure).
2021-11-11 18:53:52 +00:00
wn_
eb068fbc47 Address PHPStan warnings in 'classes/prefs.php'. 2021-11-11 16:47:51 +00:00
wn_
bf2bb875ab Address PHPStan warnings in 'include/sessions.php'. 2021-11-11 15:57:03 +00:00
wn_
14ca0f2ac0 Address PHPStan warnings in 'classes/counters.php'. 2021-11-11 12:26:30 +00:00
wn_
0f324b77df Address PHPStan warning and tweak 'tasks'+'interval' handling in 'update_daemon2.php'.
This ensures both are of the expected type (int) and meet a reasonable minimum.
2021-11-11 12:11:33 +00:00
wn_
7a919a79d7 Fix some additional PHPStan warnings in UrlHelper. 2021-11-11 11:12:40 +00:00
wn_
bf53dfa515 Don't use 'mixed' directly (PHP 8+). 2021-11-10 21:53:28 +00:00
wn_
4cc3374f9f Initial go at PHPStan rule level 6. 2021-11-10 21:38:25 +00:00
Andrew Dolgov
87a30d88d3 plugin cleanup re: phpstan 1.0 warnings 2021-11-10 20:58:40 +03:00
Andrew Dolgov
9e8d69739f add two helper account access levels:
- read only - can't subscribe to more feeds, feed updates are skipped
 - disabled - can't login
define used access levels as UserHelper constants and refactor code to
use them instead of hardcoded numbers
2021-11-10 20:44:51 +03:00
fox
7a52560e4e Merge pull request 'PHPStan 1.0.0 and related warning fixes' (#49) from wn/tt-rss:feature/phpstan-1.0.0-and-fixes into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/49
2021-11-02 08:37:11 +03:00
wn_
57436ee0c1 Address PHPStan warning in 'update.php'.
------ --------------------------------------
Line   update.php
------ --------------------------------------
213    While loop condition is always true.
------ --------------------------------------
2021-11-01 21:10:27 +00:00
wn_
3cc60a0219 Address PHPStan warnings in 'include/colors.php'.
------ ------------------------------------------------------------------
Line   include/colors.php
------ ------------------------------------------------------------------
215    Variable $out might not be defined.
223    Parameter #3 $pad_string of function str_pad expects string, int
        given.
255    Variable $h might not be defined.
317    Variable $img might not be defined.
------ ------------------------------------------------------------------
2021-11-01 21:10:27 +00:00
wn_
9dac9c5a0d Address PHPStan warnings in 'classes/urlhelper.php'.
Intentionally skipping the line 66 one for now; adding an 'is_array' check clears the warning, but there's a larger topic of how to handle an invalid '' that doesn't result in an array.

------ ---------------------------------------------------------------------
Line   classes/urlhelper.php
------ ---------------------------------------------------------------------
66     Offset 'path' on array{scheme: string} in isset() does not exist.
165    Parameter #2 $associative of function get_headers expects bool, int
        given.
167    Parameter #2 $associative of function get_headers expects bool, int
        given.
278    Negated boolean expression is always true.
309    Negated boolean expression is always true.
------ ---------------------------------------------------------------------
2021-11-01 21:10:27 +00:00
wn_
ac5a4f5937 Address PHPStan warning in 'classes/pref/users.php'.
------ -------------------------------
Line   classes/pref/users.php
------ -------------------------------
170    If condition is always false.
------ -------------------------------
2021-11-01 21:10:27 +00:00
wn_
a38892d5d7 Address PHPStan warning in 'classes/pref/prefs.php'.
------ ------------------------------------------------
Line   classes/pref/prefs.php
------ ------------------------------------------------
1328   Expression on left side of ?? is not nullable.
------ ------------------------------------------------
2021-11-01 21:10:27 +00:00
wn_
8a920a16e7 Address PHPStan warnings in 'classes/pluginhost.php'.
------ --------------------------------------------------------------------
Line   classes/pluginhost.php
------ --------------------------------------------------------------------
16     Property PluginHost::$last_registered is never read, only written.
386    If condition is always true.
------ --------------------------------------------------------------------
2021-11-01 21:10:27 +00:00
wn_
a7a59fe0e2 Address PHPStan warning in 'classes/logger/sql.php'.
------ --------------------------------------
Line   classes/logger/sql.php
------ --------------------------------------
4      Property Logger_SQL::$pdo is unused.
------ --------------------------------------
2021-11-01 21:10:27 +00:00
wn_
72cf4f1f0d Address PHPStan warning in 'classes/feeds.php'.
------ ------------------------------------
Line   classes/feeds.php
------ ------------------------------------
8      Property Feeds::$params is unused.
------ ------------------------------------
2021-11-01 21:10:27 +00:00
wn_
5b17c44e70 Address PHPStan warning in 'classes/feeditem/common.php'.
------ ---------------------------------------------
Line   classes/feeditem/common.php
------ ---------------------------------------------
194    No error to ignore is reported on line 194.
------ ---------------------------------------------
2021-11-01 21:10:27 +00:00
wn_
7d8837ca17 Address PHPStan warnings in 'classes/db.php'.
------ --------------------------------------------------
Line   classes/db.php
------ --------------------------------------------------
7      Property Db::$link is unused.
86     Property Db::pdo (PDO) in empty() is not falsy.
------ --------------------------------------------------
2021-11-01 21:10:27 +00:00
wn_
77a98134b8 Address PHPStan warnings in 'classes/config.php'.
------ -----------------------------------------------------------------------
Line   classes/config.php
------ -----------------------------------------------------------------------
3      Constant Config::_ENVVAR_PREFIX is unused.
177    Constant Config::_DEFAULTS is unused.
237    Property Config::$schema_version is never read, only written.
352    Property Config::$migrations (Db_Migrations) in empty() is not falsy.
------ -----------------------------------------------------------------------
2021-11-01 21:10:26 +00:00
wn_
1584d00891 Bump PHPStan to 1.0.0 2021-11-01 21:10:26 +00:00
Andrew Dolgov
9714c4fbcf Merge branch 'weblate-integration' 2021-10-29 14:10:02 +03:00
Andrew Dolgov
76d8b1bf6f HOOK_ARTICLE_BUTTON/HOOK_ARTICLE_LEFT_BUTTON: only try to parse markup if its actually there 2021-10-26 15:45:12 +03:00
Andrew Dolgov
933913410c css: make plugin button container a flexbox
backend: pass plugin button generated code through domdocument to ensure
its correctness; set data-plugin-name attribute on children to make
them sortable via css
2021-10-24 20:11:49 +03:00
Andrew Dolgov
9f734c9050 minor phpstan tweaks 2021-10-22 13:49:08 +03:00
Andrew Dolgov
3b70d1f622 require phpstan via composer 2021-10-22 13:42:29 +03:00
Andrew Dolgov
2a5c2be6cd af_redditimgur: allow subscribing to teddit.net subreddits directly (rewriting to reddit.com) 2021-10-22 13:25:43 +03:00
Andrew Dolgov
41245da8a6 pluginhost: update comments for HOOK_ constants to use phpdoc syntax; add HOOK_PRE_SUBSCRIBE 2021-10-22 13:24:10 +03:00
Andrew Dolgov
6fe0751038 af_redditimgur: set some @var hints 2021-10-22 12:33:55 +03:00
Andrew Dolgov
377e0b812c add data-orig-feed-title to generated headline markup 2021-10-18 07:44:38 +03:00
Andrew Dolgov
dd30825b94 af_comics: pass PluginHost to filter constructors 2021-10-18 07:41:24 +03:00
fox
d1ffe6d6cf Merge pull request 'Fix undefined array key "output" when adding new label' (#47) from klempin/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/47
2021-10-16 08:18:40 +03:00
Philip Klempin
aead30a041 Fix undefined array key "output" when adding new label 2021-10-15 23:05:50 +00:00
Andrew Dolgov
a936e80630 OPML improvements/fixes:
* allow CLI import of OPML files (--opml-import)
 * visualize OPML structure when importing
 * add strict type hints to most OPML class methods
2021-10-15 10:06:00 +03:00
Andrew Dolgov
9e7e0e84d7 fix vfeed menu in three panel mode 2021-10-11 13:49:20 +03:00
Andrew Dolgov
c6f5902cbc fix wrongly renamed CLI options --debug-force-... to --force-... 2021-10-11 12:18:46 +03:00
Andrew Dolgov
a9646b9574 headlines: attach context menu to vfeed title node 2021-10-10 22:17:11 +03:00
Andrew Dolgov
145fc31625 feed tree context menu: add an entry to open originating website 2021-10-10 22:08:17 +03:00
Andrew Dolgov
949e2ab4d2 properly sanitize video poster attribute 2021-09-24 08:40:06 +03:00
TonyRL
23768709d0 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2021-09-22 14:10:57 +00:00
Andrew Dolgov
8ed927dbd2 OPML: multiple fixes
- remove unused integer indexes when exporting filters as JSON
 - fix warning when importing filters without rules
 - properly assign category IDs for category filter rules
 - fix warning: check if outline attributes like xmlUrl are set before trying to use them
 - fix warning: don't try to use libxml_disable_entity_loader on PHP 8
2021-09-08 09:04:15 +03:00
Andrew Dolgov
78ff7770d1 classes/opml: fix indentation; when importing, don't produce warning
on filters with no rules defined.
2021-09-08 08:12:13 +03:00
fox
012a9fdee3 Merge pull request 'Fix undefined index error' (#45) from jpschewe/tt-rss:fix-undefined-index into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/45
2021-09-08 07:40:15 +03:00
Jon Schewe
e44f0cb937 Fix undefined index error
Getting $op is handled at the top of the file, use the same variable
at the end of the file to avoid errors about an undefined index.
2021-09-07 06:38:17 -05:00
Andrew Dolgov
36e174750e fix label ordering in feed tree 2021-09-02 08:21:05 +03:00
fox
b8f82ca12f Merge pull request 'fix password recovery' (#44) from mechnich/tt-rss:fix-password-recovery into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/44
2021-08-25 19:20:01 +03:00
jmechnich
e8f9567d79 fix password recovery 2021-08-25 18:18:04 +02:00
Andrew Dolgov
a1173ab06a block useless usort() E_DEPRECATED for the time being 2021-08-23 18:38:37 +03:00
Andrew Dolgov
2c931df77c remove SELF_USER_AGENT custom constant, replaced with configurable Config::HTTP_USER_AGENT / Config::get_user_agent() 2021-08-23 10:56:31 +03:00
Andrew Dolgov
5c60254474 Pref_Feeds:calculate_children_count - fix operator precedence 2021-08-23 10:45:34 +03:00
TonyRL
235ae4ded2 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2021-08-21 16:10:59 +00:00
Andrew Dolgov
0808123179 fix broken feed tree generation when categories are disabled 2021-08-18 21:02:58 +03:00
fox
5ed108dce4 Merge pull request 'Use ORM more in 'classes/pref/feeds.php'.' (#43) from wn/tt-rss:feature/pref-feeds-idiorm into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/43
2021-08-18 11:07:04 +03:00
fox
28eafa2bcd Merge pull request 'pull latest readability-php via composer' (#42) from niehztog/tt-rss:update-readability into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/42
2021-08-18 07:51:19 +03:00
wn_
23b4152c9e Make prefs feed search case-insensitive.
Previously the search query had to match lower title or feed_url (i.e. searching w/ uppercase wouldn't match).
2021-08-17 23:14:14 +00:00
wn_
992e9cd9e3 Use ORM more in 'classes/pref/feeds.php'. 2021-08-17 23:03:35 +00:00
Nils Gotzhein
b6b6771d8d pull latest readability-php via composer 2021-08-17 22:18:46 +02:00
fox
a73e3bec45 Merge pull request 'Use ORM in some more parts of 'update.php'.' (#41) from wn/tt-rss:feature/update-use-idiorm into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/41
2021-08-16 07:24:15 +03:00
wn_
cf0ec06b8c Use ORM in some more parts of 'update.php'. 2021-08-15 21:48:50 +00:00
Andrew Dolgov
73d14338ab fix rendering of category filters on Uncategorized 2021-07-28 12:59:47 +03:00
TonyRL
aa3aa34c82 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2021-07-20 11:09:40 +00:00
TonyRL
f458450c17 Translated using Weblate (Chinese (Traditional))
Currently translated at 99.3% (661 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2021-07-16 08:49:20 +00:00
TonyRL
a59401b121 Translated using Weblate (Chinese (Simplified))
Currently translated at 96.9% (645 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2021-07-16 08:49:19 +00:00
TonyRL
88c1132bb8 Translated using Weblate (Chinese (Traditional))
Currently translated at 98.0% (652 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2021-07-05 18:40:11 +00:00
TonyRL
4505bf31b5 Translated using Weblate (Chinese (Traditional))
Currently translated at 96.3% (641 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2021-07-02 16:40:08 +00:00
TonyRL
010e343b70 Translated using Weblate (Chinese (Simplified))
Currently translated at 96.6% (643 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2021-07-02 16:40:06 +00:00
Andrew Dolgov
9669bb94de main toolbar: clarify element ordering, fix some indents 2021-06-28 12:16:55 +03:00
Andrew Dolgov
44c5d0feba prolong PHP session cookie automatically to stop hard logouts after SESSION_COOKIE_LIFETIME expires 2021-06-25 12:12:05 +03:00
fox
cd26dbe64c Merge pull request 'Rewrite feed entry link as href content' (#40) from klempin/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/40
2021-06-21 08:22:59 +03:00
Dario Di Ludovico
2610afcdb7 Translated using Weblate (Italian)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2021-06-20 09:32:19 +00:00
Glandos
acbc89b369 Translated using Weblate (French)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fr/
2021-06-20 08:32:42 +00:00
Philip Klempin
14d57d9a14 Rewrite feed entry link as href content 2021-06-19 14:36:04 +02:00
fox
7bd9572aa1 Merge pull request 'Fix operator precedence' (#39) from klempin/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/39
2021-06-18 13:58:19 +03:00
Philip Klempin
1d4d3bc49c Fix operator precedence 2021-06-18 12:52:29 +02:00
Weblate
f16fc3bf41 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/
2021-06-18 10:37:27 +00:00
Andrew Dolgov
93a5ba55d3 rebase translations 2021-06-18 13:36:15 +03:00
Andrew Dolgov
800ebd6373 revise previous a little bit more 2021-06-18 11:52:06 +03:00
Andrew Dolgov
69f261c41d revise previous a little bit 2021-06-18 11:30:11 +03:00
Andrew Dolgov
e9c062a189 UrlHelper::rewrite_relative():
- support invoking specifying owner URL element/attribute
 - restrict mailto/magnet/tel schemes for A href
 - allow some data: base64 image types for IMG src

Sanitizer::sanitize():

 - when checking href and src attributes, pass element tagname and attribute to rewrite_relative()
2021-06-18 11:20:57 +03:00
fox
34807bacd4 Merge pull request 'Skip all urls with schemes different from base_url in rewrite_relative' (#38) from klempin/tt-rss:fix/mailto into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/38
2021-06-17 18:51:35 +03:00
Andrew Dolgov
4e9c3500fb clarify some @deprecation notices 2021-06-17 11:27:00 +03:00
Philip Klempin
b3bedd0a94 Skip URI base on ALLOWED_RELATIVE_SCHEMES in rewrite_relative 2021-06-16 15:24:15 +02:00
Andrew Dolgov
8ed8a10965 add settings profile cloning 2021-06-16 14:24:57 +03:00
Andrew Dolgov
92c78beb90 apply usort workaround for readability-php because its authors were unable to do so for 3 months (https://github.com/andreskrey/readability.php/issues/99) 2021-05-28 14:42:14 +03:00
Andrew Dolgov
8e1281b41e add workaround for prefs feed tree favicon placement 2021-05-28 11:53:44 +03:00
Andrew Dolgov
326850845d UrlHelper::rewrite_relative: don't try to feed NULL to with_trailing_slash() 2021-05-21 17:10:32 +03:00
Andrew Dolgov
dff479af64 feeditem_atom: support xml:base for enclosures and entry content
UrlHelper::rewrite_relative: use base URL path if relative url path is not absolute (experimental)
2021-05-21 15:39:41 +03:00
Andrew Dolgov
d09a64d6f9 split googlereaderkeys plugin into separate repo 2021-05-21 13:30:56 +03:00
Andrew Dolgov
8574532b7f add hotkeys J/K to move between unread feeds 2021-05-20 20:32:00 +03:00
Andrew Dolgov
4795c4a2a9 Merge branch 'weblate-integration' 2021-05-20 20:23:42 +03:00
Eike
0f51350e9f Translated using Weblate (German)
Currently translated at 100.0% (660 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2021-05-19 15:33:05 +00:00
Andrew Dolgov
295fc1f88a API: bump api level to 17 2021-05-18 16:55:00 +03:00
Andrew Dolgov
2adf364c2c provide base configuration object in login response to skip on initial getConfig 2021-05-18 16:54:33 +03:00
Andrew Dolgov
9f6237a1b8 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-05-18 16:37:09 +03:00
Andrew Dolgov
57cd8acfc9 API: return custom sort types in getConfig 2021-05-18 16:36:56 +03:00
fox
77031575ab Merge pull request 'Fix:Plugins-share:init.php - site_url is NULL when share article by URL from archived articles' (#35) from kdan/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/35
2021-05-12 08:42:16 +03:00
linkai
983655165e Fix:Plugins-share:init.php - site_url is NULL when share article by URL from archived 2021-05-12 09:46:52 +08:00
kdan
6c06a26649 Merge branch 'master' into master 2021-05-12 04:43:04 +03:00
Andrew Dolgov
f423874e05 checking for PDO there is rather useless 2021-05-11 19:37:31 +03:00
Andrew Dolgov
b5a559a1a7 sanity check: in single user mode, only test for admin user if migrations have been completed 2021-05-11 19:36:25 +03:00
Andrew Dolgov
e3c4724dc1 use database-backed sessions in single user mode 2021-05-11 19:21:53 +03:00
fox
82749ee7a7 Merge pull request 'Improve missing token check' (#36) from skazi/tt-rss:quiet-csrf into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/36
2021-05-11 11:34:13 +03:00
Jacek Tomasiak
0c38dc8456 Improve missing token check
Avoid "E_NOTICE (8) (classes/userhelper.php:78) Undefined index:
csrf_token" in logs.
2021-05-11 10:32:59 +02:00
kdan
2ccf0e50a2 Merge branch 'master' into master 2021-05-11 11:05:20 +03:00
linkai
acf0e0d266 Fix:Plugins-share:init.php - site_url is NULL when share article by URL from archived 2021-05-11 16:03:18 +08:00
Andrew Dolgov
b2f888e386 include archived articles (which lack associated feed id) when browsing by tag 2021-05-07 19:15:10 +03:00
Andrew Dolgov
fea59de26b af_redditimgur: use core youtube vid helper 2021-05-07 07:42:24 +03:00
Andrew Dolgov
86300a0ca8 add urlhelper to extract youtube video id from url 2021-05-07 07:37:27 +03:00
Andrew Dolgov
d11718c89c fix combined/three panel transition to expandable mode 2021-05-06 22:27:49 +03:00
linkai
0574675ed6 Fix:Plugins-share:init.php - site_url is NULL when share atircle by URL form archived 2021-04-26 12:17:29 +08:00
Andrew Dolgov
e8f78181f1 af_redditimgur: instead of generating potentially blacklisted iframes (i.e. huge black boxes),
save found youtube videos as post enclosures for af_youtube_... plugins to deal with later, if enabled
2021-04-23 20:30:29 +03:00
Andrew Dolgov
88a7130d79 fix for previous changeset that broke expanded mode 2021-04-22 19:49:51 +03:00
Andrew Dolgov
e8e4fc641e Article.pack: add no-op for three panel mode 2021-04-22 17:50:07 +03:00
Andrew Dolgov
df145c8064 * cdm: render enclosures into content element
* deprecate cdm.intermediate
 * implement lazy-load for rendered enclosures
 * simplify pack/unpack logic for articles
2021-04-22 10:45:27 +03:00
Andrew Dolgov
c6befcddb7 Merge branch 'master' of git.fakecake.org:fox/tt-rss 2021-04-21 18:48:46 +03:00
Andrew Dolgov
5a71426ea5 youtube_embed: use embed-responsive 2021-04-21 18:47:49 +03:00
Andrew Dolgov
b3d45a4a5d Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-04-20 09:12:28 +03:00
Andrew Dolgov
cc634ba91d Merge branch 'weblate-integration' 2021-04-20 09:12:13 +03:00
Eike
b12072fef9 Translated using Weblate (German)
Currently translated at 99.8% (659 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2021-04-19 11:32:23 +00:00
fox
a6c5dda7e0 Merge pull request 'FIX: public.php - Undefined index: feed_title' (#34) from ohaucke/tt-rss:public-php-fix into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/34
2021-04-19 11:48:36 +03:00
Oliver Haucke
cfd9e6b53b FIX: public.php - Undefined index: feed_title 2021-04-19 10:43:30 +02:00
fox
0f61675cd0 Merge pull request 'Fix getCategory method.' (#32) from rodneys_mission/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/32
2021-04-13 07:03:55 +03:00
Rodney Stromlund
c18383d1ea Fix getCategory method. 2021-04-12 20:20:52 +00:00
Andrew Dolgov
3e22368962 getPreviousFeed/getNextFeed: implement wrap around 2021-04-12 16:04:45 +03:00
Andrew Dolgov
eadaaebd58 functions_enabled: trim spaces from disable_functions php ini setting 2021-04-12 11:55:19 +03:00
fox
61b4a678ea Merge pull request 'if backend request 'op' is empty fixed' (#27) from Cyb10101/tt-rss:cyb-backend-op into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/27
2021-04-10 08:30:32 +03:00
Cyb10101
c15c1dfb0b if backend request 'op' is empty fixed 2021-04-09 21:55:08 +02:00
Andrew Dolgov
a61348e2b7 pluginhost: add profile_get/profile_set helpers 2021-04-09 14:01:30 +03:00
Andrew Dolgov
a5af15cfe9 fix noscript notifications 2021-04-09 13:45:25 +03:00
Andrew Dolgov
49ef15f11d * fonts-ui: use system font family instead of segoe, etc. by name
* disable segoe-specific baseline hack for the time being
2021-04-07 16:04:50 +03:00
Andrew Dolgov
c0fba62fa0 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-03-29 19:48:10 +03:00
Andrew Dolgov
0acd33abe3 OTP: generate longer secrets, also make them easier to read/copy 2021-03-29 19:26:04 +03:00
Андрій Жук
0294297ccc Translated using Weblate (Ukrainian)
Currently translated at 89.8% (593 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/uk/
2021-03-27 22:30:33 +00:00
Piotr
a4f82fddf4 Translated using Weblate (Polish)
Currently translated at 100.0% (660 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/pl/
2021-03-27 22:30:32 +00:00
Glandos
49b25a6430 Translated using Weblate (French)
Currently translated at 100.0% (660 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fr/
2021-03-27 22:30:29 +00:00
fox
f2f2b6d1f4 Merge pull request 'Adjust quotation marks in search query before 'str_getcsv'.' (#26) from wn/tt-rss:search-quotation-marks into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/26
2021-03-27 09:43:22 +03:00
wn_
5d5c034a90 Adjust quotation marks in search query before 'str_getcsv'.
This moves a potential first quotation mark to before the associated keyword to ensure 'str_getcsv' groups the key and value correctly.  Without this 'str_getcsv' would split on potential spaces within the quoted value.
2021-03-27 00:18:05 +00:00
fox
0b82afabd5 Merge pull request 'Fix automatically showing next feed on catchup' (#25) from wn/tt-rss:bugfix/on-catchup-show-next-feed into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/25
2021-03-26 13:54:41 +03:00
wn_
2ed5a79e64 Fix automatically showing next feed on catchup 2021-03-26 10:00:36 +00:00
Andrew Dolgov
8c32ed76df revert back to lower contrast light theme by default, add separate light-high-contrast.less 2021-03-25 20:29:36 +03:00
Andrew Dolgov
ceb8179ccc don't use css-defined .svg files because firefox 2021-03-24 16:33:03 +03:00
Andrew Dolgov
19c277391e fonts-ui: add Cantarell 2021-03-23 21:06:29 +03:00
Andrew Dolgov
58ab641fea light theme: increase contrast 2021-03-23 20:45:08 +03:00
Andrew Dolgov
be2d1602bd fix previous issue properly 2021-03-23 11:52:08 +03:00
Andrew Dolgov
e3c51b0e6c Revert "clip max displayed counter value to 9999 because of container node width"
This reverts commit c34a4c85bd.
2021-03-23 11:51:17 +03:00
Andrew Dolgov
c34a4c85bd clip max displayed counter value to 9999 because of container node width 2021-03-23 10:47:06 +03:00
Andrew Dolgov
0f6644880a yet another flex feedtree attempt 2021-03-22 16:18:59 +03:00
Andrew Dolgov
98251022d4 Revert "Revert "another attempt at flex-based feed tree""
This reverts commit 43744412f4.
2021-03-22 14:46:23 +03:00
Andrew Dolgov
334a361e79 don't try to j/k move to nonexistant feed 2021-03-22 09:50:58 +03:00
Andrew Dolgov
d275134f26 unify return values for getPreviousFeed and usages of both prev/next 2021-03-22 07:50:56 +03:00
Andrew Dolgov
2e6d48ead7 * Feeds.openNextUnread: fix
* model.getNextFeed: make sure return values are consistent, stop
wrapping back to starred
2021-03-22 07:39:31 +03:00
Andrew Dolgov
43744412f4 Revert "another attempt at flex-based feed tree"
This reverts commit e12a6ca540.
2021-03-21 17:20:03 +03:00
Andrew Dolgov
ef5d6b9b78 Merge branch 'master' of git.fakecake.org:fox/tt-rss 2021-03-21 17:16:40 +03:00
Andrew Dolgov
e12a6ca540 another attempt at flex-based feed tree 2021-03-21 17:14:45 +03:00
Andrew Dolgov
1f5adf1600 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-03-21 09:35:55 +03:00
Andrew Dolgov
68299c914b share: move og:image back to head 2021-03-21 09:35:43 +03:00
fox
56f7b25e85 Merge pull request 'Switch most of API to ORM' (#23) from wn/tt-rss:orm-api into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/23
2021-03-20 17:05:32 +03:00
wn_
711e8e70e0 Switch most of API to ORM
'updateArticle' was left as-is due to Idiorm not supporting efficient multi-row updating (i.e. it would do an UPDATE per row).
2021-03-20 14:00:53 +00:00
Andrew Dolgov
718c9f07fa remove model.getNextUnreadFeed; unify code with feedTree.getNextFeed 2021-03-19 14:06:23 +03:00
Andrew Dolgov
43ea36d030 prefs: allow setting email if it was previously blank 2021-03-17 19:50:04 +03:00
fox
ce9955c6ff Merge pull request 'Switch Handler_Public to ORM' (#22) from wn/tt-rss:orm-handler-public into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/22
2021-03-17 19:46:00 +03:00
wn_
cd52ca80ab Minor cleanup in 'Handler_Public->getProfiles' 2021-03-17 16:37:39 +00:00
wn_
baf3ecd4cf Fix a couple of array index warnings in 'Handler_Public->forgotpass' 2021-03-17 16:30:17 +00:00
Andrew Dolgov
968270ed48 fix excessive CPU usage on linux chromium caused by animated SVG icons 2021-03-17 19:28:20 +03:00
wn_
541a07250c Switch 'Handler_Public->forgotpass' to ORM 2021-03-17 16:18:06 +00:00
wn_
f057c124d1 Switch 'Handler_Public->login' to ORM, fix 'Handler_Public->getProfiles' 2021-03-17 15:52:43 +00:00
wn_
7ea48f7a4b Switch 'Handler_Public->rss' to ORM 2021-03-17 14:00:19 +00:00
wn_
b6ae280446 Switch 'Handler_Public->getProfiles' to ORM 2021-03-17 13:48:27 +00:00
Andrew Dolgov
db0315e596 feed tree: set cursor pointer on tree label 2021-03-17 12:23:27 +03:00
Andrew Dolgov
88534a8ae4 fix loadingNode offset for feeds 2021-03-17 11:45:12 +03:00
Andrew Dolgov
82bed1e651 filter test dialog: remove .gif; cleanup markup 2021-03-17 08:45:04 +03:00
fox
7c2b473d12 Merge pull request 'Switch 'RSSUtils::update_basic_info' to ORM' (#21) from wn/tt-rss:orm-update_basic_info into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/21
2021-03-17 08:24:57 +03:00
wn_
401b22666d Switch 'RSSUtils::update_basic_info' to ORM 2021-03-17 01:51:32 +00:00
Andrew Dolgov
0f5fd9ea13 use svg icon for headlines loadmore prompt 2021-03-16 22:09:01 +03:00
Andrew Dolgov
32c080bec0 use svg icon for the subscribe dialog (night mode) 2021-03-16 21:47:55 +03:00
Andrew Dolgov
166517240e use svg icon for the subscribe dialog 2021-03-16 21:47:10 +03:00
Andrew Dolgov
7a1e1630d8 use svg icon for packed article placeholders 2021-03-16 21:40:20 +03:00
Andrew Dolgov
92f859add2 update night theme re: previous 2021-03-16 21:34:36 +03:00
Andrew Dolgov
a0e41f41a4 add svg loading indicators 2021-03-16 21:32:44 +03:00
Andrew Dolgov
7ec8a6cad0 simplify feed tree expando/loading/feed icon handling 2021-03-16 20:50:23 +03:00
Andrew Dolgov
d9ba403927 remove some hardcoded color values 2021-03-16 13:43:16 +03:00
Andrew Dolgov
44b274b6d4 remove published opml (use CLI instead) 2021-03-16 12:27:46 +03:00
fox
c134aa387d Merge pull request 'Fix E_NOTICE in add_handler().' (#20) from JustAMacUser/tt-rss:fix-addhandler-notice into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/20
2021-03-16 07:15:40 +03:00
Andrew Dolgov
f81a579386 fix selected feedtree item being invisible in dark theme 2021-03-16 07:13:10 +03:00
JustAMacUser
39bbbef030 Fix E_NOTICE in add_handler(). 2021-03-15 16:20:38 -04:00
Andrew Dolgov
1870fe172b feed tree: css cleanup; set cursor 2021-03-15 21:33:35 +03:00
Andrew Dolgov
b23ba3e236 error log: fix column widths 2021-03-15 20:07:59 +03:00
Andrew Dolgov
a0ce7f556b nsfw: set cursor pointer 2021-03-15 19:42:48 +03:00
Andrew Dolgov
1664b87821 Merge branch 'weblate-integration' 2021-03-15 14:46:38 +03:00
Andrew Dolgov
13210747d8 mailer: stop warning if to_name is unset (it's optional anyway) 2021-03-15 14:45:50 +03:00
Andrew Dolgov
15b39a534d Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-03-14 12:49:44 +03:00
Andrew Dolgov
f7ee812db2 update editorconfig 2021-03-14 12:49:37 +03:00
fox
1b71cd9f44 Merge pull request 'Set orm and pdo mysql charset on connection' (#19) from Gravemind/tt-rss:fix-mysql-charset into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/19
2021-03-13 20:08:06 +03:00
Jordan Galby
3d801b1ac5 set orm and pdo mysql charset on connection 2021-03-13 17:56:52 +01:00
Andrew Dolgov
2f402d598d only show right-side feed icon for vfeeds 2021-03-13 11:35:15 +03:00
Andrew Dolgov
38ab3ef11c Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-03-13 11:22:06 +03:00
Andrew Dolgov
4ddcd54e8d * limit progressfunction debugging to size quota exceeded notifications
* af_redditimgur: reparent generated iframes outside of post table
2021-03-13 11:18:59 +03:00
fox
06ebb81eb8 Merge pull request 'Add coalescing operator to otp_enabled when changing user password' (#18) from klempin/tt-rss:fix/undefined-array-key into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/18
2021-03-12 22:33:56 +03:00
Philip Klempin
fa22e1bc35 Add coalescing operator to otp_enabled when changing user password 2021-03-12 20:26:24 +01:00
Andrew Dolgov
4e81233ac9 make description clickable in plugin list row 2021-03-12 18:10:23 +03:00
Andrew Dolgov
fcce1c443e api: don't try to pass null site_url to Article::_get_image() 2021-03-12 17:15:45 +03:00
Andrew Dolgov
bc73bf0f67 cdmToggleGridSpan: toggle classname instead of a style property 2021-03-12 14:05:51 +03:00
Andrew Dolgov
efde6d36c7 add HOOK_HEADLINES_SCROLL_HANDLER 2021-03-12 12:29:26 +03:00
Andrew Dolgov
e85cba5958 sticky header: better positioning strategy 2021-03-12 11:59:26 +03:00
Marek Pavelka
f9d366f028 Translated using Weblate (Czech)
Currently translated at 100.0% (660 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/cs/
2021-03-12 06:53:30 +00:00
Andrew Dolgov
52d1a5c96d gettextify previous 2021-03-12 09:35:56 +03:00
Andrew Dolgov
580eccd3da throttle login attempts, controlled by Config::AUTH_MIN_INTERVAL 2021-03-12 09:35:01 +03:00
Andrew Dolgov
b9268fcc88 schema: add ttrss_users.last_auth_attempt 2021-03-12 09:19:50 +03:00
Andrew Dolgov
96d89fe912 shorten_expanded: reduce log spam 2021-03-12 08:34:03 +03:00
Andrew Dolgov
bd1630d278 grid mode: limit word breaking to link elements 2021-03-12 07:55:40 +03:00
Andrew Dolgov
76a6060ca3 get_override_links: actually return overrides 2021-03-12 07:40:34 +03:00
Andrew Dolgov
4949e1a590 valid OTP code should not be enough to login, oops 2021-03-12 07:32:15 +03:00
Andrew Dolgov
146b1e0feb * shorten_expanded: use ResizeObserver (DUH)
* add HOOK_HEADLINES_RENDERED
2021-03-11 22:55:14 +03:00
Andrew Dolgov
6e0474a7c8 update zoom layout a bit 2021-03-11 22:08:40 +03:00
Andrew Dolgov
f67d2623b7 add some media queries to improve main UI on small-width devices 2021-03-11 19:26:19 +03:00
Andrew Dolgov
a4da2f1e62 continuation of the css cleanup 2021-03-11 15:07:54 +03:00
Andrew Dolgov
755072de91 css cleanup, combined mode, fonts 2021-03-11 14:32:15 +03:00
Andrew Dolgov
de47082ca6 Article.cdmToggleGridSpan: also set as active 2021-03-11 08:45:43 +03:00
Andrew Dolgov
f9a381ecca grid: add a header icon (and a hotkey) to toggle article span entire row 2021-03-11 08:35:02 +03:00
Andrew Dolgov
27ab16b6dc add Config::LOCAL_OVERRIDE_JS 2021-03-11 07:44:58 +03:00
Andrew Dolgov
324aef9f6f route Logger:log() to user_error() if there's no adapter 2021-03-10 21:31:57 +03:00
Andrew Dolgov
03361dda34 remove previous spacer-specific hack, not needed anymore 2021-03-10 21:06:34 +03:00
Andrew Dolgov
24e64b8c78 exp: set last odd grid child to span all columns 2021-03-10 21:03:25 +03:00
Andrew Dolgov
21e0b28cf1 nsfw plugin: we don't actually need any JS 2021-03-10 20:47:00 +03:00
Andrew Dolgov
f9a9fcbb56 fix related to Promise.allSettled() returning a bit different result object 2021-03-10 20:34:48 +03:00
Andrew Dolgov
3e1b3e8ea8 grid: add workaround for a single loaded headline not spanning all columns 2021-03-10 20:27:20 +03:00
Andrew Dolgov
143617afb1 * it feels weird for requireIdleCallback() to be optional while more
modern browser features are required
 * simplify browser startup feature check a bit
2021-03-10 19:53:09 +03:00
Andrew Dolgov
84fe383ed4 adjust grid view footer (3) 2021-03-10 19:31:25 +03:00
Andrew Dolgov
16726ec07f adjust grid view footer (2) 2021-03-10 19:30:32 +03:00
Andrew Dolgov
a0dd5baa51 adjust grid view footer 2021-03-10 19:27:39 +03:00
Andrew Dolgov
5e738ec278 shorten stuff in af_zz_vidmute 2021-03-10 18:23:34 +03:00
Andrew Dolgov
71b12857e0 in grid mode, also force word-break .intermediate (enclosures) 2021-03-10 18:01:14 +03:00
Andrew Dolgov
353ee40378 shorten_expanded: remove loading=lazy on the js side instead 2021-03-10 17:51:06 +03:00
Andrew Dolgov
668b0ac7a6 shorten_expanded: no need to hook on HOOK_SANITIZE anymore 2021-03-10 16:34:27 +03:00
Andrew Dolgov
fb89c3bad0 instead of a fixed column layout, fit based on minimum column size 2021-03-10 16:14:42 +03:00
Andrew Dolgov
5bc47451e1 shorten_expanded: increase timeout 2021-03-10 15:08:36 +03:00
Andrew Dolgov
36ad46e60d * shorten_expanded: use promises instead of a timeout hack
* normalize some icon colors
2021-03-10 14:57:03 +03:00
Ptsa Daniel
02af69328f Translated using Weblate (Chinese (Simplified))
Currently translated at 99.2% (655 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2021-03-10 10:30:31 +00:00
Dario Di Ludovico
4aa595e3ba Translated using Weblate (Italian)
Currently translated at 100.0% (660 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2021-03-10 10:30:29 +00:00
Andrew Dolgov
96031c80bf stop setting specific background color on .cdm.expanded 2021-03-10 12:57:46 +03:00
Andrew Dolgov
e826c9e055 fix crash in preferences due to headlines-frame missing 2021-03-10 12:25:52 +03:00
Andrew Dolgov
f58879c1dc small stuck header fixes in grid mode 2021-03-10 12:10:11 +03:00
Andrew Dolgov
bdc72e5b63 fix headlines-spacer height in grid mode 2021-03-10 11:59:52 +03:00
Andrew Dolgov
df9c389cbf in grid mode, hide feed title from header 2021-03-10 11:51:06 +03:00
Andrew Dolgov
b6033d0bbd grid view tweaks 2021-03-10 11:44:16 +03:00
Andrew Dolgov
0b93d8d013 add hotkey to toggle grid view 2021-03-10 10:01:22 +03:00
Andrew Dolgov
089fa5ec26 use proper syntax for equal-width columns 2021-03-10 09:34:57 +03:00
Andrew Dolgov
87d13e826f fix vfeed group subtitle in grid mode 2021-03-10 09:17:49 +03:00
Andrew Dolgov
eba8c97f36 some minor grid stuff 2021-03-10 09:10:40 +03:00
Andrew Dolgov
a3ab4020bf set #headlines-spacer, etc, to span grid columns 2021-03-10 08:47:47 +03:00
Andrew Dolgov
ddfa39015e experimental: add preference to show combined mode headlines as a 2 column grid 2021-03-10 08:33:56 +03:00
Andrew Dolgov
6ec66d0ce5 set border color on that too 2021-03-09 18:59:49 +03:00
Andrew Dolgov
f804caec90 support coloring counters by feed-id/is-cat; set fresh counter to green 2021-03-09 18:55:28 +03:00
Andrew Dolgov
ae7b87bca9 add HOOK_HEADLINE_MUTATIONS, HOOK_HEADLINE_MUTATIONS_SYNCED 2021-03-09 17:01:22 +03:00
Andrew Dolgov
2160a86092 show E_COMPILE_ERROR in event log at higher severity levels 2021-03-09 17:00:51 +03:00
Andrew Dolgov
4e1c78374f error log: allow wrapping long filenames 2021-03-09 15:17:28 +03:00
Andrew Dolgov
74391ec30a reorganize update.php a bit, remove unneeded options 2021-03-09 14:45:35 +03:00
Andrew Dolgov
dd9d017f7d add another coalesce for rule inverse 2021-03-09 13:42:28 +03:00
Andrew Dolgov
9b321be270 get_article_filters: set coalesce values for inverse and match_any_rule 2021-03-09 09:31:52 +03:00
Andrew Dolgov
4fe2e6bbf1 app password list: fix th/td alignment 2021-03-09 09:04:13 +03:00
Andrew Dolgov
b1961163b8 af_redditimgur: import link flair as tags 2021-03-09 08:19:55 +03:00
Andrew Dolgov
bc7cb76379 describe global settings in classes/config.php 2021-03-08 20:39:11 +03:00
Weblate
63ca6333a5 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/
2021-03-08 16:08:01 +00:00
Andrew Dolgov
ea25c49eb9 update messages.pot 2021-03-08 19:07:05 +03:00
Andrew Dolgov
fe4c284858 Merge branch 'weblate-integration' 2021-03-08 18:57:49 +03:00
fox
9b2267510b Merge pull request 'Default to null 'rv' for plugin update check.' (#17) from wn/tt-rss:inaccurate-available-plugin-updates into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/17
2021-03-08 18:50:52 +03:00
wn_
fed5158ec5 Default to null 'rv' for plugin update check.
Previously 'rv' was returned as an empty JS array, causing 'p.rv.git_status != 0' to evaluate to true and a misleading 'Ready to update' appearing for certain plugins.
2021-03-08 15:38:52 +00:00
Andrew Dolgov
cfb4882591 cleanup javascript_tag and stylesheet_tag 2021-03-08 17:39:24 +03:00
Andrew Dolgov
28dd255c30 show user css editor before xhr is completed 2021-03-08 16:54:11 +03:00
Andrew Dolgov
bfeaf4d6a4 search dialog: add button icon 2021-03-08 15:55:08 +03:00
Andrew Dolgov
ef03f8188c api: add support for setting score (bump api level to 16) 2021-03-08 13:45:15 +03:00
Andrew Dolgov
c26f58d8a5 fix some php8 warnings 2021-03-08 11:16:32 +03:00
Andrew Dolgov
a125e8540d Merge branch 'master' of git.fakecake.org:fox/tt-rss 2021-03-08 10:43:58 +03:00
Andrew Dolgov
1fb7125f90 minor cleanup related to toolbar-main (use dijit methods, etc) 2021-03-08 10:43:49 +03:00
Andrew Dolgov
46b77fc6b7 fix digest preview not working on mysql because of a quoted LIMIT argument 2021-03-08 09:10:44 +03:00
Andrew Dolgov
5db6939dc9 add to previous a bit 2021-03-07 20:24:44 +03:00
Andrew Dolgov
603cc89638 check updates one plugin at a time 2021-03-07 20:11:54 +03:00
Andrew Dolgov
f4d0e7bb6d * af_redditimgur: optionally import score
* add pluginhost->set_array() to set many plugin settings at once
2021-03-07 15:21:31 +03:00
Andrew Dolgov
72c04123d4 HOOK_ARTICLE_IMAGE: stop after first provided match 2021-03-07 14:19:00 +03:00
Andrew Dolgov
518e677a6b nsfw: fix wrong return parameter count in hook article image 2021-03-07 14:00:56 +03:00
Andrew Dolgov
266c8a6eae add nsfw.png placeholder 2021-03-07 13:26:03 +03:00
Andrew Dolgov
ac6a59914b nsfw: support API clients 2021-03-07 13:22:38 +03:00
Andrew Dolgov
ffb93d72ac fix previous to actually save enabled plugins 2021-03-07 12:28:24 +03:00
Andrew Dolgov
773bad1490 prevent list of enabled plugins resetting if saved while in search results 2021-03-07 12:26:33 +03:00
Andrew Dolgov
1dcc36deca make rendered labels clickable 2021-03-07 12:02:23 +03:00
Andrew Dolgov
c036c27ec7 logger: use constants instead of hardcoded string literals 2021-03-07 09:05:23 +03:00
Andrew Dolgov
17650775d2 hide event log accordion pane if LOG_DESTINATION is not sql 2021-03-07 09:02:24 +03:00
Andrew Dolgov
5bb8714839 allow blank override values 2021-03-07 09:00:36 +03:00
Andrew Dolgov
77b5201b7d set plugin list name width same as preferences table 2021-03-07 08:29:47 +03:00
Andrew Dolgov
d6fd0d5462 add some icons, remove some words 2021-03-06 23:51:48 +03:00
Andrew Dolgov
39c570a9ff Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-03-06 23:32:32 +03:00
Andrew Dolgov
b27218a1e3 add some more dialog icons 2021-03-06 23:32:25 +03:00
fox
cb81b784e8 Merge pull request 'Fix "array offset on value of type null" for $error and $old_error' (#16) from ltGuillaume/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/16
2021-03-06 22:42:26 +03:00
Andrew Dolgov
1d9fa2a42e reduce overhead in hash set/get 2021-03-06 22:41:46 +03:00
ltGuillaume
825e362f0e Fix "array offset on value of type null" for $error and $old_error
I tried applying to only $error and only $old_error, but both appear to be needed.

Log entries:
E_NOTICE (8) 	classes/urlhelper.php:464 	Trying to access array offset on value of type null
1. classes/urlhelper.php(464): ttrss_error_handler(8, Trying to access array offset on value of type null, classes/urlhelper.php, 464, [)
2. classes/rssutils.php(464): fetch([{"url":"https://some.url.rss","login":"","pass":"","timeout":15,"last_modified":"Sat, 31 Aug 2019 15:22:31 GMT"})
3. update.php(235): update_rss_feed(732, 1)
2021-03-06 20:33:23 +01:00
Andrew Dolgov
7b0b5b55c7 fix plugins-list line height 2021-03-06 20:28:10 +03:00
Andrew Dolgov
68ecf52594 some small layout fixes, remove a few inline styles 2021-03-06 20:03:36 +03:00
Andrew Dolgov
473ea6255c render list of plugins on the client 2021-03-06 18:14:25 +03:00
Andrew Dolgov
217922899d set some more type hints 2021-03-06 15:23:54 +03:00
Andrew Dolgov
270f0c3132 general cleanup, set some type hints 2021-03-06 15:19:31 +03:00
Andrew Dolgov
63651bd91d fix some leftover variables 2021-03-06 15:05:49 +03:00
Andrew Dolgov
e5469479c1 * don't try to update custom set feed favicons
* cleanup update_rss_feed() a bit, use ORM
2021-03-06 11:17:15 +03:00
Andrew Dolgov
42e057c808 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-03-05 22:33:42 +03:00
Andrew Dolgov
53dcd4b229 fix plugins not shown as already installed if they have more than 1 dash 2021-03-05 22:33:06 +03:00
fox
42cb2e5112 Merge pull request 'The type hint for 'DAEMON_MAX_CHILD_RUNTIME' should be T_INT' (#15) from wn/tt-rss:deamon-max-child-runtime-type-hint into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/15
2021-03-05 21:56:27 +03:00
wn_
2e8b064236 The type hint for 'DAEMON_MAX_CHILD_RUNTIME' should be T_INT 2021-03-05 17:32:32 +00:00
Andrew Dolgov
2cd159e2ce use separate database column for OTP secrets (migrate previous format if needed) 2021-03-05 17:40:17 +03:00
Andrew Dolgov
2aed79d729 schema: add separate otp_secret column 2021-03-05 17:16:48 +03:00
Andrew Dolgov
ecb94ec23d login page: fix a warning if return is unset 2021-03-05 15:35:48 +03:00
Andrew Dolgov
5c1f9f31bd add a bunch of button icons 2021-03-05 15:16:41 +03:00
Andrew Dolgov
fe06416f17 sessions: stop validating against hash of user agent because chromium is sending
different agent headers for whatever reason, example:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/88.0.4324.192 Safari/537.36

Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/88.0.4324.104 Safari/537.36

seems to be related, at least, to App.postOpenWindow() hack.
2021-03-05 12:27:23 +03:00
Andrew Dolgov
98c75a9e43 don't check for plugin updates automatically on pane open 2021-03-05 10:25:32 +03:00
Andrew Dolgov
b649d2240f split af_zz_noautoplay into a separate repo 2021-03-05 10:17:35 +03:00
Andrew Dolgov
c8883d3440 af_comics filters: don't try to load empty html 2021-03-05 10:07:34 +03:00
Andrew Dolgov
bc2953b5e7 split no_url_hashes into a separate repo 2021-03-05 09:55:28 +03:00
Andrew Dolgov
198c9b4069 split scored_oldest_first into a separate repo 2021-03-05 09:52:45 +03:00
Andrew Dolgov
e8e6329040 rename unfairly prefixed get_enclosures() in feeditem 2021-03-05 09:35:24 +03:00
Andrew Dolgov
c744cfe2dc plugin installer: show last commit timestamp 2021-03-05 08:23:26 +03:00
Andrew Dolgov
d016f7a499 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-03-04 19:50:32 +03:00
Andrew Dolgov
476965b161 show installed plugins in the installer list 2021-03-04 19:50:19 +03:00
fox
c9b0196de0 Merge pull request 'Fix Undefined index when using Single User Mode' (#14) from Threk/tt-rss:master into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/14
2021-03-04 18:50:35 +03:00
Threk
9442ceb7bd Fix Undefined index when using Single User Mode 2021-03-04 18:32:18 +03:00
Andrew Dolgov
f398fea414 shorten plugin list action buttons 2021-03-04 16:44:21 +03:00
Andrew Dolgov
cb4b730e42 split af_unburn 2021-03-04 16:31:48 +03:00
Andrew Dolgov
386dc415d9 a bit better search behavior for plugin installer 2021-03-04 16:28:58 +03:00
Andrew Dolgov
9b8b07376f shorten install button text 2021-03-04 15:59:13 +03:00
Andrew Dolgov
f90531ae40 reduce plugin installer entry height 2021-03-04 15:58:26 +03:00
Andrew Dolgov
6cf771f2bc _get_available_plugins: decode as array 2021-03-04 15:57:11 +03:00
Andrew Dolgov
c50a4296a5 split vf_shared 2021-03-04 15:54:44 +03:00
Andrew Dolgov
04128c7870 add search to plugin installer 2021-03-04 15:52:37 +03:00
Andrew Dolgov
2f6ea8b387 split a bunch of plugins into separate repos 2021-03-04 15:09:56 +03:00
Andrew Dolgov
b74e313844 use computed style for element.prototype.visible 2021-03-04 14:53:33 +03:00
Andrew Dolgov
4fda5ccd0e fix a bunch of bookmarklets login forms not leading back 2021-03-04 13:40:54 +03:00
Andrew Dolgov
30765805fd use orm for settings profiles stuff 2021-03-04 12:30:45 +03:00
Andrew Dolgov
31b29e0a56 log applied migrations 2021-03-04 11:33:25 +03:00
Andrew Dolgov
8f8ca49e4b migrations: refuse to apply empty schema files 2021-03-04 10:13:29 +03:00
Andrew Dolgov
4ede76280b migrations: don't try to use transactions on mysql 2021-03-04 09:43:12 +03:00
Andrew Dolgov
bd4ade6329 remove ttrss_version from base schema 2021-03-04 09:31:38 +03:00
Andrew Dolgov
5eb0f3d640 bring back web dbupdate using new migrations system 2021-03-04 09:22:24 +03:00
Andrew Dolgov
e19570f422 sessions: don't check schema version 2021-03-04 08:32:19 +03:00
Andrew Dolgov
c0fb0a5ec0 wip for db_migrations for core schema 2021-03-04 08:30:52 +03:00
Andrew Dolgov
921569e5da support loading base schema as latest version 2021-03-04 07:26:05 +03:00
Andrew Dolgov
8256ab5dd9 wip: initial for db_migrations 2021-03-03 23:38:52 +03:00
Andrew Dolgov
0cb719a404 add basic local plugin uninstaller 2021-03-03 19:35:11 +03:00
Marek Pavelka
5c6c123676 Translated using Weblate (Czech)
Currently translated at 100.0% (660 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/cs/
2021-03-03 16:30:48 +00:00
Andrew Dolgov
dfdb746a76 add word wrap for git stdout/stderr pre elements 2021-03-03 19:18:43 +03:00
Andrew Dolgov
cb7f322f09 add basic plugin installer (uses tt-rss.org) 2021-03-03 19:07:39 +03:00
Andrew Dolgov
06cb181f73 add update button for system plugins 2021-03-03 14:17:55 +03:00
Andrew Dolgov
75e659ba65 reduce Amount of Caps Used in Multiple Dialogs 2021-03-03 14:10:18 +03:00
Andrew Dolgov
0730128a97 add a send test email button to prefs/system 2021-03-03 14:00:18 +03:00
Andrew Dolgov
dbda996a7a previous one was not good enough i guess 2021-03-03 11:37:58 +03:00
Andrew Dolgov
1aedd22306 config::make_self_url() strip index.php etc 2021-03-03 11:35:04 +03:00
Andrew Dolgov
50087df162 * remove _SKIP_SELF_URL_PATH_CHECKS
* simplify SELF_URL_PATH checks wrt trailing slash
2021-03-03 11:23:39 +03:00
Andrew Dolgov
adf7189e94 show timing information in xhr.post/json 2021-03-03 09:56:35 +03:00
Andrew Dolgov
3b67abb0ea reddit: import comment counts 2021-03-02 21:32:20 +03:00
Andrew Dolgov
6f93c45c28 use orm in some more places; prevent _get_cat_title from hitting the db for uncategorized 2021-03-02 20:07:31 +03:00
Andrew Dolgov
9ec0732942 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-03-02 19:21:27 +03:00
Andrew Dolgov
ba86c64d38 add digest preview button, also fix a bunch of bugs 2021-03-02 19:21:21 +03:00
fox
c4b78ed0a6 Merge pull request 'Fix undefined array key warnings when using iOS app' (#12) from sam302psu/tt-rss:undefined-array-keys into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/12
2021-03-02 19:00:08 +03:00
sam302psu
57fdf032e9 changed skip and limit to coalesce to 0 instead of "" 2021-03-02 18:44:13 +03:00
sam302psu
8f8142df29 Fix undefined array key warnings when using iOS app
Use coalesce operator and empty string/default value to fix undefined array key warnings filling up logs when using iOS app to access api.
2021-03-02 17:36:57 +03:00
Andrew Dolgov
386316aba1 update previous (comment) 2021-03-02 17:12:35 +03:00
Andrew Dolgov
1ab6ca57af initialize Db object early because otherwise ORM might be used unconfigured 2021-03-02 17:11:38 +03:00
Andrew Dolgov
d6629ed188 move dbupdater to db/updater; move base SCHEMA_VERSION constant inside db/updater class 2021-03-02 15:03:01 +03:00
Andrew Dolgov
86b12fc06c pluginhost: remove namespace classloader, plugins should use composer instead 2021-03-02 13:38:03 +03:00
Andrew Dolgov
08ff629af5 limit user data sent to frontend 2021-03-02 13:29:54 +03:00
Andrew Dolgov
d4ad483add user editor: allow toggling otp 2021-03-02 13:27:41 +03:00
Andrew Dolgov
982bd838bf use orm when setting personal data; fix some warnings in mailer class 2021-03-02 13:20:41 +03:00
Andrew Dolgov
30b94fb194 store widescreen mode setting in preferences instead of a cookie 2021-03-02 12:22:48 +03:00
Andrew Dolgov
1a7f724bfa move around some methods in base plugins class 2021-03-02 12:15:42 +03:00
Andrew Dolgov
20d0cbff77 use ORM for article _labels_of/_feeds_of 2021-03-02 12:08:54 +03:00
Andrew Dolgov
f9888fc67f use separate connection for logging 2021-03-02 11:37:56 +03:00
Andrew Dolgov
c4eaab8a31 feeds/_add_cat: use ORM 2021-03-02 10:24:15 +03:00
Andrew Dolgov
7cf12233d7 use ORM when subscribing feeds 2021-03-02 10:11:42 +03:00
Andrew Dolgov
dae0476159 sql logger: use orm 2021-03-02 09:58:50 +03:00
Andrew Dolgov
2005a7bf4f revise behavior of Feeds::_cat_of 2021-03-02 09:36:44 +03:00
Andrew Dolgov
f097ae608d article/redirect: use orm (cast id to int) 2021-03-02 09:31:57 +03:00
Andrew Dolgov
3bab5ca6b1 article/redirect: use orm 2021-03-02 09:31:23 +03:00
Andrew Dolgov
f195e86be3 don't rely on exit code when checking version (again) 2021-03-02 08:33:56 +03:00
Andrew Dolgov
84d8b08d1f use orm for feed access keys 2021-03-02 08:26:37 +03:00
Andrew Dolgov
70adfd4a74 * sanitize: never rewrite relative links to our own prefix
* use Config::get_self_url() instead of get_self_url_prefix() in a bunch
of places
2021-03-02 08:16:41 +03:00
Andrew Dolgov
6f835ded78 remove (unused) prefs/toggleAdvanced 2021-03-02 08:10:06 +03:00
Andrew Dolgov
f56a4eab17 use orm for app password stuff 2021-03-02 08:08:48 +03:00
Andrew Dolgov
372e8e062c Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-03-02 07:35:31 +03:00
Andrew Dolgov
51ed72efab use dash instead of space when invoking git to get version 2021-03-02 07:35:20 +03:00
fox
cd504b0e60 Merge pull request 'Get the version as an array in RPC->checkforupdates.' (#11) from wn/tt-rss:bugfix/checkforupdates-version into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/11
2021-03-02 07:31:08 +03:00
wn_
03400bd8d4 Get the version as an array in RPC->checkforupdates. 2021-03-02 03:14:21 +00:00
Andrew Dolgov
031ee47a3e don't try to pass string literal NOW() to ORM as a timestamp 2021-03-01 23:07:20 +03:00
Andrew Dolgov
b150e46a52 revert back load_filters-related changes 2021-03-01 22:25:41 +03:00
Andrew Dolgov
cd962dfa00 delete Article getScore (seems to be unused) 2021-03-01 20:32:44 +03:00
Andrew Dolgov
56f658711f use orm for a bunch of short feed/cat queries 2021-03-01 20:25:53 +03:00
Andrew Dolgov
8b1a2406e6 userhelper: use orm for a few more user-related things 2021-03-01 19:32:27 +03:00
Andrew Dolgov
127a868e40 userhelper: use orm for some things 2021-03-01 19:03:21 +03:00
Andrew Dolgov
f38be747d1 initial for idiorm 2021-03-01 18:36:47 +03:00
Andrew Dolgov
f96abd2b52 generate_syndicated_feed: timestamp is a strtotime() expression, not an integer 2021-03-01 16:16:50 +03:00
Andrew Dolgov
2d1391a02b come to think of it, we don't need it at all 2021-03-01 15:50:41 +03:00
Andrew Dolgov
dbad39d7a2 auth_internal: don't try to get otp_enabled on old schema 2021-03-01 15:49:44 +03:00
Andrew Dolgov
6359259dbb simplify internal authentication code and bump default algo to SSHA-512 2021-03-01 15:24:18 +03:00
Andrew Dolgov
320503dd39 move version-related stuff to Config; fix conditional feed requests 2021-03-01 13:43:37 +03:00
Andrew Dolgov
20a844085f hide version for bundled plugins because it's meaningless; for everything else support showing version using git (if about[0] is null) 2021-03-01 12:11:42 +03:00
Andrew Dolgov
1e6973307c we don't need to initialize urlhelper properties 2021-03-01 10:23:44 +03:00
Andrew Dolgov
7ef72fe0dc move startup checks to Config, set a bunch of @deprecated annotations 2021-03-01 10:20:21 +03:00
Andrew Dolgov
b05d4e3d9f speed up plugin updating a bit, fix some phpstan warnings 2021-02-28 21:50:05 +03:00
Andrew Dolgov
bf02afed45 check schema version on backend calls because session stuff does it anyway and it's already cached 2021-02-28 17:46:36 +03:00
Andrew Dolgov
1bb0d9b603 sanity_check: config.php is now optional, also cleanup some error messages 2021-02-28 17:42:21 +03:00
Andrew Dolgov
a22ddb2fe0 move material-icons to composer 2021-02-28 14:53:04 +03:00
Andrew Dolgov
bada1601fc OTP form: simplify layout, use dojo controls 2021-02-28 14:18:23 +03:00
Andrew Dolgov
f4fdc9c2a3 some plugin updater UI improvements 2021-02-28 12:52:27 +03:00
Andrew Dolgov
afc7142250 move all $fetch globals to UrlHelper 2021-02-28 10:12:57 +03:00
Andrew Dolgov
e2cbb54b2c plugin updater: show changes before updating 2021-02-28 09:46:06 +03:00
Andrew Dolgov
7f2fe465b0 add plugin updates checker into normal updates checker 2021-02-27 19:14:13 +03:00
Andrew Dolgov
d821e4b090 disable plugin update checking if CHECK_FOR_UPDATES is disabled 2021-02-27 17:40:17 +03:00
Andrew Dolgov
85f411d688 don't try to update all plugins 2021-02-27 17:35:00 +03:00
Andrew Dolgov
15f9cb708e reload prefs when plugin updater is closed 2021-02-27 17:32:41 +03:00
Andrew Dolgov
de63e3799a only show plugin update buttons when needed 2021-02-27 17:29:41 +03:00
Ptsa Daniel
5832b0b040 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.2% (655 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2021-02-27 11:30:59 +00:00
Andrew Dolgov
cf5c7c4f29 feeds/add: hide php8 warning 2021-02-27 14:16:49 +03:00
Andrew Dolgov
78a7b3642f af_redditimgur: allow adding custom tags for NSFW posts 2021-02-27 13:50:28 +03:00
Andrew Dolgov
dfff2cef7b add basic updater for stuff in plugins.local 2021-02-27 13:05:02 +03:00
Andrew Dolgov
5edcbf2e9b add an option to disable conditional counters 2021-02-27 11:25:07 +03:00
Andrew Dolgov
c1cd3324e3 bump schema for ttrss_user_labels2 indexes 2021-02-27 11:04:25 +03:00
Andrew Dolgov
6d06450649 don't rely only on label_cache contents when displaying headline labels 2021-02-27 10:58:11 +03:00
Andrew Dolgov
126b1fd2de don't try to compare null value against anything 2021-02-26 21:48:20 +03:00
Andrew Dolgov
c521e26a19 use absolute namespace for readability 2021-02-26 19:55:49 +03:00
Andrew Dolgov
d6bb77f452 exclude a bunch of phpunit test files 2021-02-26 19:37:09 +03:00
Andrew Dolgov
ebf16a36a1 remove a bunch of return type hints that didn't quite fit 2021-02-26 19:27:40 +03:00
Andrew Dolgov
ef8c3abd7e Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-02-26 19:17:27 +03:00
Andrew Dolgov
3fd7856543 * switch to composer for qrcode and otp dependencies
* move most OTP-related stuff into userhelper
* remove old phpqrcode and otphp libraries
2021-02-26 19:16:17 +03:00
fox
c6fb62f384 Merge pull request 'fix-mysql-support' (#10) from klatch/tt-rss:fix-mysql-support into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/10
2021-02-26 19:03:15 +03:00
Andrew Dolgov
bc4475b669 add missing composer files 2021-02-26 17:39:57 +03:00
Andrew Dolgov
cf1ede0ba8 pull latest readability-php via composer 2021-02-26 17:35:58 +03:00
fox
1baf8c5217 Merge pull request 'Fix the type hint for '_DEFAULT_VIEW_MODE'.' (#9) from wn/tt-rss:bugfix/default-view-mode-type into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/9
2021-02-26 15:46:48 +03:00
Andrew Dolgov
d577eb898c when browsing by tags, return same set of columns as normally 2021-02-26 15:45:30 +03:00
Andrew Dolgov
c01b6e43fd add pluginhost->get_array() shorthand 2021-02-26 15:33:59 +03:00
wn_
86513d70dd Fix the type hint for '_DEFAULT_VIEW_MODE'. 2021-02-26 12:21:58 +00:00
Andrew Dolgov
bf9033beb6 rebase-translations: disable everything except for messages.pot 2021-02-26 14:50:12 +03:00
Andrew Dolgov
167c9fc34e silence php8 warnings in otp secondary login form 2021-02-26 14:25:40 +03:00
Andrew Dolgov
e6a875b7e4 check if client-presented URL scheme is different from one configured in SELF_URL_PATH 2021-02-26 14:14:44 +03:00
Andrew Dolgov
4896874bda _get_headlines: don't try to use _SESSION uid 2021-02-26 13:52:16 +03:00
Andrew Dolgov
fa7c6a6129 we need to compile .mo files after all 2021-02-26 13:26:31 +03:00
Dario Di Ludovico
b63119df33 Translated using Weblate (Italian)
Currently translated at 100.0% (660 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2021-02-26 10:23:38 +00:00
Andrew Dolgov
b5d9b285f1 Translated using Weblate (Russian)
Currently translated at 91.5% (604 of 660 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/ru/
2021-02-26 10:23:26 +00:00
Weblate
05364e11ed Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/
2021-02-26 10:08:32 +00:00
Andrew Dolgov
cb512d653c match a few more translated strings 2021-02-26 13:07:31 +03:00
Andrew Dolgov
2a0b3a161c rebase-translations: try only dealing with messages.pot, let weblate rebuild .po files 2021-02-26 13:03:33 +03:00
Weblate
ab0bf8692d Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/
2021-02-26 09:59:53 +00:00
Andrew Dolgov
c21fbb2d13 rebase translations, fixing a few JS strings not mached; remove obsolete scripts (2) 2021-02-26 12:59:13 +03:00
Andrew Dolgov
15cad4a9c0 rebase translations, fixing a few JS strings not mached; remove obsolete scripts 2021-02-26 12:58:33 +03:00
Andrew Dolgov
634f1210a6 Merge branch 'weblate-integration' 2021-02-26 12:56:30 +03:00
Andrew Dolgov
9a2f893672 Translated using Weblate (Russian)
Currently translated at 90.0% (588 of 653 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/ru/
2021-02-26 09:55:58 +00:00
Andrew Dolgov
8d49b6396e Merge branch 'weblate-integration' 2021-02-26 12:47:23 +03:00
Ptsa Daniel
5794a801f0 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.2% (648 of 653 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2021-02-26 09:46:08 +00:00
Dario Di Ludovico
1dfa699aea Translated using Weblate (Italian)
Currently translated at 100.0% (653 of 653 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2021-02-26 09:46:06 +00:00
Glandos
a6853d2f49 Translated using Weblate (French)
Currently translated at 100.0% (653 of 653 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fr/
2021-02-26 09:46:04 +00:00
Andrew Dolgov
26a6177bc9 upd previous 2021-02-26 12:36:15 +03:00
Andrew Dolgov
9689f884ab add Prefs::DEBUG_HEADLINE_IDS 2021-02-26 12:34:50 +03:00
Andrew Dolgov
05f690c86b add a separator before HEADLINES_NO_DISTINCT 2021-02-26 10:22:04 +03:00
Andrew Dolgov
3ab664f846 feeds/view: silence view_mode warning 2021-02-26 10:02:25 +03:00
Andrew Dolgov
f3d4bae32e add an option to disable DISTINCT on headlines query (unless it's Labels category) 2021-02-26 09:57:34 +03:00
Andrew Dolgov
51142e1bf8 silence phpstan warning 2021-02-26 09:24:43 +03:00
Andrew Dolgov
7815a881e8 cleanup previous 2021-02-26 09:22:16 +03:00
Andrew Dolgov
56b10fea18 pass translations to frontend as a json object 2021-02-26 09:21:17 +03:00
Andrew Dolgov
fd9cd52929 prefs: migrate after cache has been filled to skip 1 pref request 2021-02-25 21:45:16 +03:00
Andrew Dolgov
a1ca62af50 cache schema version better 2021-02-25 21:42:05 +03:00
Andrew Dolgov
22ae284db4 reduce overall amount of unnecessary database queries 2021-02-25 21:27:16 +03:00
Andrew Dolgov
281f2efeb8 wrap prefs->migrate() into a transaction block 2021-02-25 19:21:29 +03:00
Andrew Dolgov
89ad25405e userhelper: only notify failed login for actual logins 2021-02-25 18:26:37 +03:00
Andrew Dolgov
8915bd1b21 fix crash caused by non-numeric non-null _SESSION[uid] passed to sql logger 2021-02-25 18:21:48 +03:00
Andrew Dolgov
34c74400a4 enforce some stricter type checking for loggers 2021-02-25 17:10:03 +03:00
Andrew Dolgov
dcf0135285 logger: shorter syntax 2021-02-25 15:49:30 +03:00
Andrew Dolgov
59c14e9c00 api: remove base64 encoded passwords (wtf), log all authentication failures in userhelper 2021-02-25 15:39:46 +03:00
Andrew Dolgov
efd196839a stop caching schema version entirely, fix some session_start() related warnings 2021-02-25 15:28:27 +03:00
Andrew Dolgov
1464abbbfc prefs cleanup 2021-02-25 14:59:02 +03:00
Andrew Dolgov
f137e64a13 get_version: pass int to strftime() 2021-02-25 14:51:13 +03:00
Andrew Dolgov
c96172fa04 use constants in get_pref()/set_pref() 2021-02-25 14:49:58 +03:00
Andrew Dolgov
5aa05c90e1 pref-prefs: use constants instead of hardcoded strings 2021-02-25 14:45:11 +03:00
Andrew Dolgov
011e318947 prefs: don't try to do anything on schema < 141 2021-02-25 14:38:29 +03:00
Andrew Dolgov
6f02b1afd0 cleanup a bunch of old prefs code 2021-02-25 14:25:37 +03:00
Frenck Lutke
27b676b7b2 fix checkboxes shown as checked when they're not with mysql
The issue occurs because boolean/tinyint values are retrieved from mysql
as strings, and in php/js all non-empty strings are cast as boolean
true.

Current PDO mysql driver doesn't support `PDO::ATTR_STRINGIFY_FETCHES =
false`, and if I disable prepare-emulation so it uses the native MySQL
driver instead which supposedly does support it, prepare statements no
longer play nice with named parameters.

Every remaining clean solution that comes to mind that can cover all
cases, just for MySQL, adds an annoying amount of additional code /
overhead.

As long as the `App.FormFields.checkbox_tag()` JS function is the only
one suffering from the lack of conversion, I'll go with easy ugly over
here.
2021-02-25 12:24:23 +01:00
Andrew Dolgov
7f18e8c33b updater: show owner login instead of just uid 2021-02-25 14:23:56 +03:00
Andrew Dolgov
7869378436 deal with feed update scheduling w/ new prefs 2021-02-25 14:20:54 +03:00
Frenck Lutke
2f2642bbd4 add fallback for feed_language on edit-feed-saving
Feed_language is only included in the form if running on pgsql, failing
the not null constraint on mysql setups.
2021-02-25 12:06:25 +01:00
Andrew Dolgov
00d0cb8c81 remove unused data from schema files 2021-02-25 12:58:00 +03:00
Andrew Dolgov
2621fe7955 fix get_pref always using default profile; remove unneeded code from db_prefs 2021-02-25 12:53:20 +03:00
Andrew Dolgov
bd2314170d implement prefs UI based on new prefs class and a few more things 2021-02-25 12:46:13 +03:00
Andrew Dolgov
e858e979e9 Merge branch 'master' into wip-new-prefs 2021-02-25 10:35:01 +03:00
Andrew Dolgov
49a9afadce add prefs caching 2021-02-25 10:34:59 +03:00
Andrew Dolgov
1112922029 bump schema for upcoming prefs overhaul 2021-02-25 10:11:09 +03:00
Andrew Dolgov
8026f3c3bd initial (wip) for new prefs: add missing 2021-02-25 09:34:03 +03:00
Andrew Dolgov
988eb3ac91 initial (wip) for new prefs 2021-02-25 09:33:36 +03:00
Andrew Dolgov
922a699215 reorder debug targets 2021-02-24 22:28:49 +03:00
Weblate
c70fc68012 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/
2021-02-24 18:58:52 +00:00
Andrew Dolgov
93940d2a9f Merge branch 'master' of git.fakecake.org:fox/tt-rss into weblate-integration 2021-02-24 21:56:52 +03:00
Andrew Dolgov
1adacd0572 rebase translations 2021-02-24 19:55:18 +03:00
Andrew Dolgov
db583287b2 add hide/show events for feeds sidebar 2021-02-24 17:01:40 +03:00
Andrew Dolgov
2f14fa1bc3 add a hack to position labels on a dijit toolbar better 2021-02-24 15:53:47 +03:00
Andrew Dolgov
7f41228a71 decouple runtime-info object from counters 2021-02-24 15:40:19 +03:00
Andrew Dolgov
553548b689 request label counters conditionally 2021-02-24 15:07:31 +03:00
Andrew Dolgov
9313ebf2e7 fix warning in counters::get_feeds() 2021-02-24 13:25:26 +03:00
Andrew Dolgov
8b09e653e0 pass array to setScore 2021-02-24 12:10:09 +03:00
Andrew Dolgov
155e4f6125 pass a bunch of related arrays properly to backend 2021-02-24 12:07:25 +03:00
Andrew Dolgov
96182597c4 fix typo 2021-02-24 10:38:54 +03:00
Andrew Dolgov
9ad5f04e51 only request counters once for headline mutations 2021-02-24 10:31:03 +03:00
Andrew Dolgov
e468e5a589 cats_of: enforce owner_uid 2021-02-24 10:09:08 +03:00
Andrew Dolgov
6ea1430a04 no special counter handling for catchupAll 2021-02-24 10:01:39 +03:00
Andrew Dolgov
e6505b7d83 _cats_of: only request parents if needed 2021-02-24 09:56:59 +03:00
Andrew Dolgov
d6203bf350 try to calculate counters conditionally based on feed ids 2021-02-24 09:47:26 +03:00
Andrew Dolgov
a42e8aad97 add Errors.php 2021-02-23 22:31:43 +03:00
Andrew Dolgov
8d2e3c2528 drop errors.php and simplify error handling 2021-02-23 22:26:07 +03:00
Andrew Dolgov
37d46411c7 App.requestCounters() is not a thing 2021-02-23 17:43:35 +03:00
Andrew Dolgov
85095f8a53 rename TTRSS_SESSION_NAME to SESSION_NAME 2021-02-23 17:01:25 +03:00
Andrew Dolgov
ab4dafa4be config: add a type hint system 2021-02-23 16:58:48 +03:00
Andrew Dolgov
9e2e12dff8 add some ;s 2021-02-23 13:36:02 +03:00
Andrew Dolgov
46e650622c floIcon: declare images property 2021-02-23 11:05:58 +03:00
Andrew Dolgov
2ae0b7059f cleanup some defined-stuff 2021-02-23 09:01:27 +03:00
Andrew Dolgov
5229cc58b2 Merge branch 'wip-config-object' 2021-02-23 08:34:37 +03:00
Andrew Dolgov
4ed91619dd af_redditimgur: fix an oopsie 2021-02-23 00:28:05 +03:00
Andrew Dolgov
cae54dad56 af_redditimgur: fix an oopsie 2021-02-23 00:27:52 +03:00
Andrew Dolgov
6e4fbbfa4d cleanup config.php-dist 2021-02-23 00:05:20 +03:00
Andrew Dolgov
29ada58b4a move db-prefs shortcut functions to functions.php 2021-02-22 23:25:14 +03:00
Andrew Dolgov
77e6d589ff allow adding custom config options 2021-02-22 23:20:52 +03:00
Andrew Dolgov
fd5dd27f16 Merge branch 'master' of git.tt-rss.org:fox/tt-rss into wip-config-object 2021-02-22 23:11:43 +03:00
fox
ac6cea859a Merge pull request 'Check whether data is parsable by 'imagecreatefromstring' in jimIcon.' (#7) from wn/tt-rss:jimIcon-imagecreatefromstring into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/7
2021-02-22 23:10:43 +03:00
Andrew Dolgov
caf3040313 update config.php-dist 2021-02-22 23:04:50 +03:00
Andrew Dolgov
445ac1213c finalize config:: migration; make config.php optional 2021-02-22 22:51:12 +03:00
Andrew Dolgov
6b7af973b2 update gitignore 2021-02-22 22:43:07 +03:00
Andrew Dolgov
12bcf826e4 don't include config.php everywhere 2021-02-22 22:39:20 +03:00
Andrew Dolgov
211f699aa0 migrate the rest into Config:: 2021-02-22 22:35:27 +03:00
Andrew Dolgov
383f4ca04a add config.php 2021-02-22 21:49:09 +03:00
Andrew Dolgov
e4107ac952 wip: initial for config object 2021-02-22 21:47:48 +03:00
wn_
7c966b69d5 Check whether data is parsable by 'imagecreatefromstring' in jimIcon. 2021-02-22 18:03:36 +00:00
Andrew Dolgov
42173386b3 dirname(__FILE__) -> __DIR__ 2021-02-22 17:38:46 +03:00
Andrew Dolgov
add6242e51 do not use define_default() because it screws with static analyzers 2021-02-22 17:35:52 +03:00
fox
3f00502305 Merge pull request 'Let 'RSSUtils::check_feed_favicon' update existing favicons.' (#6) from wn/tt-rss:check-feed-favicon into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/6
2021-02-22 17:25:02 +03:00
wn_
6fbf7ef368 Remove check against the old file in 'RSSUtils::check_feed_favicon'. 2021-02-22 12:06:27 +00:00
Andrew Dolgov
be4e7b1340 fix several issues reported by phpstan 2021-02-22 14:41:09 +03:00
Andrew Dolgov
043ef3dad6 add chrome configuration for debugging 2021-02-22 13:44:25 +03:00
Andrew Dolgov
167ed87684 add launch.json for xdebug 2021-02-22 11:40:31 +03:00
Andrew Dolgov
33fff26869 reinstate HOOK_RENDER_ENCLOSURE 2021-02-22 10:00:50 +03:00
wn_
02a9485966 Try to limit max favicon size, don't store current/old in a var. 2021-02-21 23:30:31 +00:00
Andrew Dolgov
6f29ecbbb9 add phpstan config 2021-02-21 23:19:58 +03:00
Andrew Dolgov
f6bfb89b29 pref-prefs: switch to new control shorthand in a few places 2021-02-21 23:18:32 +03:00
wn_
cb401af6f6 Let 'RSSUtils::check_feed_favicon' update existing favicons. 2021-02-21 19:01:40 +00:00
Andrew Dolgov
861a632ac7 move published opml JS code to pref helpers 2021-02-21 18:04:44 +03:00
Andrew Dolgov
c6b7a7f8d0 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-02-21 16:06:57 +03:00
Andrew Dolgov
2ab215daca batch editor: comment out getChildByName 2021-02-21 16:06:46 +03:00
fox
d0efa35d22 Merge pull request 'Open the default feed after unsubscribing' (#5) from wn/tt-rss:bugfix/post-unsubscribe-feed-selection into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/5
2021-02-21 16:04:37 +03:00
Andrew Dolgov
521d0b65c7 batch feed editor: use tab layout, cleanup 2021-02-21 16:02:57 +03:00
wn_
1bd5152c80 Open the default feed after unsubscribing.
Previously the UI appeared to hang, even though the backend request had already completed successfully.
2021-02-21 12:48:15 +00:00
Andrew Dolgov
d1328321be move published OPML endpoint to public.php 2021-02-21 15:16:39 +03:00
Andrew Dolgov
2843b99171 minor filter UI layout fix 2021-02-21 13:08:34 +03:00
Andrew Dolgov
810afdaf5a prevent creation of filter rules matching no feeds 2021-02-21 12:28:25 +03:00
Andrew Dolgov
fb471652c0 Merge branch 'wip-filter-stuff' 2021-02-21 10:35:39 +03:00
Andrew Dolgov
9e56896bd4 Element visible: check for offsetHeight/offsetWidth 2021-02-21 10:34:28 +03:00
Andrew Dolgov
3b8d69206c deal with filter actions UI 2021-02-21 10:28:59 +03:00
Andrew Dolgov
94560132dd for the most part, deal with filter rules UI 2021-02-21 09:35:07 +03:00
Andrew Dolgov
b4e96374bc more filter stuff 2021-02-20 21:48:05 +03:00
Andrew Dolgov
da97b29dbe prevent filter selected text dialog from opening in wrong order 2021-02-20 21:07:28 +03:00
Andrew Dolgov
590b1fc39e a few more methods shuffled around 2021-02-20 18:21:36 +03:00
Andrew Dolgov
be91355c20 first for filter frontend overhaul 2021-02-20 18:15:08 +03:00
Andrew Dolgov
d6de021ae6 haven't i fixed this already 2021-02-20 13:52:02 +03:00
Andrew Dolgov
39be169f0b also disable Article.completeTags 2021-02-20 13:39:17 +03:00
Andrew Dolgov
5c7416458f rpc: disable completeLabels for now 2021-02-20 13:37:21 +03:00
Andrew Dolgov
22fe9b54d2 feed editor: use client dialog 2021-02-20 13:32:09 +03:00
Andrew Dolgov
9586c72a17 wip: feed editor client-side 2021-02-20 10:26:09 +03:00
Andrew Dolgov
545bcc3e4b bookmarklets: cleanup some more markup 2021-02-20 08:49:40 +03:00
fox
b8786215dc Merge pull request 'Fix an undefined array key warning in 'catchupFeed'.' (#4) from wn/tt-rss:rpc-catchupfeed-warning into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/4
2021-02-20 07:24:44 +03:00
wn_
ce3e1756b3 Fix an undefined array key warning in 'catchupFeed'. 2021-02-19 21:46:30 +00:00
Andrew Dolgov
053b262aa7 rename public.php/cached_url to cached 2021-02-19 20:28:15 +03:00
Andrew Dolgov
fc0ebf0891 move bookmarklet-related methods out of public.php into the plugin 2021-02-19 20:21:36 +03:00
Andrew Dolgov
c9ccb0791d fix for startup crash because of classes containing spaces 2021-02-19 20:05:56 +03:00
Andrew Dolgov
cf249d7e8c modify classname helpers to use element.classList; fix feed debugger & share--get 2021-02-19 19:29:43 +03:00
Andrew Dolgov
d5f4979831 headlines.select: maybe fix another one 2021-02-19 18:50:02 +03:00
Andrew Dolgov
5cec4eb015 af_readability: fix selector 2021-02-19 18:47:50 +03:00
Andrew Dolgov
760a26e484 fix height of dijit input boxes embedded into toolbars 2021-02-19 18:14:05 +03:00
Andrew Dolgov
737cffc241 render feed icon markup on the client 2021-02-19 17:40:11 +03:00
Andrew Dolgov
d445530fa0 format note on the client 2021-02-19 17:15:22 +03:00
Andrew Dolgov
4fa8450d38 setArticleTags: always return tags from the db 2021-02-19 15:50:42 +03:00
Andrew Dolgov
921b5ca2ce add onTagsUpdated similar to onLabelsUpdated 2021-02-19 15:34:28 +03:00
Andrew Dolgov
e73779fec1 render tags on the client 2021-02-19 15:31:50 +03:00
Andrew Dolgov
d9fe14a012 use template strings in a bunch of places instead of id concatenation 2021-02-19 15:09:53 +03:00
Andrew Dolgov
131f34648d render headline labels on the client 2021-02-19 15:03:48 +03:00
Andrew Dolgov
660a1bbe01 * switch to xhr.post() almost everywhere
* call App.handlerpcjson() automatically on json request (if possible)
 * show net/log indicators in prefs
2021-02-19 13:44:56 +03:00
Andrew Dolgov
bb4e4282f4 migrate a bunch of xhrPost invocations 2021-02-19 11:28:14 +03:00
Andrew Dolgov
6b43b788d9 migrate xhrJson invocations to the new helper 2021-02-19 10:22:00 +03:00
Andrew Dolgov
dba6dce3b3 add element fadeout/fadein and a shorter xhr helper 2021-02-19 10:15:36 +03:00
Andrew Dolgov
f645120641 table helpers: don't try to iterate over a single element 2021-02-19 07:54:44 +03:00
Andrew Dolgov
d26269865f use .closest() instead of .up() to lookup parent by selector 2021-02-19 07:43:05 +03:00
Andrew Dolgov
bec35200e9 fix some eslint-related stuff 2021-02-19 07:29:21 +03:00
Andrew Dolgov
0832dd9d40 fix eslint configuration 2021-02-19 07:07:45 +03:00
Andrew Dolgov
00310d2d23 cleanup some unused code, fix App.byId() invoked by wrong name 2021-02-19 06:58:50 +03:00
Andrew Dolgov
dcfea9baac properly validate feed editor dialog 2021-02-19 06:51:15 +03:00
Andrew Dolgov
d57e7eaa98 move stuff in common.js around a bit 2021-02-19 06:40:35 +03:00
Andrew Dolgov
5475eed452 bring back hash functions 2021-02-19 06:35:37 +03:00
Andrew Dolgov
b6c3dde1cc add $/423 shims 2021-02-18 22:26:00 +03:00
Andrew Dolgov
c088e9d9d8 get rid of a few more prototype-isms 2021-02-18 22:23:06 +03:00
Andrew Dolgov
89fd9ec8c3 compat shim fixes 2021-02-18 22:15:54 +03:00
Andrew Dolgov
e61e7c8356 compat shim fixes 2021-02-18 22:14:40 +03:00
Andrew Dolgov
f77c17c6f0 add Element toggleClassName 2021-02-18 22:05:06 +03:00
Andrew Dolgov
70fa423026 initial for RIP prototype/scriptaculous 2021-02-18 21:51:18 +03:00
Andrew Dolgov
0b6a71f8ea Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-02-18 18:12:46 +03:00
Andrew Dolgov
049c423454 fix hotkey help toolbar action 2021-02-18 18:12:30 +03:00
Andrew Dolgov
839cb2cd21 rebase translations 2021-02-18 16:56:04 +03:00
Andrew Dolgov
61fdce4f44 rework previous to be even less jumpy 2021-02-18 15:40:54 +03:00
Andrew Dolgov
2c5927d8cd rework previous to be less jumpy 2021-02-18 15:38:26 +03:00
Andrew Dolgov
2e4b403787 * use es5 (?) default parameter values for some functions
* when moving to next article, try to show hsp if its next
2021-02-18 15:13:41 +03:00
Andrew Dolgov
bed36cbf9f af_psql_trgm: cleanup 2021-02-18 13:41:40 +03:00
Andrew Dolgov
a2c75257f1 bookmarklets: cleanup 2021-02-18 13:16:55 +03:00
Andrew Dolgov
75435aa960 user details: cleanup 2021-02-18 13:00:20 +03:00
Andrew Dolgov
d8a99ce06a remove unneeded headings 2021-02-18 12:45:31 +03:00
Andrew Dolgov
39c0fe3697 shorten many invocations of Ajax.Request in inline form methods 2021-02-18 12:27:26 +03:00
Andrew Dolgov
ee0b66b6bd af_proxy_http: markup cleanup 2021-02-18 12:13:13 +03:00
Andrew Dolgov
e03d6379a6 small markup adjustment 2021-02-18 11:55:00 +03:00
Andrew Dolgov
466cba39d8 Merge branch 'master' of git.fakecake.org:fox/tt-rss 2021-02-18 11:54:29 +03:00
Andrew Dolgov
1adb9bb6b6 profiles: use client dialog; move related methods to pref-prefs 2021-02-18 11:54:22 +03:00
Andrew Dolgov
b888bc2091 cache_starred_images: don't try to use undefined array index 2021-02-17 21:54:43 +03:00
Andrew Dolgov
e4609c18ef * add (disabled) shortcut syntax for plugin methods
* add controls shortcut for pluginhandler tags
 * add similar shortcut for frontend
 * allow plugins to selectively exclude their methods from CSRF checking
2021-02-17 21:44:21 +03:00
Andrew Dolgov
b16abc157e * App: rename hidden to hidden_tag
* search: use client dialog
 * add some form field helpers
2021-02-17 19:34:54 +03:00
Andrew Dolgov
92cb91e2e2 search dialog: bring back id of language dropdown 2021-02-17 16:33:28 +03:00
Andrew Dolgov
35b6d63289 af_proxy_http: don't try to proxy back to ourselves 2021-02-17 16:27:52 +03:00
Andrew Dolgov
6ecee2abbd cache_starred_images: minor fixes 2021-02-17 16:17:05 +03:00
Andrew Dolgov
ea37d05a83 delete unused mail .pngs 2021-02-17 15:53:58 +03:00
Andrew Dolgov
2ac6508fe6 mail, mailto: cleanup markup 2021-02-17 15:53:00 +03:00
Andrew Dolgov
7be1e3ed38 pluginhandler: reject method requests without CSRF 2021-02-17 15:04:39 +03:00
Andrew Dolgov
2b2833bb4f plugins: load dialogs via xhr instead of http 2021-02-17 14:56:36 +03:00
Andrew Dolgov
4632d6cf55 fix some php8 warnings 2021-02-17 14:14:17 +03:00
Andrew Dolgov
e9c3118ddd don't show E_USER_DEPRECATED on the frontpage 2021-02-17 14:14:10 +03:00
Andrew Dolgov
538f87e415 af_psql_trgm: don't load dialog via http 2021-02-17 14:08:06 +03:00
Andrew Dolgov
d439685895 pluginhandlers: post notice if pluginmethod is requested without CSRF token 2021-02-17 14:05:12 +03:00
Andrew Dolgov
00b31c3f53 af_readability: cleanup markup 2021-02-17 13:55:58 +03:00
Andrew Dolgov
3c14eed1c2 close_button: fix color not applying 2021-02-17 13:45:38 +03:00
Andrew Dolgov
35b6a88146 RIP af_tumblr_1280 2021-02-17 13:36:24 +03:00
Andrew Dolgov
7587f2cdc6 af_redditimgur: cleanup markup 2021-02-17 13:35:10 +03:00
Andrew Dolgov
91049335eb af_readability: cleanup markup 2021-02-17 12:36:02 +03:00
Andrew Dolgov
9ac6741d24 af_comics: markup cleanup 2021-02-17 12:25:33 +03:00
Andrew Dolgov
4325c30a3f share: markup cleanup 2021-02-17 12:10:19 +03:00
Andrew Dolgov
273ada7353 * implement shortcut syntax for exposed plugin methods
* move shared article rendering code to share plugin
2021-02-17 09:59:14 +03:00
Andrew Dolgov
7adcada324 share plugin: cleanup, fix icon not highlighting properly 2021-02-17 08:52:39 +03:00
Andrew Dolgov
0fc783e2b3 cleanup markup in some plugins, make nsfw generate dijit widgets 2021-02-16 22:07:37 +03:00
Andrew Dolgov
89e8176c69 Article.render: parse dojo widgets 2021-02-16 22:05:12 +03:00
Andrew Dolgov
91e7969383 replace a few more controls to new style 2021-02-16 18:57:06 +03:00
Andrew Dolgov
24c79d91c2 controls_compat: comment out most of them 2021-02-16 18:53:56 +03:00
Andrew Dolgov
f58c49beaa replace a few more controls to new style 2021-02-16 18:50:18 +03:00
Andrew Dolgov
bf88c64d1e fix floicon not imported from global namespace 2021-02-16 18:14:57 +03:00
Andrew Dolgov
9d7ba773ec move session-related functions to their own namespace 2021-02-16 17:13:16 +03:00
Andrew Dolgov
7fad6ce651 move rgb/hsl functions to their own namespace 2021-02-16 17:07:23 +03:00
Andrew Dolgov
bdbbdbb0ed rework controls to accept parameters as array 2021-02-16 16:59:21 +03:00
Andrew Dolgov
627af2c236 amend previous to fix actual underlying problem (double escaping) 2021-02-16 15:36:40 +03:00
Andrew Dolgov
4f4e57bb26 hidden_tag: temporarily prevent htmlspecialchars() to stop embedded JSON from breaking 2021-02-16 15:27:22 +03:00
Andrew Dolgov
1f5d81b77c use a few more control helpers for checkboxes 2021-02-16 15:19:42 +03:00
Andrew Dolgov
af4b3e7df0 login form: use control helpers 2021-02-16 15:05:32 +03:00
Andrew Dolgov
22fc6871e8 remove backend helper and move its only function to rpc for the time being 2021-02-16 14:51:42 +03:00
Andrew Dolgov
d7127cead3 feed debugger: use hidden helpers; add button helpers 2021-02-16 14:42:27 +03:00
Andrew Dolgov
1f43d7916c replace print_hidden with hidden_tag 2021-02-16 14:32:06 +03:00
Andrew Dolgov
26d6b84a57 add namespaced controls with unified naming; deprecated old-style control shortcuts 2021-02-16 14:23:00 +03:00
Andrew Dolgov
cb6b3584ce pref-labels: remove unused code 2021-02-16 14:19:06 +03:00
Andrew Dolgov
3887665bcb CommonDialogs.addLabel: remove long unused parameters 2021-02-16 14:13:38 +03:00
Andrew Dolgov
cca84aedfd _format_enclosures: always return entries array 2021-02-16 10:18:50 +03:00
Andrew Dolgov
88f7c4f1a5 feeds/view: fix php8 warning 2021-02-16 10:11:58 +03:00
Andrew Dolgov
6e06fe2885 shorten_expanded: fix for posts without attachments 2021-02-16 08:31:24 +03:00
Andrew Dolgov
5c4223992f db-prefs: minor cleanup, add warnings if unknown prefs are requested 2021-02-15 22:01:11 +03:00
Andrew Dolgov
70e293bccb pref-filters: fix some warnings 2021-02-15 17:07:50 +03:00
Andrew Dolgov
d4157b9e4e counters: just merge everything at once 2021-02-15 17:01:05 +03:00
Andrew Dolgov
39604bedef move reset_password to UserHelper 2021-02-15 16:59:54 +03:00
Andrew Dolgov
5d42ce553f drop legacy DB interface and related sanity checks 2021-02-15 16:55:55 +03:00
Andrew Dolgov
9f55454f63 remove the rest of db.php; rename some leftover methods in feeds 2021-02-15 16:51:35 +03:00
Andrew Dolgov
bd3c38de84 move bookmarklet-related subscribe_to_feed_url to bookmarklet plugin 2021-02-15 16:41:52 +03:00
Andrew Dolgov
cfad740c99 drop legacy db_ functions wrapper 2021-02-15 16:38:18 +03:00
Andrew Dolgov
91285e3868 router: add additional logging for refused requests; reject requests for methods starting with _ 2021-02-15 16:34:44 +03:00
Andrew Dolgov
d1c83fad14 api: unify naming 2021-02-15 16:18:17 +03:00
Andrew Dolgov
71f2f4288f counters: one more 2021-02-15 16:14:48 +03:00
Andrew Dolgov
6426ae559a dbupdater: unify naming 2021-02-15 16:14:00 +03:00
Andrew Dolgov
166f2d4666 diskcache: unify naming 2021-02-15 16:11:30 +03:00
Andrew Dolgov
8e79f1717d prefs: unify naming 2021-02-15 16:07:22 +03:00
Andrew Dolgov
5704deb460 counters: unify naming 2021-02-15 16:00:54 +03:00
Andrew Dolgov
257efb43c6 article: unify naming 2021-02-15 15:52:28 +03:00
Andrew Dolgov
020f062a76 feeds: unify naming 2021-02-15 15:43:07 +03:00
Andrew Dolgov
6b006a18e7 subscribe to feed: use client dialog 2021-02-15 15:21:41 +03:00
Andrew Dolgov
ecb36b6354 edit tags: use client dialog 2021-02-15 14:50:40 +03:00
Dario Di Ludovico
8b022c2bfb Translated using Weblate (Italian)
Currently translated at 100.0% (705 of 705 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2021-02-15 11:24:55 +00:00
Andrew Dolgov
82adb01307 render enclosures on the client 2021-02-15 14:10:46 +03:00
fox
916c21fe60 Merge pull request 'Lazy load image attachments' (#2) from verifiedjoseph/tt-rss:lazy-load-image-attachments into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/2
2021-02-15 11:55:12 +03:00
Andrew Dolgov
868b9b476e api: rewrite article urls at the very end to prevent plugins which expect source URLs from breaking 2021-02-15 09:40:43 +03:00
Andrew Dolgov
52a86c5e38 Revert "api: get flavor image from plugin-processed content"
This reverts commit a4604e892c.
2021-02-15 08:49:12 +03:00
Andrew Dolgov
a4604e892c api: get flavor image from plugin-processed content 2021-02-15 08:28:46 +03:00
Andrew Dolgov
3c584376ca shared opml and feed dialogs: remove unique target element id, move associated methods into dialog 2021-02-15 07:59:11 +03:00
Andrew Dolgov
9f31381bb6 renderToolbar: support empty data i.e. dashboard feed 2021-02-15 07:46:24 +03:00
Andrew Dolgov
a2e688fcb2 render headline-specific toolbar on the client 2021-02-14 22:17:13 +03:00
Joseph
68e2ccb354 Lazy load image attachments 2021-02-14 17:31:01 +00:00
Andrew Dolgov
37a81ba594 SingleUseDialog: destroy existing widget with same id on create 2021-02-14 19:19:25 +03:00
Andrew Dolgov
ff6031d3c9 remove old-style markup from exception dialog 2021-02-14 18:59:09 +03:00
Andrew Dolgov
4996d8ccfe pref-users edit: use client dialog 2021-02-14 16:44:41 +03:00
Andrew Dolgov
0b7377238a add Handler_Administrative 2021-02-14 15:50:46 +03:00
Andrew Dolgov
33ea46c2bc pref-users/add: remove unused variable 2021-02-14 15:42:12 +03:00
Andrew Dolgov
0fbf109912 * remove users/filters toolbar edit button (just click on it)
* fix title of edit filter dialog always showing create filter
2021-02-14 15:38:45 +03:00
Andrew Dolgov
a8cc43a0ff move logout_user() to UserHelper 2021-02-14 15:31:03 +03:00
Andrew Dolgov
2547ece0ca pref-users: cleanup index 2021-02-14 14:59:22 +03:00
Andrew Dolgov
1c7e4782aa prefs system: load phpinfo using inline method 2021-02-14 12:29:08 +03:00
Andrew Dolgov
6b5c9c781b pref prefs: load secondary tabs when needed 2021-02-14 12:25:41 +03:00
Andrew Dolgov
e5cedc7d5f appPasswordList: markup cleanup 2021-02-14 11:39:26 +03:00
Andrew Dolgov
8e75551f95 pref prefs: split index into manageable chunks 2021-02-14 11:29:38 +03:00
Andrew Dolgov
15fd23c374 use shortcut echo syntax for php templates 2021-02-14 09:15:51 +03:00
Ptsa Daniel
ce1831e2be Translated using Weblate (Chinese (Simplified))
Currently translated at 99.4% (701 of 705 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2021-02-14 03:24:42 +00:00
Andrew Dolgov
d4c925819b pref-feeds: load error button via xhr 2021-02-13 23:12:49 +03:00
Andrew Dolgov
43d8a1f2ff remove getinactivefeeds (duplicate functionality) 2021-02-13 23:08:20 +03:00
Andrew Dolgov
103d30ad3f batch subscribe: use client dialog 2021-02-13 22:16:17 +03:00
Andrew Dolgov
c36b2adf84 feeds with errors: use client dialog 2021-02-13 21:57:02 +03:00
Andrew Dolgov
8464c619e4 inactive feeds: use client dialog 2021-02-13 21:41:38 +03:00
Andrew Dolgov
17413078a7 pref feeds: index cleanup, split into several methods, use tabs to maximize space for feed tree, persist feed tree state 2021-02-13 18:32:02 +03:00
Andrew Dolgov
9684ce5c4b minor fixes re: previous 2021-02-13 16:07:52 +03:00
Andrew Dolgov
b112198991 pref filters index: markup cleanup 2021-02-13 14:05:25 +03:00
Andrew Dolgov
5127c29297 prefs system: markup cleanup 2021-02-13 13:50:53 +03:00
Andrew Dolgov
aa63014073 pref-labels index: use cleaner markup 2021-02-13 13:37:57 +03:00
Andrew Dolgov
46f6d7c11a pref-labels/index: cleanup 2021-02-13 13:26:17 +03:00
Andrew Dolgov
e7924c6dac label editor: use client dialog 2021-02-13 13:17:34 +03:00
Andrew Dolgov
0b71729bd3 Merge branch 'weblate-integration' 2021-02-13 11:13:34 +03:00
Andrew Dolgov
eec5871f5f fail better if requested article URL is blank 2021-02-13 10:10:44 +03:00
Glandos
e2c7166719 Translated using Weblate (French)
Currently translated at 98.7% (696 of 705 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fr/
2021-02-12 22:53:34 +00:00
Andrew Dolgov
d3940b6259 fix a bunch of warnings related to generated feeds 2021-02-12 22:00:33 +03:00
Andrew Dolgov
481bd76100 pref helpers: move some methods to their own sections 2021-02-12 21:51:32 +03:00
Andrew Dolgov
6af83e3881 drop ENABLE_GZIP_OUTPUT; system prefs: load php info only if needed 2021-02-12 21:43:38 +03:00
Andrew Dolgov
e6624cf631 fix a few more session-related warnings 2021-02-12 21:24:49 +03:00
Andrew Dolgov
119a4226d8 validate_csrf: remove warning 2021-02-12 21:21:23 +03:00
Andrew Dolgov
f2d3cba231 add HTTP_ACCEPT_LANGUAGE handling for php8 2021-02-12 21:20:04 +03:00
Weblate
d02872983d Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/
2021-02-12 16:57:31 +00:00
Andrew Dolgov
6365bf39d9 rebase translations 2021-02-12 19:56:51 +03:00
Andrew Dolgov
6d7fea537e silence some more eslint warnings 2021-02-12 19:55:05 +03:00
Andrew Dolgov
157675d9fd prefs: fix published shared URL dialog 2021-02-12 19:17:50 +03:00
Andrew Dolgov
7f0800537e silence (or fix) a bunch of eslint warnings 2021-02-12 19:02:09 +03:00
Andrew Dolgov
ad7842c98a RIP tag cloud: last of the vanilla popup dialog system 2021-02-12 18:43:30 +03:00
Andrew Dolgov
9330bde991 batchsubscribe: xhr 2021-02-12 18:40:22 +03:00
Andrew Dolgov
03b85248e6 move some dialogs to xhr loading 2021-02-12 18:38:26 +03:00
Andrew Dolgov
71dfc83466 force _ENABLED_PLUGINS to string when passed to pluginhost 2021-02-12 17:20:37 +03:00
Andrew Dolgov
1f2ba932b8 RIP easy-installer 2021-02-12 15:59:19 +03:00
Andrew Dolgov
d23a261b92 RIP self-registration 2021-02-12 15:57:43 +03:00
Andrew Dolgov
3268364693 more dialog-related cleanup 2021-02-12 15:50:06 +03:00
Andrew Dolgov
3d11c61f32 * OPML import: don't reload everything, just feed tree
* dialogs: use auto-destroying dialog for almost all dialogs instead of destroying them manually
* some general dialog-related cleanup
2021-02-12 15:22:10 +03:00
Andrew Dolgov
219cc9a0ab fix previous: secondary dialog not opening because of onLoad 2021-02-12 14:35:10 +03:00
Andrew Dolgov
8f8675a26a * filters: remove duplicate code, overall cleanup
* check if some tres exist before trying to reload them
2021-02-12 14:31:36 +03:00
Andrew Dolgov
699186f430 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-02-12 14:08:34 +03:00
fox
a718b692a0 Merge pull request 'Add defaults to api.php variables' (#1) from klempin/tt-rss:fix/undefined-content into master
Reviewed-on: https://git.tt-rss.org/fox/tt-rss/pulls/1
2021-02-12 13:25:24 +03:00
Philip Klempin
ace19c0790 Add defaults to api.php variables 2021-02-12 10:59:30 +01:00
Andrew Dolgov
0f7af07c6e edit filter dialog: cleanup 2021-02-12 12:12:47 +03:00
Andrew Dolgov
9804a17b79 fix typo 2021-02-12 12:12:31 +03:00
Andrew Dolgov
a72171f8ef dialogOf: deal with both raw DOM elements and widgets 2021-02-12 12:12:12 +03:00
Andrew Dolgov
20fb056323 remove customizecss from csrf-ignored methods 2021-02-12 10:37:14 +03:00
Andrew Dolgov
bf6d0f2817 various dialog-related fixes; stop referring to many dialogs by name; move filter test initial dialog to client side 2021-02-12 10:35:13 +03:00
Andrew Dolgov
72e38bfe1f rework a few more dialogs to use App.dialogOf() 2021-02-12 09:56:27 +03:00
Andrew Dolgov
d466284fab * customizeCSS: client dialog
* remove hardcoded width from most dialogs (move to css)
* add helper to easily get dialog from its widget
* rework some dialog buttons to use current object instead of calling dialog by name
2021-02-12 09:02:44 +03:00
Andrew Dolgov
cb7c075cd2 remove OPML.onImportComplete 2021-02-12 08:22:39 +03:00
Andrew Dolgov
83b0738b04 opml import: no more iframe, use client dialog 2021-02-12 08:22:00 +03:00
Andrew Dolgov
3134d71b8f fix typo introduced by 4182018cb7 2021-02-12 08:15:30 +03:00
Andrew Dolgov
eac7ad5d34 remove explainError server-side dlg 2021-02-12 08:00:25 +03:00
Andrew Dolgov
4182018cb7 generated feed: use client dialog 2021-02-11 22:04:39 +03:00
Andrew Dolgov
1a680d4eae publishedOPML: use client dialog 2021-02-11 21:42:38 +03:00
Andrew Dolgov
848bc57f29 disable themes in safe mode; rework safe mode warning/login prompt 2021-02-11 21:19:57 +03:00
Andrew Dolgov
74986d1ac6 shorten pref tab names; make log-alert clickable 2021-02-11 15:49:32 +03:00
Andrew Dolgov
cc646790fd format_backtrace: don't try to use resources as strings 2021-02-11 10:29:42 +03:00
Andrew Dolgov
09e9f34bb4 add UserHelper::find_user_by_login() and rewrite some user checks to invoke it instead of going through PDO 2021-02-11 10:22:27 +03:00
Andrew Dolgov
7af8744c85 authentication: make logins case-insensitive (force lowercase) 2021-02-11 09:57:57 +03:00
Andrew Dolgov
e7e73193fe fix warning in profile edit dialog (2) 2021-02-10 22:06:01 +03:00
Andrew Dolgov
2505ae43a9 fix warning in profile edit dialog 2021-02-10 22:03:08 +03:00
Andrew Dolgov
9e1459d5db pref/prefs: fix warning when in non-default profile 2021-02-10 21:40:43 +03:00
Andrew Dolgov
72edab5f1c close_button: fix warning 2021-02-10 21:40:31 +03:00
Andrew Dolgov
7833760fa0 make feed/cat nested dropdowns a bit more readable 2021-02-10 08:58:31 +03:00
Andrew Dolgov
d630a92c40 fix 2 warnings in feed editor 2021-02-09 15:04:01 +03:00
Andrew Dolgov
2f8efab275 api: one more php8 warning 2021-02-09 12:04:59 +03:00
Andrew Dolgov
a5819569f2 pluginhost: a few more warnings and type hints 2021-02-09 10:20:58 +03:00
Andrew Dolgov
6a25bc53ef api: pass hook object payload by reference 2021-02-09 08:57:23 +03:00
Andrew Dolgov
3655e7aaf1 api: fix some php8 warnings (4) 2021-02-09 08:50:51 +03:00
Andrew Dolgov
aba028a375 api: fix some php8 warnings (3) 2021-02-09 08:47:41 +03:00
Andrew Dolgov
f6f0f21664 make ARTICLE_KIND_ constants class members 2021-02-09 08:24:46 +03:00
Andrew Dolgov
0871a51cb4 api: fix some php8 warnings (2) 2021-02-09 08:16:04 +03:00
Andrew Dolgov
63a90d26f3 api: fix some php8 warnings 2021-02-09 08:15:07 +03:00
Andrew Dolgov
7ae0e8d9c5 rewrite some more hooks in classes/feeds 2021-02-08 23:10:22 +03:00
Andrew Dolgov
345dbb3521 rewrite some more hooks 2021-02-08 22:46:01 +03:00
Andrew Dolgov
6c8ccd2acc front page log checker: filter out idiotic GD warning 2021-02-08 22:15:35 +03:00
Andrew Dolgov
9f3de2d24c login: fix profile warning 2021-02-08 22:03:27 +03:00
Andrew Dolgov
07408ac222 opml: normalize class name 2021-02-08 21:38:26 +03:00
Andrew Dolgov
d91eae9c7e pluginhost: add some type hints 2021-02-08 21:38:09 +03:00
Andrew Dolgov
7eb860af61 even more hooks 2021-02-08 21:28:09 +03:00
Andrew Dolgov
6e57fd77af db: add type hints 2021-02-08 21:11:56 +03:00
Andrew Dolgov
a14873d5b4 more hooks, also add type hint for PluginHost::getInstance() 2021-02-08 21:06:14 +03:00
Andrew Dolgov
54bbd08f38 some more hooks 2021-02-08 20:45:11 +03:00
Andrew Dolgov
ca4c93c6b9 pluginhost: note hook function prototypes 2021-02-08 20:20:24 +03:00
Andrew Dolgov
7874f6ac58 remove PHPMD.UnusedFormalParameter 2021-02-08 19:42:10 +03:00
Andrew Dolgov
a341a838b1 pluginhost: deny hook registration to plugins which lack relevant implementation methods 2021-02-08 19:16:53 +03:00
Andrew Dolgov
51d2deeea9 fix hierarchy of authentication modules, make everything extend Auth_Base and implement hook_auth_user() for pluginhost 2021-02-08 19:11:31 +03:00
Andrew Dolgov
fc2e0bf67b log viewer: disable previous page on page 1 2021-02-08 17:05:50 +03:00
Andrew Dolgov
fa2ebcd0a2 api: rewrite a few more hooks 2021-02-08 17:03:34 +03:00
Andrew Dolgov
363b3629a4 rewrite a few more hooks 2021-02-08 16:52:47 +03:00
Andrew Dolgov
3b52cea811 move some old-style handlers to new callback ones 2021-02-08 16:14:48 +03:00
Andrew Dolgov
1d5c8ee500 prefs: fix user plugins shown by incorrect criteria 2021-02-08 15:41:15 +03:00
Andrew Dolgov
1eb1629d9e pluginhost: rework run_hooks() to be shorter, add callback variant; implement exception handling for both 2021-02-08 14:24:45 +03:00
Andrew Dolgov
20b56b5b23 pluginhost: catch errors while loading plugin source code 2021-02-08 12:14:12 +03:00
Andrew Dolgov
4165834f80 pluginhost: catch fatal errors in plugin init 2021-02-08 12:10:25 +03:00
Andrew Dolgov
9de26d44da af_psql_trgm: fix warning 2021-02-08 11:47:41 +03:00
Andrew Dolgov
d293cbd5a9 fix several warnings related to feed editor 2021-02-08 11:46:43 +03:00
Andrew Dolgov
43abc183ab add phpstan dummy file 2021-02-08 10:31:44 +03:00
Andrew Dolgov
0a788da2d2 dlg: fix unset param warning 2021-02-08 09:00:29 +03:00
Andrew Dolgov
3aada04c7f Merge branch 'master' of git.fakecake.org:fox/tt-rss 2021-02-08 08:52:43 +03:00
Andrew Dolgov
942afb43a1 sanity checks: use better CLI detection, shorten most of the text 2021-02-08 08:49:21 +03:00
Andrew Dolgov
5d0f65358f revert jimIcon stuff 2021-02-08 08:33:37 +03:00
Andrew Dolgov
3ad820e083 oops, remove unneeded warnings 2021-02-08 08:31:06 +03:00
Andrew Dolgov
479da5aa86 jimIcon: hide GD warning 2021-02-08 08:30:04 +03:00
Andrew Dolgov
3f972f8fed public/subscribe: fix warnings 2021-02-08 08:20:30 +03:00
Andrew Dolgov
983a874ddd bookmarklet: encode URL properly so special characters won't get lost 2021-02-07 21:09:57 +03:00
Andrew Dolgov
c1ad7acfb9 bookmarklet: encode URL properly so special characters won't get lost 2021-02-07 21:09:27 +03:00
Andrew Dolgov
41fc03287e fix even more warnings reported by phpstan 2021-02-06 17:56:47 +03:00
Andrew Dolgov
c94f1b6ff8 fix some more warnings reported by phpstan 2021-02-06 17:38:24 +03:00
Andrew Dolgov
b6e1a5c91a fix several warnings reported by phpstan 2021-02-06 17:19:07 +03:00
Andrew Dolgov
ce2335deaf pref-users: css fixes 2021-02-06 16:24:40 +03:00
Andrew Dolgov
d8de10d78a error log: fix severity dropdown 2021-02-06 16:16:43 +03:00
Andrew Dolgov
73e697a0df fix some warnings in prefs (filters, users) 2021-02-06 16:13:11 +03:00
Andrew Dolgov
73070544ca error log: make it more readable 2021-02-06 16:11:29 +03:00
Andrew Dolgov
5cfc5914f2 log viewer: show total pages 2021-02-06 15:33:19 +03:00
Andrew Dolgov
5849a39820 af_redditimgur: don't try to load empty html; fix a warning in update debugger 2021-02-06 10:31:06 +03:00
Andrew Dolgov
ce489a724b fix a few more warnings 2021-02-06 10:23:45 +03:00
Andrew Dolgov
10392ecc28 event log: add pagination 2021-02-06 10:10:54 +03:00
Andrew Dolgov
9fdeb58fd3 check a few more php8 warnings 2021-02-06 09:51:28 +03:00
Andrew Dolgov
8b39e6bca7 _color_pack: define variable before using 2021-02-06 09:29:31 +03:00
Andrew Dolgov
a544123b59 fix clean() for arrays and user plugin list 2021-02-06 00:17:41 +03:00
Andrew Dolgov
6e774a58fe more php8 fixes mostly related to login 2021-02-06 00:12:15 +03:00
Andrew Dolgov
403dca154c initial WIP for php8; bump php version requirement to 7.0 2021-02-05 23:41:32 +03:00
Andrew Dolgov
b4cbc792cc add workaround for gulp4 not updating file timestamps on build 2021-02-04 15:30:28 +03:00
Andrew Dolgov
6d8f2221b8 Merge branch 'weblate-integration' 2021-01-29 11:52:21 +03:00
Eike
505cbdd82e Translated using Weblate (German)
Currently translated at 100.0% (729 of 729 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2021-01-28 18:19:10 +00:00
Andrew Dolgov
eb896f824d night theme: disable on-hover undimming 2021-01-28 07:14:13 +03:00
Andrew Dolgov
927df33d49 night theme: dim images unless hovered over 2021-01-27 21:32:00 +03:00
Andrew Dolgov
64f7ac0e74 dark theme: fix color of .dijitSplitterHover 2021-01-27 16:53:24 +03:00
fox
607ecab31e Update 'CONTRIBUTING.md' 2021-01-24 09:30:22 +03:00
fox
1507b051fd update contributing.md due to gitea changes 2021-01-24 09:25:44 +03:00
Andrew Dolgov
6c546f37ba af_redditimgur: handle youtube /embed/ URLs 2021-01-23 08:57:36 +03:00
Andrew Dolgov
43e9dd5ea9 feed debugger: wrap long lines 2021-01-22 15:58:26 +03:00
Andrew Dolgov
b30b354b53 af_redditimgur: add some last minute handling for generic preview media URLs provided in JSON 2021-01-22 15:44:44 +03:00
Andrew Dolgov
0d1336bd29 af_redditimgur:
* draw a basic form for testurl() if no url is given
 * only process specific JSON media files/child elements until something is found
 * handle generic preview images for self posts (not link posts because
link is handled afterwards)
2021-01-21 08:28:55 +03:00
Andrew Dolgov
1ded706f8f af_redditimgur: cleanup, rework to embed stuff from reddit-provided JSON first 2021-01-19 22:21:57 +03:00
Andrew Dolgov
2933483393 add a hack (Headlines.unpackVisible) to workaround against unpack observer sometimes missing articles 2021-01-19 11:54:13 +03:00
Andrew Dolgov
41bde84a92 af_redditimgur: add basic support for reddit galleries 2021-01-18 15:34:05 +03:00
Andrew Dolgov
4e95591087 af_redditimgur: shorten href stuff 2021-01-18 14:46:08 +03:00
Andrew Dolgov
da0ad82c24 Archive cleanup:
- remove code to manually archive/unarchive articles
- remove ttrss_archived_feeds/orig_feed_id handling - the whole thing was implemented for
this data to be kept indefinitely; it doesn't make a lot of sense to deal with this stuff
now that it is expired after one month anyway (same reasons as feed browser being removed - privacy)
- remove "originally from"-related stuff because of the above
- also remove unused remaining frontend/backend code related to feed browser (rip)
2021-01-17 14:55:11 +03:00
Andrew Dolgov
6c13449088 remove CommonDialogs.feedBrowser() 2021-01-17 14:34:04 +03:00
Andrew Dolgov
25520e9784 Select... dropdown: replace dijit Select with DropDownButton, simplify layout
PluginHost: add HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM
Headlines.onActionChanged: removed
2021-01-17 11:27:07 +03:00
Andrew Dolgov
7a2ad08a7d scored_oldest_first: update sort caption 2021-01-17 10:50:40 +03:00
Andrew Dolgov
c82457e534 add plugins/scored_oldest_first 2021-01-17 10:47:37 +03:00
Andrew Dolgov
bc0d50e892 remove show as feed from Select dropdown in main toolbar 2021-01-17 10:43:29 +03:00
Andrew Dolgov
b2993bcd30 remove menu options to manually un/archive articles 2021-01-17 10:37:40 +03:00
Andrew Dolgov
78ed64932f Merge branch 'weblate-integration' 2021-01-15 08:41:00 +03:00
Andrew Dolgov
ee4b7bebe8 pluginhost: load_data: check schema last 2021-01-15 08:35:05 +03:00
Andrew Dolgov
3d32a5f755 Merge branch 'master' of git.fakecake.org:tt-rss 2021-01-15 08:32:17 +03:00
Andrew Dolgov
40f38fc87f pluginhost: load plugin data automatically (also marks load_data method as private) 2021-01-15 08:32:06 +03:00
Andrew Dolgov
6311fb607d pre: set white-space: pre-wrap to remove horizontal scrolling 2021-01-13 13:38:51 +03:00
Andrew Dolgov
f67f0f864b HOOK_ARTICLE_EXPORT_FEED: also pass owner_uid 2021-01-11 22:52:31 +03:00
Dario Di Ludovico
ff194fadea Translated using Weblate (Italian)
Currently translated at 100.0% (729 of 729 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2021-01-11 10:21:39 +00:00
Andrew Dolgov
d1e8042cf3 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2021-01-11 12:26:07 +03:00
Andrew Dolgov
6d4005f984 af_psql_trgm:
1. better debugging output
2. fix incorrect default values being used sometimes
3. remove special workaround for equal titles because trgm extension
seems to be working properly for those now (tested on postgres 11)
4. code cleanup
2021-01-11 12:23:46 +03:00
fox
8cf8db8456 Merge branch 'inc-tags' of JustAMacUser/tt-rss into master 2021-01-10 08:28:24 +00:00
JustAMacUser
fadf4dec96 Include tags for HOOK_ARTICLE_EXPORT_FEED. 2021-01-10 03:23:16 -05:00
Ptsa Daniel
17bc1de49e Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (728 of 729 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2021-01-09 04:19:32 +00:00
Weblate
219b493550 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/
2021-01-07 15:22:41 +00:00
Andrew Dolgov
8e1e9ec2e3 rebase translations 2021-01-07 18:21:29 +03:00
Andrew Dolgov
6ed6b9e120 Merge branch 'weblate-integration' 2021-01-07 18:20:54 +03:00
Andrew Dolgov
dcad60284c translations: rebase, add T_nsprintf 2021-01-07 18:18:22 +03:00
Andrew Dolgov
33a5ecd2ce feed editor: show purge interval correctly if FORCE_ARTICLE_PURGE is set 2021-01-07 18:16:42 +03:00
Andrew Dolgov
0868ff9d64 auth_remote: use empty() instead of isset() while checking headers 2021-01-07 11:18:02 +03:00
Andrew Dolgov
dc40f69511 fix auth_remote broken by previous commit 2021-01-05 18:55:05 +03:00
Andrew Dolgov
8a34084df1 auth_remote: rewrite header checking to be more readable 2021-01-05 10:37:30 +03:00
Andrew Dolgov
4e3ef7a4dd get_user_ip: remove REMOTEADDR for the time being 2021-01-05 10:25:43 +03:00
Andrew Dolgov
a8302fb253 use X-Real-IP headers if possible while authenticating 2021-01-05 10:17:24 +03:00
Andrew Dolgov
8764662138 af_redditimgur: also blacklist in-content links 2021-01-03 10:55:57 +03:00
Andrew Dolgov
2abc434e26 daemon: clarify some task-related messages 2020-12-31 10:11:41 +03:00
Andrew Dolgov
8cc07bc8bd event log: add severity filtering 2020-12-24 15:02:47 +03:00
Andrew Dolgov
e86b2e60d3 edit tags dialog: initialize autocomplete in onShow (instead of onLoad) because of xhr 2020-12-23 12:14:11 +03:00
Andrew Dolgov
8de2100cf7 Merge branch 'master' of git.fakecake.org:tt-rss 2020-12-23 12:09:45 +03:00
Andrew Dolgov
57f36f3f97 search dialog: populate current search values onShow instead of onLoad because the dialog is preloaded via xhr 2020-12-23 12:09:34 +03:00
Ptsa Daniel
7282f0bf38 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (716 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2020-12-22 13:23:14 +00:00
fox
d4666d30d2 Merge branch 'master' of Tony/tt-rss into master 2020-12-22 04:58:53 +00:00
Tony
564a24fd78 Add support for HTTP_REMOTE_USER variable for user authentication 2020-12-21 16:56:39 +00:00
Andrew Dolgov
6da576dbe4 BLACKLISTED_TAGS: use textarea for editing; normalize value when saving 2020-12-21 08:50:34 +03:00
Andrew Dolgov
f59c567831 update_rss_feed: fix BLACKLISTED_TAGS not working properly, simplify tag-related code 2020-12-20 23:12:45 +03:00
Andrew Dolgov
5f733604f0 purge_feed: limit debugging to LOG_VERBOSE 2020-12-20 23:11:26 +03:00
Andrew Dolgov
9e62513095 af_redditimgur: also rewrite in the API handler 2020-12-20 13:12:50 +03:00
Andrew Dolgov
b65e07a12b Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2020-12-20 11:28:57 +03:00
Andrew Dolgov
f25ea5355c af_redditimgur: add option to rewrite reddit URLs to teddit.net 2020-12-20 11:28:48 +03:00
fox
82d3c653a7 Merge branch 'bugfix/return-errors' of wn/tt-rss into master 2020-12-18 14:14:04 +00:00
wn_
12435b223e Actually return the array of errors... 2020-12-18 07:53:07 -06:00
Andrew Dolgov
50d089ae59 redditimgur: blacklist github because it usually resolves to a huge profile photo of someone 2020-12-18 08:12:31 +03:00
fox
e48beee7fc Merge branch 'bugfix/php8-vsprintf' of wn/tt-rss into master 2020-12-16 13:58:05 +00:00
wn_
d2db58de4f Switch from 'vsprintf' to 'sprintf' in another place. 2020-12-16 07:55:32 -06:00
fox
ef7e679363 Merge branch 'feature/php8' of wn/tt-rss into master 2020-12-16 11:06:48 +00:00
Andrew Dolgov
b4b2ba99ef purge_feed: shorten one log message 2020-12-15 09:43:59 +03:00
Andrew Dolgov
f05f9b4252 purge_feed: add more debugging output 2020-12-15 08:50:01 +03:00
Andrew Dolgov
9b7338e807 feed editor: properly show global purging interval as disabled 2020-12-15 08:49:43 +03:00
Andrew Dolgov
8aa1b0fed6 purge_intervals global: set '1 week old' to mean 7 days instead of 5 (???) 2020-12-15 08:49:02 +03:00
Andrew Dolgov
83962a8561 feed debugger: allow setting log level to LOG_EXTENDED 2020-12-12 22:17:23 +03:00
wn
62da307ef1 Use correct 'sprintf' function and other minor fixes in Pref_Feeds. 2020-12-12 10:28:55 -06:00
wn
a1f8d6941b Remove duplicate block in 'classes/pref/filters.php'.
Also a minor tweak to getting the search filter.
2020-12-12 10:28:54 -06:00
wn
8c4ca7c8ef Fix some 'isset' checks in 'classes/pref/prefs.php'. 2020-12-12 10:28:53 -06:00
wn
95d0cb4953 Handle potential absence of a URL path in UrlHelper. 2020-12-12 10:28:53 -06:00
wn
c68f2aabc9 Make 'ttrss_error_handler' compatible w/ 8.
2d467abc46/UPGRADING (L43)
2d467abc46/UPGRADING (L63)
2020-12-12 10:28:52 -06:00
wn
936b91a7e6 Don't do deprecated 'libxml_disable_entity_loader(true)' under PHP 8.
2d467abc46/UPGRADING (L886)
2020-12-12 10:28:49 -06:00
wn
6bdf4a1a25 Switch to 'get_error_types()' to ensure availability in 'include/functions.php'.
The global in 'sanity_check()' was null... possibly due to circular requires?
2020-12-12 10:28:48 -06:00
wn
08a6f6bde2 Only do sanity checks for self URL if we can create a valid URL.
'sanity_check.php' gets included in 'update.php' and 'update_daemon2.php', where a Host request header is likely not provided.
2020-12-12 10:28:47 -06:00
wn
75536b4790 Switch to recommended 'default_charset' to fix 'gettext' error.
'mbstring.internal-encoding' was deprecated in 5.6.

https://www.php.net/manual/en/mbstring.configuration.php#ini.mbstring.internal-encoding
https://www.php.net/manual/en/ini.core.php#ini.default-charset
2020-12-12 10:28:46 -06:00
wn
6f31372b37 Address param order deprecation warning for 'af_redditimgur'. 2020-12-12 10:28:45 -06:00
wn
358bcdd881 Fix passing options to plugins in 'update.php'. 2020-12-12 10:28:43 -06:00
Andrew Dolgov
65254f5db4 - move sphinx plugin to a separate repo
- regenerate config checks without sphinx-related variables
2020-12-11 09:48:34 +03:00
Andrew Dolgov
43bd3394c3 shorten_expanded: remove loading=lazy from images if enabled 2020-12-11 09:22:30 +03:00
Andrew Dolgov
71c8d8d365 queryFeedHeadlines:
- there should be no need for DISTINCT query when checking for first id
 - fix DISTINCT query part being undefined when browsing by tags
 - add query debugging for tags
2020-12-08 17:01:19 +03:00
Andrew Dolgov
7608f3d7b0 Merge branch 'master' of git.fakecake.org:tt-rss 2020-12-08 13:55:11 +03:00
Andrew Dolgov
2edfcbbd85 get_article_image: add support for ARTICLE_KIND_ALBUM 2020-12-08 13:54:52 +03:00
Andrew Dolgov
85b788709a setArticleTags: prevent duplicate tags being assigned if called twice
editTagsDlg: prevent dialot from being submitted twice
normalize_categories: filter out empty values that failed validation
2020-12-07 23:35:37 +03:00
Andrew Dolgov
0e4e0e624e viewfeed debugger: open properly for categories 2020-12-07 17:10:36 +03:00
Andrew Dolgov
d06cc8267b queryFeedHeadlines: bring back DISTINCT for a limited set of columns 2020-12-07 16:59:48 +03:00
Andrew Dolgov
e40b79ab33 get_article_image: return basic kind to which flavor image belongs 2020-12-07 12:09:06 +03:00
Andrew Dolgov
db3fcb861b viewfeed: reintroduce timestamps, fix debugging, fix some indents 2020-12-04 18:55:53 +03:00
Andrew Dolgov
20af8d5caf queryFeedHeadlines: properly define for a few more variables 2020-12-04 08:59:37 +03:00
Andrew Dolgov
1580748c17 queryFeedHeadlines: make sure feed_check_qpart is always defined 2020-12-04 08:55:26 +03:00
Andrew Dolgov
904d5f7a3b queryFeedHeadlines: no longer select DISTINCT headlines for performance reasons (this also removes _HEADLINES_QUERY_NO_DISTINCT) 2020-12-04 08:44:43 +03:00
Andrew Dolgov
e9673eb13d experimental: add optional _HEADLINES_QUERY_NO_DISTINCT to disable DISTINCT keyword in queryFeedHeadlines query 2020-12-03 14:42:01 +03:00
Andrew Dolgov
81c52b4b1e add support for an override stylesheet which applies to all users 2020-11-30 15:53:32 +03:00
Andrew Dolgov
8089fcc762 feed editor: also show default value for purge interval 2020-11-30 15:34:15 +03:00
Andrew Dolgov
d48460969d feed editor: show actual value of default update interval 2020-11-30 15:29:22 +03:00
Andrew Dolgov
87184904ed don't select next unread feed when marking as read last week, etc. 2020-11-30 15:15:51 +03:00
Andrew Dolgov
d7973fe1b6 use more consistent margins for left toolbar icons 2020-11-27 18:15:05 +03:00
Andrew Dolgov
d1ee30d1ba prevent horizontal scrolling in filter editor dialog if rules are very long 2020-11-27 12:27:12 +03:00
Andrew Dolgov
8479421da4 af_readability: allow appending to original summary instead of always
replacing it, some minor code cleanup
2020-11-26 13:39:47 +03:00
Andrew Dolgov
328d7b55c8 URLHelper: fix E_DEPRECATED error related to idn_to_ascii() 2020-11-14 15:13:35 +03:00
fox
242aa6e411 Merge branch 'allow-audio-cache' of johnjaylward/tt-rss into master 2020-11-05 15:40:42 +00:00
John Aylward
01c0d4bbfd allow audio to be sent to client from the cache 2020-11-04 14:34:37 -05:00
fox
a90d42a556 Merge branch 'disable-mobile-safari-context-menu' of JustAMacUser/tt-rss into master 2020-11-01 05:45:48 +00:00
JustAMacUser
7f38130004 Disable mobile Safari right-click menu on headlines. 2020-10-31 21:20:48 -04:00
Andrew Dolgov
5738e422b5 update jimIcon (https://github.com/jimparis/jimIcon/pull/4) 2020-10-30 09:49:49 +03:00
Andrew Dolgov
6e01d65c6e Merge branch 'weblate-integration' 2020-10-29 09:45:12 +03:00
fox
9259ad366d Merge branch 'debug-typo' of JustAMacUser/tt-rss into master 2020-10-29 05:02:33 +00:00
JustAMacUser
f782ee46ad Fix incorrect parenthesis placement in count(). 2020-10-29 00:52:07 -04:00
Piotr
1039694cdf Translated using Weblate (Polish)
Currently translated at 100.0% (717 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/pl/
2020-10-28 18:24:23 +00:00
Piotr
ed061a5127 Translated using Weblate (Polish)
Currently translated at 100.0% (717 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/pl/
2020-10-26 13:24:29 +00:00
Piotr
5aeceec38c Translated using Weblate (Polish)
Currently translated at 100.0% (717 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/pl/
2020-10-23 23:54:32 +00:00
Piotr
ba7febf8d8 Translated using Weblate (Polish)
Currently translated at 100.0% (717 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/pl/
2020-10-22 23:52:17 +00:00
xnor
f5c88b6603 Translated using Weblate (German)
Currently translated at 100.0% (717 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2020-10-22 23:52:16 +00:00
Piotr
227b2ae350 Translated using Weblate (Polish)
Currently translated at 95.6% (686 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/pl/
2020-10-21 12:45:47 +00:00
Patrick Ahles
8857b0706b Translated using Weblate (Dutch)
Currently translated at 100.0% (717 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nl/
2020-10-21 12:45:46 +00:00
xnor
fa7d5ca342 Translated using Weblate (German)
Currently translated at 100.0% (717 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2020-10-21 12:45:43 +00:00
Andrew Dolgov
0a6ff72e70 Revert "fix HOOK_ARTICLE_RENDERED_CDM never being called"
This reverts commit e3a522cdc1.
2020-10-21 07:32:30 +03:00
Andrew Dolgov
e3da11bf6d Revert "somewhat experimental: disable article packing/unpacking, render content immediately"
This reverts commit ab53591957.
2020-10-21 07:19:15 +03:00
Andrew Dolgov
8d75a542cd Merge branch 'master' of git.fakecake.org:tt-rss 2020-10-20 15:00:10 +03:00
CERT
236013b7b9 Translated using Weblate (Persian)
Currently translated at 99.5% (714 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fa/
2020-10-20 07:52:54 +00:00
Andrew Dolgov
f1fd5e8db1 mark feed as having an error if update task fails (and no last error is already stored for this feed) 2020-10-18 16:12:58 +03:00
Glandos
592fcc578d Translated using Weblate (French)
Currently translated at 100.0% (717 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fr/
2020-10-17 09:24:18 +00:00
Ptsa Daniel
fffccbd2fc Translated using Weblate (Chinese (Simplified))
Currently translated at 99.5% (714 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2020-10-15 17:22:55 +00:00
Dario Di Ludovico
fcf0464e16 Translated using Weblate (Italian)
Currently translated at 100.0% (717 of 717 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2020-10-15 17:22:54 +00:00
Weblate
c0d1a6f85f Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/
2020-10-15 13:27:43 +00:00
Andrew Dolgov
dd5e13b00a rebase translations 2020-10-15 16:27:18 +03:00
Andrew Dolgov
903635c181 Merge branch 'weblate-integration' 2020-10-15 16:27:11 +03:00
Dario Di Ludovico
d4f0f3616e Translated using Weblate (Italian)
Currently translated at 100.0% (727 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2020-10-15 13:25:22 +00:00
Andrew Dolgov
83e08e0e20 Merge branch 'weblate-integration' of git.tt-rss.org:fox/tt-rss 2020-10-13 10:38:32 +03:00
fox
0aa579665f Merge branch 'persist-af-proxy-settings' of JustAMacUser/tt-rss into master 2020-10-11 05:35:58 +00:00
JustAMacUser
65b3926ae5 Ensure proxy_all setting is saved in database. 2020-10-11 01:31:30 -04:00
Andrew Dolgov
e3a522cdc1 fix HOOK_ARTICLE_RENDERED_CDM never being called 2020-10-09 13:18:47 +03:00
Andrew Dolgov
ab53591957 somewhat experimental: disable article packing/unpacking, render content immediately 2020-10-09 13:07:34 +03:00
CERT
a361c17678 Translated using Weblate (Persian)
Currently translated at 96.4% (701 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fa/
2020-10-08 07:39:35 +00:00
CERT
44f99ca1e6 Translated using Weblate (Persian)
Currently translated at 88.8% (646 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fa/
2020-10-06 15:27:07 +00:00
Андрій Жук
0c39a85a31 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (727 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/uk/
2020-10-03 17:41:10 +00:00
Andrew Dolgov
e13d66536c Merge branch 'weblate-integration' 2020-10-03 15:31:49 +03:00
CERT
491369efb1 Translated using Weblate (Persian)
Currently translated at 71.8% (522 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fa/
2020-10-03 12:31:31 +00:00
Andrew Dolgov
935f163919 api: catchupfeed: allow passing 'mode' (optional), bump api version 2020-10-03 10:23:21 +03:00
Andrew Dolgov
4ea407f613 when auto disabling feeds based on DAEMON_UNSUCCESSFUL_DAYS_LIMIT only consider feeds with recent attempts to update (to prevent clashes with not recently logged users, etc) 2020-10-01 15:59:40 +03:00
Andrew Dolgov
38a7a1da88 hide uninteresting errors in several DOMDocument->loadHTML() invocations 2020-10-01 13:20:07 +03:00
Andrew Dolgov
24cdacd59e enable Farsi locale in the UI 2020-10-01 10:19:04 +03:00
Andrew Dolgov
98cad3cdc8 Merge branch 'weblate-integration' 2020-10-01 10:17:07 +03:00
Andrew Dolgov
8a02a728c8 add DAEMON_UNSUCCESSFUL_DAYS_LIMIT tunable (defaults to 30 days) 2020-09-30 17:03:16 +03:00
Andrew Dolgov
e641547d37 set ttrss_feeds.last_successful_update as needed 2020-09-30 16:35:50 +03:00
CERT
14e0efd5b3 Translated using Weblate (Persian)
Currently translated at 48.8% (355 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fa/
2020-09-30 13:08:03 +00:00
Andrew Dolgov
da5deaaca1 set session.cookie_lifetime to 0 initially instead of a rather useless min() 2020-09-30 14:43:53 +03:00
Andrew Dolgov
476be67ff9 updater: set --update-schema as optional-value 2020-09-30 06:56:31 +03:00
Andrew Dolgov
15e8ee3471 housekeeping: add task to cleanup orphan feed icons 2020-09-29 12:46:55 +03:00
Andrew Dolgov
82bc740363 Logger::log - allow specifying errno
bump severity of PDO exception log messages to E_USER_WARNING
2020-09-29 10:08:54 +03:00
Andrew Dolgov
3b17c45887 exclude E_USER_NOTICE from recent events icon 2020-09-29 10:03:11 +03:00
Andrew Dolgov
23d20847a3 update_rss_feed: fallback to previous method if passthru() is not available 2020-09-28 21:19:53 +03:00
Andrew Dolgov
c70e26db31 validate url: feed urlencoded() URL to filter_var() only 2020-09-28 19:46:31 +03:00
Andrew Dolgov
7c8bed0524 accept -1 as a valid exit code for per-feed update processes 2020-09-28 16:02:59 +03:00
Andrew Dolgov
de22464ea8 schema: add ttrss_feeds.last_successful_update 2020-09-28 14:14:06 +03:00
Andrew Dolgov
97d7e5a42a allow updating database schema in batch mode 2020-09-28 13:51:47 +03:00
Andrew Dolgov
335dcd3bf9 don't mention last_updated in non-zero failure error message because that's not what it means 2020-09-28 08:32:14 +03:00
Andrew Dolgov
3534b8dfa7 improve logging for per-feed update task failures 2020-09-28 08:02:38 +03:00
Andrew Dolgov
74cd60d7cc update_rss_feed: don't return as if failed on http 304 2020-09-27 17:13:36 +03:00
Andrew Dolgov
d4d0e976dc update-feed: exit with non-zero exit code if update_rss_feed() failed
daemon: log if per-feed update task terminated with non-zero exit code
2020-09-27 16:42:45 +03:00
Andrew Dolgov
0761533d0a lock per-feed update processes based on feed ID to reduce possibilty
of concurrent updates
2020-09-27 16:01:39 +03:00
Andrew Dolgov
528b387563 update individual feed in a separate process to prevent PHP fatal errors
(for example, OOM) from stopping the entire batch
this should also slightly increase memory budget for update processes
2020-09-27 15:58:13 +03:00
CERT
ce550a13bf Translated using Weblate (Persian)
Currently translated at 11.9% (87 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fa/
2020-09-27 11:35:15 +00:00
Andrew Dolgov
e993d4feb2 Merge branch 'master' of git.fakecake.org:tt-rss 2020-09-25 10:04:09 +03:00
Andrew Dolgov
71e9f70b8a search_to_sql: use per-user default language instead of hardcoded english if isn't specified explicitly 2020-09-25 10:03:42 +03:00
Andrew Dolgov
d0ed7890df prev: add missing class 2020-09-23 13:05:00 +03:00
Andrew Dolgov
215f388992 move timestamp-related stuff to a separate class 2020-09-23 13:04:26 +03:00
Andrew Dolgov
05744bb474 fix updater never scheduling feeds for update if they never been updated before while having default update interval set 2020-09-22 20:33:51 +03:00
Andrew Dolgov
8fb2baecdc another hack for validation of URLs with invalid characters 2020-09-22 19:56:26 +03:00
Andrew Dolgov
a897c4165b validate URLs: convert IDN to punycode before passing URL to filter_var() 2020-09-22 15:32:22 +03:00
Andrew Dolgov
6811d0bde2 use self:: in some places to invoke static methods from the same class 2020-09-22 14:54:15 +03:00
Andrew Dolgov
b5710baf34 - don't fail on non-ascii characters when validating URLs
- fix IDN hostnames not being converted properly
2020-09-22 14:37:45 +03:00
Andrew Dolgov
e3780050e7 Merge branch 'weblate-integration' 2020-09-22 11:55:53 +03:00
Andrew Dolgov
490df818aa router: only allow functions without required parameters as handler methods 2020-09-22 09:34:39 +03:00
Andrew Dolgov
ab6aa0ad3e fix previous re: resolve_redirects 2020-09-22 09:18:24 +03:00
Andrew Dolgov
74568df4ff remove a lot of stuff from global context (functions.php), add a few helper classes instead 2020-09-22 09:04:33 +03:00
Glandos
4d6c80b198 Translated using Weblate (French)
Currently translated at 100.0% (727 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fr/
2020-09-22 01:24:24 +00:00
Andrew Dolgov
41fbd3f15f Added translation using Weblate (Persian) 2020-09-21 18:27:39 +00:00
Andrew Dolgov
d04ac399ff clarify some URL validation-related error messages 2020-09-21 20:37:29 +03:00
Andrew Dolgov
3dd4169b5f clarify some URL validation-related error messages 2020-09-21 20:35:24 +03:00
Andrew Dolgov
4785f21316 update_rss_feed: log effective URL after fetching
validate_url: treat scheme as case-insensitive
2020-09-21 20:26:57 +03:00
Andrew Dolgov
486f1d84ed resolve_redirects: fix previous 2020-09-20 18:14:34 +03:00
Andrew Dolgov
d2867d887a resolve_redirects: only use three argument version of get_headers() on php 7.1+ 2020-09-20 17:27:04 +03:00
Andrew Dolgov
05ef9aac2f update URL pointing to version.json 2020-09-19 07:33:59 +03:00
fox
7584ecc8a2 Merge branch 'gettext-const-scope' of JustAMacUser/tt-rss into master 2020-09-19 04:04:47 +00:00
JustAMacUser
c8ac9dc7ea Remove private scope for class constants.
This change branches from the merged patch by Sunil Mohan Adapa's for
Debian's package.
2020-09-18 18:13:18 -04:00
Andrew Dolgov
03a337a660 add basic safe mode which doesn't load any user plugins 2020-09-18 15:48:22 +03:00
Andrew Dolgov
3588d5186e - gettext: merge patch from Sunil Mohan Adapa which rewrites plural parser to not use eval()
- fix typo in aforementioned patch which caused plurals to never load
- update code again to newer PHP constructor syntax
2020-09-18 14:05:34 +03:00
Andrew Dolgov
4f5ae94b62 prevent source errors from crashing gulp watch 2020-09-18 12:14:37 +03:00
Andrew Dolgov
f3803c9e60 add eslint to package.json 2020-09-17 20:47:01 +03:00
Andrew Dolgov
5c1f70348e add less to package.json 2020-09-17 20:45:21 +03:00
Andrew Dolgov
4efc3d7b3f validate_url: relax requirements for URLs, limit additional port/loopback filtering to fetch_file_contents() 2020-09-17 20:20:23 +03:00
Andrew Dolgov
a4525d31b2 replace FALSE with false so that static analyzer shuts up about it 2020-09-17 19:02:27 +03:00
Andrew Dolgov
57fac84516 rename gettext.inc to gettext.inc.php (cosmetic) 2020-09-17 18:56:29 +03:00
Andrew Dolgov
d8619b9a84 auth_internal: cast OTP code to integer before trying to check it 2020-09-17 16:50:34 +03:00
Andrew Dolgov
c25edd0024 fetch_file_contents: validate effective URL (after redirects) without CURL 2020-09-17 16:17:33 +03:00
Andrew Dolgov
27e695436f fetch_file_contents: validate effective URL (after redirects) if using CURL 2020-09-17 15:53:13 +03:00
Andrew Dolgov
afa0023c51 don't try to update manually disabled feeds even if they haven't been updated before or are marked for a manual update 2020-09-17 15:40:50 +03:00
Andrew Dolgov
f41fdef389 add gulp task for less compilation 2020-09-17 13:30:52 +03:00
Andrew Dolgov
5415a0e033 add makefile for less to css compilation 2020-09-17 12:15:49 +03:00
Andrew Dolgov
37f41a5246 forgotpass: use type strict comparison for reset token 2020-09-17 11:49:27 +03:00
Andrew Dolgov
5a7e7e1367 don't try to call hash_equals() on unset user token 2020-09-17 10:20:55 +03:00
Andrew Dolgov
f72e6947d5 use hash_equals() correctly 2020-09-17 10:04:00 +03:00
Andrew Dolgov
e3adacc588 fix several cases of Db class being invoked as wrong name (as DB) 2020-09-17 09:18:03 +03:00
Andrew Dolgov
16c86e2fc3 replace some plain http links with https 2020-09-17 09:02:30 +03:00
Andrew Dolgov
a817d3794d * use get_random_bytes() for CSRF token
* get_random_bytes: use PHP7 random_bytes() if it is available
* validate CSRF token using hash_equals
2020-09-17 08:59:18 +03:00
Andrew Dolgov
0757ad0406 auth_internal: use type-strict comparison when checking OTP code 2020-09-17 08:46:57 +03:00
Andrew Dolgov
89d53a7f49 fix typo in previous 2020-09-17 08:45:17 +03:00
Andrew Dolgov
1f79d614c4 fix OTP QR code not displayed because of CSRF token passed as a query
parameter
use type-strict comparison when validating CSRF token on the backend
2020-09-17 08:43:39 +03:00
Andrew Dolgov
6a4b6cf603 amend previous to 127/8 subnet 2020-09-17 07:37:48 +03:00
Andrew Dolgov
213d6330b1 fetch_file_contents: resolve requested hosts and check for possible
loopback address
2020-09-17 07:36:47 +03:00
Andrew Dolgov
88c4dc405e build_url: also put query parameters and fragment in resulting URL
rewrite_relative_url: simplify handling of relative URLs
2020-09-16 21:41:05 +03:00
Andrew Dolgov
9d3c794983 subscribe: allow pre-filling feed URL if passed via query string 2020-09-16 17:20:31 +03:00
Andrew Dolgov
da5af2fae0 cached_url: block SVG images because of potential javascript inside 2020-09-16 16:25:20 +03:00
Andrew Dolgov
33fdde249e pass CSRF token to opml import and feed icon replace dialogs 2020-09-16 06:43:55 +03:00
Andrew Dolgov
f693ebab21 fix default password nag dialog, load via xhr 2020-09-16 06:38:41 +03:00
Andrew Dolgov
77faa5d523 editFeed: only try to reload feed tree in preferences if its actually there 2020-09-15 18:55:34 +03:00
Andrew Dolgov
3f9390c45f comments link: load in new tab 2020-09-15 18:49:03 +03:00
Andrew Dolgov
42b5564d1e editarticletags: load dialog via XHR 2020-09-15 18:47:19 +03:00
Andrew Dolgov
0706a328a4 handler: default base csrf_ignore() to false 2020-09-15 18:16:33 +03:00
Andrew Dolgov
0a142912d3 backend handler: require CSRF, remove obsolete code 2020-09-15 18:08:08 +03:00
Andrew Dolgov
154417d80b public/logout: require valid CSRF token 2020-09-15 16:59:11 +03:00
Andrew Dolgov
cbcb10a272 Feeds: load quickaddfeed and search dialogs via XHR w/ CSRF protection 2020-09-15 16:28:09 +03:00
Andrew Dolgov
8080c525fd - backend: require CSRF token to be passed via POST
- do not leak CSRF token via GET request in feed debugger
- rework Article/redirect to use POST
2020-09-15 16:12:53 +03:00
Andrew Dolgov
aeaafefa07 don't pass csrf token as a GET parameter to Article 2020-09-15 16:03:09 +03:00
Andrew Dolgov
e670ac2ee5 require CSRF token for Article/redirect 2020-09-15 15:35:50 +03:00
Andrew Dolgov
7e50c6c4b5 - enable CSRF support earlier
- remove rpc/sanityCheck from CSRF-excluded calls
2020-09-15 15:32:17 +03:00
Andrew Dolgov
91e1542a82 af_proxy_http: require separate token to access imgproxy 2020-09-15 10:59:57 +03:00
Andrew Dolgov
1621abcffc rewrite_relative_url: validate resulting absolutized URLs 2020-09-15 10:41:57 +03:00
Andrew Dolgov
aa89ea7769 validate_url: only allow safe ports (80, 443), disallow access to loopback 2020-09-15 10:39:09 +03:00
Andrew Dolgov
6c02fea641 validate_url: add clean() 2020-09-15 08:45:15 +03:00
Andrew Dolgov
4abc7d7898 rename base64_img() to image_to_base64() 2020-09-15 08:05:01 +03:00
Andrew Dolgov
79f102c25d af_proxy_http: never print received data directly, always redirect to cached_url
cache/getUrl: basename() passed filename just in case
2020-09-15 08:02:28 +03:00
Andrew Dolgov
1ee458b5c1 cached_url: perform mimetype validation before possible HOOK_SEND_LOCAL_FILE hooks 2020-09-15 07:54:46 +03:00
Andrew Dolgov
0758397dd8 af_redditimgur: don't add embedded blank gif image for rewritten videos 2020-09-15 06:55:22 +03:00
Andrew Dolgov
4a074111b5 user preferences: forbid < and > characters when changing passwords (were silently stripped on save because of clean()) 2020-09-14 20:53:00 +03:00
Andrew Dolgov
da98ba662e public/subscribe: require valid CSRF token when validating the form 2020-09-14 20:21:22 +03:00
Andrew Dolgov
b4cb67e77f remove csrf token from rpc method sanityCheck 2020-09-14 20:00:01 +03:00
Andrew Dolgov
c3d14e1fa5 - fix multiple vulnerabilities in af_proxy_http
- fix vulnerability in rewrite_relative_url() which prevented some URLs from being properly absolutized
- fetch_file_contents: validate all URLs before requesting them
- validate URLs: explicitly whitelist http and https scheme, forbid everything else
- DiskCache/cached_url: only serve whitelisted content types (images, video)
- simplify filename/URL handling code, remove and consolidate some less-used functions
2020-09-14 19:46:52 +03:00
Andrew Dolgov
5b17fdc362 Merge branch 'weblate-integration' 2020-09-11 09:35:15 +03:00
Andrew Dolgov
a922b3cc6d order_to_override_query: allow HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE plugins to override built-in sorting 2020-09-11 07:48:22 +03:00
Andrew Dolgov
67f02e2aa7 properly return counters for labels with zero assigned articles
refs https://community.tt-rss.org/t/label-counter-doesnt-update-when-count-goes-down-to-zero/3766
2020-08-29 08:41:52 +03:00
fox
5497a137de Merge branch 'master' of rodneys_mission/tt-rss into master 2020-08-14 19:21:31 +00:00
Rodney Stromlund
88ced02622 Silence php 7.2 error message generated in session_set_cookie_params. 2020-08-14 10:47:46 -05:00
Andrew Dolgov
ddf9227dc4 pluginhost: allow overriding default sort modes via HOOK_HEADLINES_CUSTOM_SORT_MAP etc 2020-08-13 12:23:27 +03:00
Andrew Dolgov
dfa65e9374 move order_by to SQL override logic into a separate function 2020-08-13 11:52:32 +03:00
Andrew Dolgov
48be005774 instead of taking batch timestamp and score (?) into account, make oldest first sorting work consistently with newest first - i.e. rely on feed-provided timestamp 2020-08-11 13:29:09 +03:00
Andrew Dolgov
05a47e5cf4 OPML: export/import per-feed purge interval 2020-08-10 11:57:39 +03:00
fox
2b50aaed61 Merge branch 'master' of e1e0/tt-rss into master 2020-08-01 15:44:04 +00:00
Paco Esteban
c4ee0e25a1 more int/string type mismatches on getCategories 2020-08-01 16:30:10 +02:00
fox
86ba8a96c4 Merge branch 'master' of e1e0/tt-rss into master 2020-08-01 05:52:58 +00:00
Marek Pavelka
f99de985c1 Translated using Weblate (Czech)
Currently translated at 100.0% (727 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/cs/
2020-07-31 16:23:42 +00:00
Paco Esteban
3da618e0ea make sure all ints are casted (to int) on getCategories 2020-07-31 16:15:16 +02:00
Jan Espen Pedersen
68ccc8f636 Translated using Weblate (Norwegian Bokmål)
Currently translated at 44.7% (325 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nb_NO/
2020-07-19 16:40:06 +00:00
fox
376fe6271d Merge branch 'master' of rodneys_mission/tt-rss-fix-sanity-urls into master 2020-07-13 14:41:05 +00:00
Rodney Stromlund
376dce02bb Update wiki and forums links in error message. 2020-07-13 09:06:59 -05:00
fox
3b033a17f4 Merge branch 'feed-tree-localstorage' of nanaya/tt-rss into master 2020-07-09 18:02:02 +00:00
nanaya
8d8affdc45 Store FeedTree data in localStorage
Patching internal functions of dijit.Tree as they don't provide option on where to store the data.

It stores to cookies by default but the data can get quite big for hundreds of feeds and exceeds cookies size limit.

Not to mention it'll cause the cookie to be sent during any request with nothing handling it server side and just wasting bandwidth.

This patch will also migrate current data in cookie to local storage accordingly.
2020-07-09 01:52:46 +09:00
Jan Espen Pedersen
6868b41cd5 Translated using Weblate (Norwegian Bokmål)
Currently translated at 44.7% (325 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nb_NO/
2020-07-03 22:17:44 +00:00
Anonymous
ec970a6bc8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 44.7% (325 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nb_NO/
2020-07-03 22:17:43 +00:00
Jan Espen Pedersen
2d0424bfcf Translated using Weblate (Norwegian Bokmål)
Currently translated at 44.4% (323 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nb_NO/
2020-07-02 21:19:39 +00:00
fox
68b78ecd3d Merge branch 'bugfix/invalid-opml' of wn/tt-rss into master 2020-07-01 14:48:02 +00:00
Andrew Dolgov
b6372a846d when exporting OPML via web UI, add user login to the filename 2020-07-01 10:02:24 +03:00
Andrew Dolgov
fa653f5a43 prefs: show disabled filters properly on mysql 2020-07-01 09:49:53 +03:00
Andrew Dolgov
2996a3942f prefs: show root of filter tree as enabled so it's not grayed out 2020-07-01 09:48:27 +03:00
wn_
614d3ac1bf Properly check if OPML file was loaded during import. 2020-06-27 15:06:08 -05:00
Andrew Dolgov
c352e872e9 core: pass found enclosures to HOOK_ARTICLE_FILTER
af_redditimgur: remove enclosures if we found something to embed because it's going to be a low-res thumbnail
2020-06-24 22:54:14 +03:00
Andrew Dolgov
6eb94f1e13 better support for image srcset attributes as discussed in https://community.tt-rss.org/t/problem-with-img-srcset/3519 2020-06-15 11:58:59 +03:00
Andrew Dolgov
697418f863 more eslint fixes 2020-06-05 07:54:32 +03:00
Andrew Dolgov
d01ad09800 eslint-related fixes; move a few things from global context to App 2020-06-05 07:44:57 +03:00
Andrew Dolgov
88027d7a39 fix various minor issues reported by eslint 2020-06-04 23:27:22 +03:00
Andrew Dolgov
755662a9d7 add eslintrc 2020-06-04 22:56:28 +03:00
Andrew Dolgov
9d28b3ac50 unify prefs/main App objects, remove fake classes, use single static App object instead 2020-06-04 22:19:23 +03:00
Andrew Dolgov
30ed5d7c3c same, but for preferences 2020-06-04 20:04:17 +03:00
Andrew Dolgov
e37f8cfa78 don't use declare() for static objects with no inheritance because apparently it's not actually needed by AMD 2020-06-04 19:50:13 +03:00
Andrew Dolgov
06cc6e3a2a Merge branch 'weblate-integration' 2020-06-03 12:47:54 +03:00
Andrew Dolgov
676cdf6ee4 move isCombinedMode to AppBase so we wouldn't crash in preferences 2020-06-02 21:00:53 +03:00
Patrick Ahles
3502477f8f Translated using Weblate (Dutch)
Currently translated at 100.0% (727 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nl/
2020-06-01 22:02:07 +00:00
Eike
10884452f0 Translated using Weblate (German)
Currently translated at 100.0% (727 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2020-06-01 22:02:05 +00:00
Anonymous
148c58e172 Translated using Weblate (German)
Currently translated at 100.0% (727 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2020-06-01 15:15:03 +00:00
Eike
db5ac3a0be Translated using Weblate (German)
Currently translated at 100.0% (727 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2020-06-01 15:13:46 +00:00
Anonymous
72898a6aba Translated using Weblate (German)
Currently translated at 100.0% (727 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2020-06-01 15:13:45 +00:00
Andrew Dolgov
ac17ded854 Merge branch 'weblate-integration' 2020-06-01 17:12:51 +03:00
fox
851de81517 Merge branch 'hotkeys_force_top' of itsamenathan/tt-rss into master 2020-05-31 08:09:11 +00:00
Nathan Warner
f8d96543de Created hotkeys_force_top plugin
Renamed swap_jk to match new naming scheme.
2020-05-30 22:45:41 -06:00
Andrew Dolgov
b39e615683 add Headlines.default_force_previous, default_force_to_top 2020-05-25 10:26:03 +03:00
Andrew Dolgov
19893d33e3 only bind up/down in 3 panel mode 2020-05-23 08:53:18 +03:00
Andrew Dolgov
ebb373987a Revert "unbind up/down by default (use native scrolling for consistency with pgup/pgdn)"
This reverts commit 6fc18e450b.
2020-05-23 08:39:44 +03:00
Andrew Dolgov
6fc18e450b unbind up/down by default (use native scrolling for consistency with pgup/pgdn) 2020-05-23 08:38:03 +03:00
Andrew Dolgov
b8906c9b9c hide #toolbar-frame_splitter back again 2020-05-23 06:20:09 +03:00
Andrew Dolgov
409ba0db2d - RIP smooth scrolling and associated hacks
- attempt to make Headlines.move() / Article.cdmMoveToId() behave a bit more intuitively
2020-05-22 21:48:03 +03:00
Andrew Dolgov
c8cc845d5b when removing favicon, reset its auto-refresh timer 2020-05-22 15:06:52 +03:00
Andrew Dolgov
d63329baa1 Headlines.move: add params.force_previous to always move to previous article in CDM 2020-05-19 09:21:07 +03:00
Andrew Dolgov
2deb9c555e Headlines.move: use requestAnimationFrame in CDM :( 2020-05-19 08:07:23 +03:00
Andrew Dolgov
8dc6b48ebd Headlines.move: when going back to top of active article, use a smarter (?) offset calculation 2020-05-17 22:02:47 +03:00
Andrew Dolgov
06d2c65193 calculate_article_hash: don't die() on previous, woops 2020-05-17 17:44:32 +03:00
Andrew Dolgov
3a142cbf58 calculate_article_hash: ignore some useless or read-only fields (i.e. GUID) when calculating hash 2020-05-17 17:42:37 +03:00
Andrew Dolgov
25c8467753 rename Headlines.correctHeadlinesOffset() to scrollToArticleId()
invoke it in Article.view() instead of all over the place
2020-05-17 17:01:52 +03:00
Andrew Dolgov
05a84ab778 Headlines.move: maybe glitch less when moving back to top, etc 2020-05-17 16:04:31 +03:00
Andrew Dolgov
cd1f3cb8cc * store UID in article hashed GUID separately so it could be migrated cleanly to a different instance
* store resulting GUID as a JSON object so it could be extended easier if needed
2020-05-17 14:01:16 +03:00
Andrew Dolgov
eae79615a2 Use more specific definitions for applying stuck effects.
https://git.tt-rss.org/fox/tt-rss/pulls/143
2020-05-17 08:35:01 +03:00
Andrew Dolgov
9ae9302b6b implement keyboard-related changes discussed in https://community.tt-rss.org/t/changing-the-amount-of-scroll-by-arrow-key/3452/7 2020-05-17 08:25:51 +03:00
fox
3dc506a19a Merge branch 'responsive-iframes' of JustAMacUser/tt-rss into master 2020-05-17 05:20:23 +00:00
JustAMacUser
c93bcef91f Keep header above iframes in CDM. 2020-05-16 15:56:49 -04:00
JustAMacUser
7a0ea9d90e Make iframes size responsively. 2020-05-15 22:25:56 -04:00
Andrew Dolgov
a1ffc11619 only enable unpack observer in expanded mode 2020-05-13 12:28:48 +03:00
Andrew Dolgov
7a2e9bef77 add --opml-export to update.php 2020-05-13 12:07:31 +03:00
Andrew Dolgov
5e77d0062b use intersection observer to unpack visible articles, remove Headlines.unpackVisible() 2020-05-13 07:28:13 +03:00
Andrew Dolgov
7adbc95acc remove floating title, use position: sticky for cdm headers instead 2020-05-13 06:51:46 +03:00
Andrew Dolgov
c275a0cd33 DiskCache: append fake file extension when sending cached files based on mime type to make saving files easier 2020-05-12 13:28:54 +03:00
Andrew Dolgov
4a00d41915 Article.cdmMoveToId: don't crash if params is not given 2020-05-09 19:41:11 +03:00
Andrew Dolgov
2b55afbeec sanitize: forbid "allow" attribute
CSS: remove auto hyphens stuff, remove iframe width clipping to 98% because they get squished
2020-05-09 12:49:19 +03:00
Andrew Dolgov
a802649d53 rename cdmScrollToId to cdmMoveToId
prevent smooth scrolling when going directly to an article
2020-05-09 08:16:12 +03:00
Andrew Dolgov
2558fcbe21 add hotkey "\" to cancel current search 2020-05-09 07:56:06 +03:00
fox
c8243b03c9 Merge branch 'master' of ltGuillaume/ttrss into master 2020-05-03 13:45:40 +00:00
ltGuillaume
19064864bf Allow setting Insert (45) and Delete (46) as hotkeys
These are not reported via keypress either, so handle them via keydown.
2020-05-03 14:35:17 +02:00
Andrew Dolgov
3a4b9249a9 DiskCache: properly deal with srcset attributes 2020-04-29 19:29:36 +03:00
Andrew Dolgov
e934e9f05e sanitize: simplify initial attribute processing 2020-04-29 19:12:29 +03:00
Andrew Dolgov
7d9dd51cf4 sanitize: remove srcset plain-http hack, globally disallow width and height attributes for all elements 2020-04-29 19:04:34 +03:00
Andrew Dolgov
83c8834421 sanitize: handle picture[@srcset] elements properly, i.e. rewrite relative URLs 2020-04-29 19:02:44 +03:00
Andrew Dolgov
4a00f96733 remove unneeded var_dump() 2020-04-29 11:35:02 +03:00
Andrew Dolgov
6573541873 * add HOOK_ENCLOSURE_IMPORTED
* pass feed id to HOOK_FEED_PARSED
2020-04-29 11:33:39 +03:00
Andrew Dolgov
84bea5086c Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2020-04-23 15:56:48 +03:00
Andrew Dolgov
01248f09bc change double quotes to single quotes in config.php-dist because double quotes break docker initialization 2020-04-23 15:56:31 +03:00
fox
daaba66f85 Merge branch 'escape-install-part-two' of JustAMacUser/tt-rss into master 2020-04-22 03:55:06 +00:00
fox
2c53343e43 Merge branch 'encoded-installer-entities' of JustAMacUser/tt-rss into master 2020-04-22 03:53:14 +00:00
JustAMacUser
9c3cf60592 More fixes when installer generates config file.
* Use single quotes in config.php when when defining database values so PHP doesn't interpret `$` as a variable (mostly for the password constant).
* Use `addcslashes` instead of `addslashes` and only escape backslash and single quotes.
* Do not convert DB_PORT to integer if leaving it blank (the default).
2020-04-21 21:10:32 -04:00
JustAMacUser
0fb5267d07 During install, HTML encode POST data for forms. 2020-04-21 20:52:19 -04:00
fox
11a9d3bd9b Merge branch 'escape-install' of JustAMacUser/tt-rss into master 2020-04-19 06:41:27 +00:00
JustAMacUser
56e16a8d85 Escape user-defined values during installation. 2020-04-18 21:33:56 -04:00
Andrew Dolgov
0d467973dc Article.pack: dispose of unpacked content properly 2020-04-17 15:59:12 +03:00
Andrew Dolgov
e17c7e2fb4 Headlines.renderAgain: scroll instantly to active article when going back to combined mode on the fly 2020-04-17 07:58:34 +03:00
Andrew Dolgov
b3e4f0188e in combined non-expanded mode, pack headline rows as they are unfocused to save RAM 2020-04-17 07:37:56 +03:00
Andrew Dolgov
afaac95d8d if comment URL is not specified but comment count is non-zero, show comments prompt leading to the article 2020-04-07 06:50:24 +03:00
Andrew Dolgov
44b1f0fcc0 search: add support for label:XXX search keyword
Labels: enforce case-insensitive lookups when creating/looking for labels
2020-04-04 14:34:08 +03:00
Andrew Dolgov
586ed55178 Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2020-03-28 11:46:36 +03:00
Andrew Dolgov
a0da71598a remove atom-to-html XSLT 2020-03-28 11:45:42 +03:00
fox
c0f689a58f Merge branch 'fix_noexpand_doc' of d7415/tt-rss into master 2020-03-26 04:05:05 +00:00
Martin Stone
1ad43dd202 Fix documentation for _noexpand commands 2020-03-25 20:30:34 +00:00
Andrew Dolgov
61be0b215d minitemplator->writeString: print always returns 1 in PHP 2020-03-13 14:48:40 +03:00
Andrew Dolgov
491090d21b minitemplator: fix deprecations for PHP 7.4 2020-03-13 14:46:40 +03:00
Andrew Dolgov
1f2a721905 allow overriding built-in templates via templates.local 2020-03-13 14:40:35 +03:00
fox
82326187f9 Merge branch 'cache_videos_with_src_and_poster' of lllusion3418/tt-rss into master 2020-03-12 10:18:59 +00:00
lllusion3418
ec1b0befc7 add support for video[@src] in media cache
it's a valid alternative to a source[@src] child element:
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video
2020-03-12 11:08:39 +01:00
lllusion3418
cdde23b4dc actually download <video> posters to media cache
video[@poster] is already supported in the rewriting logic but never
actually downloaded
2020-03-12 11:08:33 +01:00
lllusion3418
b4287a2e98 fix url rewriting for videos with poster and src
if a poster attribute was present only that would have been rewritten
and the (arguably more important) src attribute would be left as-is
2020-03-12 11:08:24 +01:00
Andrew Dolgov
208e02c47d PluginHost/save_data: use separate PDO connection to prevent issues with nested transactions 2020-03-10 08:14:00 +03:00
Dario Di Ludovico
e025d92fc5 Translated using Weblate (Italian)
Currently translated at 100.0% (727 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2020-03-08 21:12:10 +00:00
fox
da926067ab Merge branch 'get-version-git-on-windows' of tsimmons/tt-rss into master 2020-03-03 05:36:22 +00:00
Toby Simmons
569228a5df In get_version() disable DIRECTORY_SEPARATOR check, permit using git on Windows to get version details; 2020-03-02 11:28:21 -06:00
Andrew Dolgov
8ad523fcec do not keep current feed visible when hide as read is enabled 2020-03-01 21:12:50 +03:00
Andrew Dolgov
68562e2618 fix wrong colors applying to rule textarea when invoked from main UI 2020-02-28 18:23:19 +03:00
Andrew Dolgov
482db14c1a filter rule editor: bring back regexp valid/invalid BG colors (now for light theme) 2020-02-28 18:21:16 +03:00
Andrew Dolgov
490a98df11 filter rule editor: bring back regexp valid/invalid BG colors 2020-02-28 18:19:34 +03:00
Ptsa Daniel
3f88dbad71 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.5% (724 of 727 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2020-02-28 14:59:46 +00:00
Andrew Dolgov
19e1d13460 add URL parameter to ignore user theme on startup 2020-02-28 14:53:01 +03:00
Andrew Dolgov
ad0a9c02e5 move default (light) theme LESS source to a more appropriate place
add compact_dark theme variant
2020-02-28 14:48:48 +03:00
Andrew Dolgov
76c8b318e5 set all tooltip colors via less variables 2020-02-28 14:34:48 +03:00
Andrew Dolgov
0875dc332e fix tooltip colors for night mode 2020-02-28 14:22:11 +03:00
Andrew Dolgov
a65749a512 source validationtextarea in main UI 2020-02-28 14:04:29 +03:00
Andrew Dolgov
bcbc5ccc78 batchSubscribe: use validationtextarea 2020-02-28 14:03:29 +03:00
Andrew Dolgov
f24ece85a6 add validationtextarea control, use it for filter match editor 2020-02-28 13:53:45 +03:00
Andrew Dolgov
2fefb4fd87 getTestResults: don't try to use previously removed variable 2020-02-28 12:54:39 +03:00
Andrew Dolgov
4f62f5f3f1 filter edit dialog: load rule editor via XHR 2020-02-28 12:52:20 +03:00
Andrew Dolgov
340bb7f392 fix typo 2020-02-28 12:17:49 +03:00
Andrew Dolgov
8645f36c5b filter test dialog: pass contents via xhr POST 2020-02-28 12:16:54 +03:00
Andrew Dolgov
0eb3f1c3dc rebase translations 2020-02-28 08:08:52 +03:00
Andrew Dolgov
864f3e7e07 Merge branch 'weblate-integration' 2020-02-28 08:08:24 +03:00
Andrew Dolgov
4e74da590e af_readability: allow get full text button to work as a toggle; in cdm, scroll to article after embedding 2020-02-28 08:03:25 +03:00
Andrew Dolgov
bdb1e475e7 external subscribe dialog: support dark theme 2020-02-27 13:40:32 +03:00
Andrew Dolgov
b2876f6c72 share anything dialog: support dark theme 2020-02-27 13:38:24 +03:00
Andrew Dolgov
8c31651a21 fix alert-info/alert-danger colors on dark theme 2020-02-27 12:25:32 +03:00
Andrew Dolgov
96fa6e3002 af_comics: split contents of subscribe/basic_info/fetch hooks into appropriate per-comic filters 2020-02-27 12:15:56 +03:00
Andrew Dolgov
ba7f7e72db af_comics: mention that Far Side needs cached media 2020-02-27 11:44:18 +03:00
Andrew Dolgov
61168847ac af_comics: escape all template urls 2020-02-27 10:25:00 +03:00
Andrew Dolgov
3b62150abd use canonical fetch url for Far Side 2020-02-27 10:24:12 +03:00
Andrew Dolgov
db8a1f76c7 remove unnecessary debugging from previous 2020-02-27 10:20:16 +03:00
Andrew Dolgov
9b4053b1ea af_comics: add experimental support for The Far Side 2020-02-27 10:19:09 +03:00
Andrew Dolgov
07b27b375f update toggle_embed_original hotkey to invoke readability embed instead of removed embed_original plugin 2020-02-27 09:47:20 +03:00
Andrew Dolgov
b159bbe55d af_readability: sanitize content requested for embedding 2020-02-27 08:28:54 +03:00
Andrew Dolgov
3b635c7557 fix plugins/note javascript part broken by previous changeset 2020-02-27 07:59:57 +03:00
Andrew Dolgov
71ff485fbf af_readability: add article button to embed content of a specific article 2020-02-27 07:57:22 +03:00
Andrew Dolgov
671a2a0275 fix hr colors for dark theme, don't use hardcoded rgb value in light theme 2020-02-25 13:25:55 +03:00
Andrew Dolgov
38b43cd559 adjust previous to not use hardcoded rgb color 2020-02-25 12:14:18 +03:00
Andrew Dolgov
d9cd790d03 dark theme, all articles icon: improve contrast 2020-02-25 10:11:18 +03:00
Andrew Dolgov
788ea95fbd feed tree: do not mark Labels as Special 2020-02-22 16:44:31 +03:00
Andrew Dolgov
4ab3854aed don't generate default.css, replace with themes/light.css as a default root CSS file 2020-02-22 16:22:44 +03:00
Andrew Dolgov
84b847074e remove night_base.css: not needed 2020-02-22 15:45:57 +03:00
Andrew Dolgov
f0a343717f remove defines.css: not needed 2020-02-22 15:45:13 +03:00
Andrew Dolgov
d6ac529d01 fix default.css not being built properly by phpstorm 2020-02-22 15:43:28 +03:00
Andrew Dolgov
282b445a43 feed tree: don't set Special class on Labels category 2020-02-21 14:12:10 +03:00
Andrew Dolgov
2d3fdd6836 hide read feeds / hide read shows special: use CSS instead of JS-based hiding 2020-02-21 14:06:21 +03:00
Andrew Dolgov
5f30061c92 properly calculate marked counters for feeds in nested categories 2020-02-20 15:54:40 +03:00
Andrew Dolgov
60288f02e8 1. feedtree: show counters for marked articles if view-mode == marked
2. hide/show relevant counter nodes using css
3. cleanup some counter-related code
4. compile default css into light theme to prevent cache-related issues
2020-02-20 14:14:45 +03:00
Andrew Dolgov
5b6d9cee29 prefs layout fixes:
1. prevent layout breakage when using an authenticator which doesn't allow changing passwords
2. show explanatory messages when OTP or password changing is not available
3. allow app (API) passwords when using any auth module
2020-02-18 11:51:04 +03:00
Dario Di Ludovico
8c907e3fe4 Translated using Weblate (Italian)
Currently translated at 100.0% (726 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2020-02-16 13:27:07 +00:00
Andrew Dolgov
06b9d39662 add support for image loading=lazy attribute 2020-02-13 20:20:55 +03:00
Marek Pavelka
fef4d2e5b9 Translated using Weblate (Czech)
Currently translated at 100.0% (726 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/cs/
2020-02-05 15:58:51 +00:00
Andrew Dolgov
47135160d1 getCategoryCounters: properly handle categories which don't have any stored feeds/articles 2020-01-27 15:45:04 +03:00
Andrew Dolgov
076c5382fa login form: add workarounds for chrome password manager 2020-01-25 17:00:51 +03:00
Andrew Dolgov
88d4324e32 mark primary button in the default password dialog 2020-01-25 13:08:29 +03:00
Andrew Dolgov
776fe4768b default password warning: fix close button, don't crash if dialog is recreated (on feed tree reload etc) 2020-01-25 13:02:11 +03:00
Andrew Dolgov
0e9e1ad112 getCategoryUnread: return correct unread count for labels category 2020-01-25 12:53:10 +03:00
Andrew Dolgov
cdd2b6fd22 getCategoryChildrenUnread: fix typo 2020-01-25 10:00:22 +03:00
Andrew Dolgov
a6ced36189 getCategoryCounters: properly calculate counters for child subcategory entries
getCategoryUnread: cleanup
2020-01-25 09:57:28 +03:00
Andrew Dolgov
deefa901ab mark feeds with marked articles if marked view mode is set 2020-01-24 16:14:10 +03:00
Andrew Dolgov
4a4d7a44fa onViewModeChanged: set view mode value as a custom body attribute 2020-01-24 16:04:12 +03:00
Andrew Dolgov
a64b8a7fdb getCategoryUnread: don't return unread counters for Special category because it doesn't make a lot of sense to do so 2020-01-24 15:54:01 +03:00
Andrew Dolgov
6f625aa8aa apply Has_Marked css class to feed tree elements with marked articles 2020-01-24 14:35:10 +03:00
Andrew Dolgov
2f6741e49a getFeedCounters: pass parameter correctly to PDO 2020-01-24 14:27:24 +03:00
Andrew Dolgov
6080cca9ca scrap counter cache system; rework counters to sum() booleans instead 2020-01-24 14:25:31 +03:00
Andrew Dolgov
a6d314b753 support dark mode for login form 2020-01-23 13:14:47 +03:00
Andrew Dolgov
3b29e865b0 support night mode in feed debugger 2020-01-19 10:56:49 +03:00
Andrew Dolgov
aa56bcaf44 support night mode when using share by URL 2020-01-19 10:51:08 +03:00
Andrew Dolgov
303f8fb329 properly escape quotes when rendering article data to html attributes via template strings 2020-01-18 10:31:00 +03:00
Andrew Dolgov
01513aa41b disable MAX_FETCH_REQUESTS_PER_HOST warnings for the time being 2020-01-17 07:26:55 +03:00
Andrew Dolgov
5fc499e19e get_version: don't rely on exec() exit code to determine whether output is valid 2020-01-14 20:50:40 +03:00
Andrew Dolgov
f47998f569 generate_syndicated_feed: use local media in generated feeds if it is available 2020-01-13 17:02:14 +03:00
fox
040dfc71fc Merge branch 'master' of conrad784/tt-rss into master 2020-01-09 13:58:48 +00:00
Conrad Sachweh
c2217f2d4b fixed gettext library deprecated constructors
https://www.php.net/manual/en/language.oop5.decon.php

was already done for parent three years ago 00b6b66827
2020-01-09 10:45:09 +01:00
Andrew Dolgov
7e2fd9bdce Headlines.move: fix move to previous article if scrollTop returns a fractional value for current item 2020-01-08 08:51:40 +03:00
Andrew Dolgov
b1c5ebdace API/getVersion: don't try to use removed VERSION constant 2020-01-05 09:42:57 +03:00
fox
67c7b6ef53 Merge branch 'af_redditimgur_update' of koffieanon/tt-rss into master 2020-01-05 05:44:00 +00:00
koffieanon
3a3c74dfa4 Also match images with query string (size, tokens, etc). 2020-01-04 17:22:58 +01:00
koffieanon
e89dd83f05 Spaces to tabs for consistency. 2020-01-04 17:21:05 +01:00
koffieanon
297a89c2d2 Fix bug processing found due to operator precedence. 2020-01-04 17:20:33 +01:00
Andrew Dolgov
5448a9462e Merge branch 'weblate-integration' 2019-12-22 08:47:56 +03:00
Patrick Ahles
5ae0898c91 Translated using Weblate (Dutch)
Currently translated at 98.3% (714 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nl/
2019-12-21 21:37:25 +00:00
Dario Di Ludovico
f80afc125a Translated using Weblate (Italian)
Currently translated at 98.8% (717 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/it/
2019-12-21 21:37:24 +00:00
Andrew Dolgov
7487067608 Merge branch 'weblate-integration' 2019-12-21 11:56:37 +03:00
Andrew Dolgov
139918a571 Merge branch 'weblate-integration' 2019-12-21 11:54:44 +03:00
Andrew Dolgov
21e028a4c6 upd CONTRIBUTING 2019-12-21 10:00:57 +03:00
Andrew Dolgov
74ccf96d0b update CONTRIBUTING.md (with weblate stuff, etc) 2019-12-21 09:40:59 +03:00
Andrew Dolgov
749667dceb Deleted translation using Weblate (Indonesian) 2019-12-20 18:39:24 +00:00
Weblate
a0e2a625d4 Merge branch 'origin/weblate-integration' into Weblate. 2019-12-20 18:29:06 +00:00
Ptsa Daniel
7f8832980d Translated using Weblate (Chinese (Simplified))
Currently translated at 99.6% (723 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hans/
2019-12-20 18:29:06 +00:00
Andrew Dolgov
bb7099ffb0 Merge branch 'master' of git.tt-rss.org:fox/tt-rss into weblate-integration 2019-12-20 21:24:53 +03:00
Andrew Dolgov
021757ad5c Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2019-12-20 18:17:50 +03:00
Andrew Dolgov
fdb1fc7608 get_version: fix commit/timestamp lost on subsequent invocations because of misbehaving caching 2019-12-20 18:17:05 +03:00
Andrew Dolgov
4b44103b84 Merge branch 'master' of git.fakecake.org:tt-rss into weblate-integration 2019-12-20 18:05:55 +03:00
Andrew Dolgov
63ee91c82e backend: load invoked classes via reflection so object constructor is called after it has been verified as an IHandler implementation.
this should prevent a potential router vulnerability if non-IHandler autoloader-enabled class is requested by malicious authorized user *and* invoked class object does something insecurely in its constructor.
2019-12-20 14:39:38 +03:00
Andrew Dolgov
573784c316 Revert "Translated using Weblate (Indonesian)"
This reverts commit 78d932b528.
2019-12-20 09:02:56 +03:00
zmni
78d932b528 Translated using Weblate (Indonesian)
Currently translated at 0.1% (1 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/id/
2019-12-20 04:36:10 +00:00
Andrew Dolgov
e9b4834b6b Merge branch 'master' of git.fakecake.org:tt-rss 2019-12-19 14:41:34 +03:00
Andrew Dolgov
ed06592114 update CONTRIBUTING.md 2019-12-19 14:41:26 +03:00
Andrew Dolgov
6439f7817d force-disable php display_errors/display_startup_errors on startup 2019-12-19 08:37:19 +03:00
Andrew Dolgov
c309856a97 get_version: filter out Darwin 2019-12-19 07:04:01 +03:00
Andrew Dolgov
74feef0f9d get_version: always return unsupported on windows 2019-12-18 19:28:00 +03:00
Andrew Dolgov
c46c5e59fc SELF_USER_AGENT: switch to get_version() 2019-12-18 15:56:27 +03:00
Andrew Dolgov
72d8a34f74 get_version: don't pass useless root dir to git, instead log it in case of failure 2019-12-18 15:29:12 +03:00
Andrew Dolgov
72d0fac80c remove version.php and VERSION global constant, do version-related things in a slightly less ridiculous way 2019-12-18 14:27:40 +03:00
zmni
6a01322bf9 Added translation using Weblate (Indonesian) 2019-12-18 10:53:12 +00:00
Andrew Dolgov
10aabfcd09 rebase translations 2019-12-17 15:00:43 +03:00
Andrew Dolgov
3bd3613531 Merge branch 'weblate-integration' 2019-12-17 14:59:09 +03:00
Andrew Dolgov
df464e3d0d update app password notice 2019-12-17 14:58:31 +03:00
Andrew Dolgov
f83836ade9 updateCurrentUnread: don't crash if counter element is not available 2019-12-17 14:06:50 +03:00
Andrew Dolgov
9f70bb010a fix blank screen on load if custom theme is enabled 2019-12-15 13:35:09 +03:00
Andrew Dolgov
07f4878d59 workaround for a race condition between dojo.parse() and tt-rss loading proper day/night css on startup because of firefox async CSS loader 2019-12-15 11:57:26 +03:00
Andrew Dolgov
0d6add5d7f show alert() if fatal exception happens while initializing base app objects and app.error is not available 2019-12-14 09:39:44 +03:00
fox
e146a1ab37 Merge branch 'safari-mediaquerychange' of JustAMacUser/tt-rss into master 2019-12-13 18:54:16 +00:00
JustAMacUser
b4dd03ba2a Wrap AppBase.setupNightModeDetection() in try/catch because Safari doesn't support matchMedia change events. 2019-12-13 13:39:52 -05:00
Andrew Dolgov
0237dee980 implement automatic night mode detection using MQL
add separate light.css to force light theme
remove manual night mode toggle and related code
2019-12-12 20:09:43 +03:00
Andrew Dolgov
9c0235ab66 show current unread counter on headlines toolbar if sidebar is hidden 2019-12-12 07:37:28 +03:00
Andrew Dolgov
0a10832491 - update descriptions of changed hotkeys
- bind noscroll variants of move article hotkeys to n/p by default
- update N/P (i.e. scroll article content) hotkeys to scroll by fraction of viewport height instead of hardcoded pixel distance
- minor fixes w/ checking for undefined
2019-12-11 06:53:32 +03:00
Nikolay Korotkiy
7982a6bdc6 Translated using Weblate (Esperanto)
Currently translated at 13.3% (94 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/eo/
2019-12-10 18:38:31 +00:00
Nikolay Korotkiy
beda8cb7fc Translated using Weblate (Russian)
Currently translated at 100.0% (708 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/ru/
2019-12-10 18:38:29 +00:00
Nikolay Korotkiy
bf6cd242b3 Translated using Weblate (Finnish)
Currently translated at 80.4% (569 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fi/
2019-12-10 18:38:27 +00:00
Andrew Dolgov
985e11b754 re-enable updates of floating title on scroll, duh 2019-12-10 16:45:59 +03:00
Andrew Dolgov
0e4f67bf2b with previous change, we don't actually need to automatically track active articles now at all in combined mode. 2019-12-10 13:03:49 +03:00
Andrew Dolgov
3993198aa7 when moving next or previous and currently active article is entirely invisible, start moving from first visible one 2019-12-10 12:55:24 +03:00
Andrew Dolgov
7d0bbe9962 only track active article on scroll if auto catchup is enabled 2019-12-10 12:34:49 +03:00
Andrew Dolgov
5b4eb8d7b9 remove unnecessary "== 1" when checking for init params
unsubscribeFeed: check for undefined title correctly
2019-12-10 09:10:45 +03:00
Andrew Dolgov
a40f22d8aa Article.cdmScrollToId: disable smooth scrolling in collapsed combined mode 2019-12-10 08:58:32 +03:00
Andrew Dolgov
560346f9d1 Article.cdmScrollToId: disable smooth scrolling on repeated events 2019-12-10 08:51:45 +03:00
Andrew Dolgov
dad3d1c7a9 combined mode n/p behavior changes:
1. instead of jumping/scrolling sometimes, always scroll by a constant viewport offset unless moving to next/prev article directly
2. when going up and current article is partially above the viewport, move to its top first instead of directly to a previous one
3. instead of previous marking active logic, on scroll in combined mode track first (partially or otherwise) visible article as active
2019-12-10 07:47:09 +03:00
Andrew Dolgov
44ef447c0f fix fatal error in previous because of event not being passed via Headlines.move()
scrollbypages, etc: make event optional anyway
2019-12-09 23:23:54 +03:00
Andrew Dolgov
e7dd634183 exp: auto-disable smooth scrolling for repeat hotkey events 2019-12-09 22:42:43 +03:00
Andrew Dolgov
008afb97a9 exp: unbind from pgup/pgdn buttons by default 2019-12-09 12:38:04 +03:00
Andrew Dolgov
7a68e4a6f7 pgup/pgdn; increase scroll distance to almost entire viewport height (from 90%) 2019-12-09 12:22:43 +03:00
Andrew Dolgov
6191c48596 trim() contents of version_static.txt 2019-12-09 07:11:34 +03:00
Andrew Dolgov
1aeeed930a remove a bunch of obsolete files 2019-12-08 09:44:05 +03:00
fox
f4945b1ba1 Merge branch 'page-hotkeys' of suraia/tt-rss into master 2019-12-08 06:02:10 +00:00
Andrew Dolgov
5907409a84 add support for custom version_static.txt for package maintainers 2019-12-08 08:58:23 +03:00
Michael Kuhn
f133b78a3e Fix Shift+PageUp/Down hotkeys 2019-12-06 20:39:22 +01:00
Andrew Dolgov
76dd74e0d9 add a hidden tweakable which forbids changing passwords 2019-12-06 17:45:22 +03:00
Andrew Dolgov
ac95ab4a65 user css dialog: allow saving and applying CSS without closing the dialog 2019-12-06 14:02:30 +03:00
Andrew Dolgov
0697eca0e1 remove testing for get_magic_quotes_gpc: deprecated in php7.4, apparently not working since php 5.4 2019-12-06 07:34:50 +03:00
Andrew Dolgov
565547f5a1 php 7.4 deprecation-related fixes 2019-12-06 07:27:22 +03:00
Andrew Dolgov
e1ef122355 force-disable headlines smooth scrolling when switching feeds
enable smooth scrolling for article frame
2019-12-05 21:48:16 +03:00
Andrew Dolgov
e40c24f829 #headlines-frame: set scroll-behavior: smooth 2019-12-05 17:01:07 +03:00
Andrew Dolgov
1902a7dcb0 pgup/pgdown hotkey normalization:
- pgup/pgdown without modifier scroll headline buffer
- shift+pgup/pgdown work similarly to shift+up/down but operating on pages
2019-12-05 17:00:17 +03:00
Andrew Dolgov
f30287be65 versioning changes
- remove VERSION_STATIC - https://community.tt-rss.org/t/versioning-changes-for-trunk/2974
- report git commit/timestamp properly by invoking git instead of trying to parse .git/HEAD etc
- remove git-related global constants used when checking for updates
2019-12-05 13:23:54 +03:00
Andrew Dolgov
6913158b82 add hotkeys to scroll headlines/articles (whichever is active) by one page 2019-12-04 15:50:49 +03:00
Andrew Dolgov
cb92f56b13 Merge branch 'weblate-integration' 2019-12-04 15:38:19 +03:00
Андрій Жук
bc3b98a756 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (708 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/uk/
2019-12-03 15:38:16 +00:00
Andrew Dolgov
219840341c Af_Youtube_Embed: whitelist youtube iframes if enabled 2019-11-27 22:46:43 +03:00
Andrew Dolgov
d15f0349bf remove hardcoded iframe domain whitelist, make iframe script whitelisting configurable by plugins (HOOK_IFRAME_WHITELISTED) 2019-11-27 11:52:51 +03:00
progit
071a46f572 Translated using Weblate (Russian)
Currently translated at 96.0% (680 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/ru/
2019-11-25 20:58:36 +00:00
Andrew Dolgov
e5b7b145e5 cache media: set referrer to source URL when fetching images 2019-11-25 09:48:24 +03:00
Nikolay Korotkiy
3f219d8585 Translated using Weblate (Esperanto)
Currently translated at 3.7% (26 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/eo/
2019-11-23 18:58:47 +00:00
Nikolay Korotkiy
880b88a856 Translated using Weblate (Russian)
Currently translated at 95.1% (673 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/ru/
2019-11-23 18:58:46 +00:00
Nikolay Korotkiy
a7a1bc0288 Translated using Weblate (Finnish)
Currently translated at 75.3% (533 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fi/
2019-11-23 18:58:33 +00:00
Nikolay Korotkiy
44a559e89c Added translation using Weblate (Esperanto) 2019-11-22 17:09:41 +00:00
洪偉翔
5b6c411fe8 Translated using Weblate (Chinese (Traditional))
Currently translated at 98.9% (700 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2019-11-22 02:58:40 +00:00
Andrew Dolgov
304d3a0b88 tag-related fixes
1. move tag sanitization to feedparser common item class
2. enforce length limit on tags when parsing
3. support multiple tags passed via one dc:subject and other such elements, parse them as a comma-separated list
4. sort resulting tag list to prevent different order between feed updates
5. remove some duplicate code related to tag validation
6. allow + symbol in tags
2019-11-20 18:56:34 +03:00
Andrew Dolgov
ffa3f9309f af_comics: support buni webtoon episodes 2019-11-18 19:00:08 +03:00
Andrew Dolgov
8c3efd51ec reset domain hit quota on feed update start 2019-11-17 13:17:21 +03:00
Andrew Dolgov
cd4b7f1988 implement MAX_FETCH_REQUESTS_PER_HOST: only generating a warning on exceeded quota for the time being 2019-11-14 07:38:49 +03:00
Andrew Dolgov
63ce7ea705 add a plugin page warning for plugins using HOOK_FEED_FETCHED, etc 2019-11-14 07:01:45 +03:00
Andrew Dolgov
0d7b10469b update_rss_feed: add specific logging for HOOK_FETCH_FEED, HOOK_FEED_FETCHED, HOOK_FEED_PARSED handlers 2019-11-14 06:39:45 +03:00
fox
762ff9b9cd Merge branch 'pdo-experimental' of cac2s/tt-rss into master 2019-11-13 09:28:16 +00:00
cac2s
6b47f5a6d1 fix position for "forgotpass" link 2019-11-13 10:52:25 +02:00
Andrew Dolgov
5bb8dad631 is_gzipped: don't try to strpos() over entire buffer 2019-11-12 07:11:10 +03:00
Andrew Dolgov
60609637bc schema: when inserting initial forum feed, don't hardcode admin UID as 1 2019-11-07 08:48:13 +03:00
fox
8551cce494 Merge branch 'patch-install-pdo-error' of JustAMacUser/tt-rss into master 2019-11-06 03:05:36 +00:00
JustAMacUser
3cae6fe6ad Fixed "Using when not in object context" error when installer query fails. 2019-11-05 19:35:48 -05:00
Andrew Dolgov
f6090655bf 2fa: check TOTP based on previous secret values (oops of the year, 2019) 2019-11-03 20:47:21 +03:00
Andrew Dolgov
17e145f481 revise schema 139 to use engine=innodb 2019-11-03 20:13:42 +03:00
Andrew Dolgov
f75fb6bd75 Merge branch 'master' of git.fakecake.org:tt-rss 2019-11-01 15:40:15 +03:00
Andrew Dolgov
266a805bfe line endings + remove : from headings 2019-11-01 15:40:08 +03:00
Andrew Dolgov
05dffcff6f OTP stuff: update notice wording a bit 2019-11-01 15:27:24 +03:00
Andrew Dolgov
812a6c9f16 auth_internal: fix indents 2019-11-01 15:25:40 +03:00
Andrew Dolgov
249130e58d implement app password checking / management UI 2019-11-01 15:03:57 +03:00
Andrew Dolgov
b158103f2f schema: add missing stuff 2019-11-01 13:30:12 +03:00
Andrew Dolgov
68b0380118 add placeholder authentication via app passwords if service is passed
forbid logins via regular passwords for services
remove AUTH_DISABLE_OTP
2019-11-01 13:03:06 +03:00
Andrew Dolgov
88cd9e586e add placeholder UI plumbing for app passwords 2019-11-01 12:23:11 +03:00
Andrew Dolgov
84e9f1d5cc update schema for app-specific passwords 2019-11-01 11:57:45 +03:00
Andrew Dolgov
178bcd4349 auth_internal: fix OTP seed checking 2019-11-01 10:34:31 +03:00
Andrew Dolgov
904ecc31e2 allow using OTP without GD 2019-11-01 10:32:58 +03:00
Andrew Dolgov
2d77d2d89e Merge branch 'weblate-integration' 2019-10-30 14:08:08 +03:00
Andrew Dolgov
647c7c45eb allow article filters to modify num_comments 2019-10-25 14:37:00 +03:00
Andrew Dolgov
81bf1125aa update OTP template 2019-10-09 09:11:57 +03:00
Andrew Dolgov
2820f41a4b add notification for OTP being disabled 2019-10-09 09:10:43 +03:00
Andrew Dolgov
ef514bc4bd add notifications for mail and password changes
update and shorten some other message templates
2019-10-09 09:04:51 +03:00
fox
d029e18976 Merge branch 'hook-feed-tree' of jc/tt-rss into master 2019-10-08 07:06:07 +00:00
jc
8fd11fd53a Add const HOOK_FEED_TREE 2019-10-07 13:46:31 +00:00
jc
a243979aaf Add const HOOK_FEED_TREE 2019-10-07 13:44:57 +00:00
jc
f56ae1dcc9 Add HOOK_FEED_TREE to div feeds-holder 2019-10-07 13:43:24 +00:00
fox
ac3d561f4d Merge branch 'comics-patch-timestamp' of JustAMacUser/tt-rss into master 2019-10-07 03:31:20 +00:00
JustAMacUser
8459238f6c af_comics: Use a fixed time of day when generating fake feed for GoComics. Without this the timestamp is always updated to be the time the feed is fetched, which causes the comics to keep moving to the top/bottom of the article list depending on the sort order. (Using 11:00 a.m. UTC as that should keep the date the same across the majority of time zones.)
Try to get the actual title for GoComics comics.

Also a little code clean up.
2019-10-06 16:19:21 -04:00
Gorfiend
ab6d37721a Translated using Weblate (Japanese)
Currently translated at 91.8% (650 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/ja/
2019-10-04 07:56:42 +00:00
Andrew Dolgov
4e05008aac update_rss_feed: force cast initial timestamp value to integer 2019-09-30 11:41:07 +03:00
Andrew Dolgov
8e8fd73dbd Merge branch 'weblate-integration' 2019-09-23 10:12:14 +03:00
fox
cc426279a6 Merge branch 'reword-keyboard-help' of fox/tt-rss into master 2019-09-23 05:26:53 +00:00
JustAMacUser
4cc6a773ff Removed redundant text for next/prev article without scroll. 2019-09-23 01:09:11 -04:00
JustAMacUser
2e61551c28 Try to clarify next/prev article keyboard shortcut help. 2019-09-22 15:13:28 -04:00
fox
f1df2c505e Merge branch 'master' of trap000d/tt-rss into master 2019-09-18 15:10:41 +00:00
fox
d2d9bb1fd8 Merge branch 'master' of rodneys_mission/tt-rss into master 2019-09-18 15:09:52 +00:00
Rodney Stromlund
958c4dc124 Removed extra php end tag that was showing in the page title 2019-09-17 09:11:30 -05:00
Aleksandr Beliaev
7a4d5cc724 Fix error "mb_convert_encoding(): Illegal character encoding specified"
modified:   plugins/af_readability/init.php
2019-09-13 09:52:40 +12:00
Andrew Dolgov
b0d67cd3d0 rework previous to pass unformatted timestamp to plugin, and deal with formatting later
also, move timestamp-related debugging output after plugin handler
2019-09-11 14:04:59 +03:00
Andrew Dolgov
94a12b9674 pass formatted entry timestamp to article filters and allow them to modify it 2019-09-11 11:43:40 +03:00
Andrew Dolgov
06393750c7 headline grouping:
1. block grouping for specific feeds where it doesn't make a lot of sense to do so or flat list fits better (archived, recently read)
2. block per-week grouping for feeds where feed-first grouping makes more sense (fresh, starred, published)
2019-08-30 10:16:38 +03:00
Andrew Dolgov
781fe3d636 setScore, selectionSetScore: check for numerical values properly 2019-08-29 12:52:22 +03:00
Андрій Жук
8d00f31e46 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (708 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/uk/
2019-08-28 15:06:44 +00:00
Patrick Ahles
7a04611e98 Translated using Weblate (Dutch)
Currently translated at 100.0% (708 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nl/
2019-08-27 12:06:52 +00:00
Oliver Meyer
e7e35d7313 Translated using Weblate (German)
Currently translated at 100.0% (708 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2019-08-27 12:06:51 +00:00
Patrick Ahles
eaf3acffd7 Translated using Weblate (German)
Currently translated at 100.0% (708 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2019-08-27 12:06:51 +00:00
Oliver Meyer
41b886c51b Translated using Weblate (German)
Currently translated at 100.0% (708 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2019-08-26 11:40:35 +00:00
Patrick Ahles
cac2048e31 Translated using Weblate (German)
Currently translated at 100.0% (708 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/de/
2019-08-26 11:40:34 +00:00
Jan Espen Pedersen
2dcb1b5eef Translated using Weblate (Norwegian Bokmål)
Currently translated at 44.4% (314 of 708 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nb_NO/
2019-08-23 22:06:42 +00:00
Andrew Dolgov
12a542977e makefeedtree: properly calculate feed total amount in no-categories mode 2019-08-21 19:32:27 +03:00
Andrew Dolgov
e887d68f21 af_readability: require php 7.0 2019-08-21 10:05:25 +03:00
Andrew Dolgov
667836ec7c SQL logger: log some parameters 2019-08-20 08:09:05 +03:00
Andrew Dolgov
ae5e08fd30 Merge branch 'master' of git.fakecake.org:tt-rss 2019-08-16 15:29:31 +03:00
Andrew Dolgov
3e4701116d af_readability: add missing file 2019-08-16 15:29:24 +03:00
Andrew Dolgov
88077702f3 Merge branch 'master' of git.fakecake.org:tt-rss 2019-08-16 12:53:51 +03:00
Andrew Dolgov
24f55d5b91 update readability library 2019-08-16 12:53:25 +03:00
Andrew Dolgov
a2d26867e6 pluginhandler/public: report errors with E_USER_WARNING 2019-08-16 09:38:24 +03:00
Andrew Dolgov
d94348421d use clean_filename() instead of basename()/clean() combinations in a bunch of places 2019-08-16 09:31:16 +03:00
Andrew Dolgov
9c366a4811 clean_filename: also remove markup 2019-08-16 09:27:14 +03:00
Andrew Dolgov
e53cd12ffd pluginhandler: better error reporting for incorrect usage 2019-08-16 09:27:05 +03:00
Andrew Dolgov
865c54abcb fix get_method_url() to use correct method parameter 2019-08-15 20:27:21 +03:00
Andrew Dolgov
10c63ed582 pluginhost: add helper methods to get private/public pluginmethod endpoint URLs 2019-08-15 20:23:45 +03:00
Andrew Dolgov
e46ed1ff97 API/getHeadlines: fix order of returned feeds to be consistent with main UI 2019-08-15 19:06:38 +03:00
Andrew Dolgov
0e3b71c535 public/pluginhandler: log invalid requests 2019-08-15 17:17:25 +03:00
Andrew Dolgov
bdf29856fb fix several leftover mentions of old (renamed) class name, duh 2019-08-15 17:12:59 +03:00
Andrew Dolgov
de5669f723 af_zz_imgproxy: rename to af_proxy_http, use priority hook loader 2019-08-15 16:27:53 +03:00
Andrew Dolgov
7f8946f14e pluginhost: implement priority-based system for running hooks 2019-08-15 15:34:09 +03:00
Andrew Dolgov
7ab99f0c32 rebase translations 2019-08-15 13:47:39 +03:00
Andrew Dolgov
5648b836aa HOOK_ARTICLE_IMAGE: allow hooks to modify article content 2019-08-15 10:22:33 +03:00
Andrew Dolgov
75ab1f05f9 DiskCache::rewriteUrls() - remove img[@srcset] 2019-08-15 09:30:28 +03:00
Andrew Dolgov
9d852e052c add HOOK_ARTICLE_IMAGE for Article::get_article_image() 2019-08-15 09:04:42 +03:00
Andrew Dolgov
ffb842f752 Article::get_article_image() - provide cached URLs if possible 2019-08-14 17:21:07 +03:00
Andrew Dolgov
150b040dad Article::get_article_image() - set default to "" instead of "false" 2019-08-14 17:07:01 +03:00
Andrew Dolgov
d4df57e1a4 Article::get_article_image() - also return stream URI if possible 2019-08-14 17:04:14 +03:00
Andrew Dolgov
68e2b05f65 * move get_article_image to Article; implement better og:image detection (similar to android app)
* pass article image to API clients in headlines row object
2019-08-14 16:55:38 +03:00
Andrew Dolgov
26dbe02968 Merge branch 'weblate-integration' 2019-08-14 16:07:16 +03:00
Andrew Dolgov
afc1ddb43a Translated using Weblate (Russian)
Currently translated at 88.8% (645 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/ru/
2019-08-14 10:49:37 +00:00
Edgar Pireyn
274fdd2733 Translated using Weblate (Dutch)
Currently translated at 99.0% (719 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/nl/
2019-08-14 10:49:36 +00:00
Edgar Pireyn
734c305e0c Translated using Weblate (French)
Currently translated at 100.0% (726 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/fr/
2019-08-14 10:49:34 +00:00
Andrew Dolgov
9806a2b5ff bump version_static 2019-08-14 13:38:20 +03:00
Andrew Dolgov
c34726b2b2 consistency: use DiskCache->exists() to check for present files 2019-08-14 12:52:41 +03:00
Andrew Dolgov
6914ad1f74 retire MIN_CACHE_FILE_SIZE 2019-08-14 12:44:50 +03:00
Andrew Dolgov
84974c60a7 RSSUtils::cache_media, cache_enclosures: use DiskCache 2019-08-14 12:15:56 +03:00
Andrew Dolgov
39f459eb04 public/cached_url: forbid sending files with extensions 2019-08-14 10:45:46 +03:00
Andrew Dolgov
d2f1cbfcb1 af_zz_imgproxy: redirect to cached_url (3!!) 2019-08-14 10:10:27 +03:00
Andrew Dolgov
c6ae5fbda1 af_zz_imgproxy: redirect to cached_url if cache already exists so that urls are a bit shorter (2) 2019-08-14 10:01:05 +03:00
Andrew Dolgov
e7edaca4db af_zz_imgproxy: redirect to cached_url if cache already exists so that urls are a bit shorter 2019-08-14 09:58:40 +03:00
Andrew Dolgov
3c075bfd21 DiskCache: more strict checking for input filenames, getUrl() is no longer static 2019-08-14 09:49:18 +03:00
Andrew Dolgov
65450f8a2b Merge branch 'master' of git.tt-rss.org:fox/tt-rss 2019-08-14 08:25:09 +03:00
Andrew Dolgov
b0fbae938d Merge branch 'weblate-integration' 2019-08-14 08:25:01 +03:00
Andrew Dolgov
fdb6066bf6 * HOOK_ENCLOSURE_ENTRY: pass article_id to handler
* DiskCache: multiple fixes; support isWritable() for cache entries, set content-disposition for send()
* public/cached_url: allow selecting files from sub-caches other than images
* plugins/Cache_Starred_Images: rework to use DiskCache, can be enabled per-user, properly handles article enclosures, etc
2019-08-13 16:40:21 +03:00
Andrew Dolgov
bed695b127 DiskCache::expire: support .no-auto-expiry to prevent automatic cache maintenance 2019-08-13 14:18:14 +03:00
Andrew Dolgov
19b9b27662 expire_cached_files to DiskCache::expire() 2019-08-13 14:13:42 +03:00
Vladimir Budylnikov
d0c82d33a7 Translated using Weblate (Russian)
Currently translated at 88.8% (645 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/ru/
2019-08-13 10:03:50 +00:00
Andrew Dolgov
133c2b482b move rewrite_cached_urls to DiskCache::rewriteUrls() 2019-08-13 12:46:57 +03:00
Andrew Dolgov
b1dd38f880 add DiskCache.getUrl() and use it in a bunch of places 2019-08-13 12:39:21 +03:00
Andrew Dolgov
7602819b98 add DiskCache.send; switch af_zz_imgproxy to use DiskCache 2019-08-13 12:20:53 +03:00
Andrew Dolgov
82694bd6ce add DiskCache.isWritable 2019-08-13 12:15:43 +03:00
Andrew Dolgov
86308b30ea add classes/diskcache 2019-08-13 12:04:36 +03:00
Andrew Dolgov
b68db2d02c Merge branch 'weblate-integration' 2019-08-09 15:40:01 +03:00
洪偉翔
9eb43aa74b Translated using Weblate (Chinese (Traditional))
Currently translated at 98.1% (712 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2019-08-08 12:15:03 +00:00
洪偉翔
ae880a2d46 Translated using Weblate (Chinese (Traditional))
Currently translated at 98.1% (712 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2019-08-07 07:15:05 +00:00
Andrew Dolgov
a60297b920 remove import_export plugin (replaced with ttrss-data-migration) 2019-08-06 09:54:12 +03:00
Andrew Dolgov
6825aaff55 update SSL certificate wiki link 2019-08-02 08:03:20 +03:00
Andrew Dolgov
aa40a268f0 parser: support multiple dc:creator elements (returns as comma-separated list) 2019-08-02 06:22:42 +03:00
Andrew Dolgov
4edfb526e1 change version.json endpoint URL 2019-08-01 11:51:27 +03:00
Andrew Dolgov
76bc53a499 Merge branch 'weblate-integration' 2019-07-31 22:20:00 +03:00
洪偉翔
062de4518b Translated using Weblate (Chinese (Traditional))
Currently translated at 86.8% (630 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2019-07-31 13:04:09 +00:00
Marcin Sadowski
779651b9a0 Translated using Weblate (Polish)
Currently translated at 99.6% (723 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/pl/
2019-07-31 13:04:04 +00:00
Andrew Dolgov
d172abb037 scroll handler: also invoke lazy load if last article in buffer is currently active 2019-07-30 16:13:47 +03:00
Andrew Dolgov
c35a618b00 lazy load (infinite scrolling) changes:
1. invoke Headlines.loadMore() if last article row is close to becoming visible instead of relying on headlines-spacer offset to viewport

2. allow one final last lazy load request if incomplete buffer was received to permit some flexibility with unread counters possible changing while request was generated / serving remainder of articles
2019-07-30 15:54:47 +03:00
Andrew Dolgov
8c982679c7 Merge branch 'weblate-integration' 2019-07-30 07:29:43 +03:00
洪偉翔
72ee27e320 Translated using Weblate (Chinese (Traditional))
Currently translated at 82.2% (597 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/zh_Hant/
2019-07-27 10:11:34 +00:00
Eduardo Kalinowski
64dedd3e28 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (726 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/pt_BR/
2019-07-20 18:11:46 +00:00
Roberto Michán Sánchez
ab7d91821a Translated using Weblate (Spanish)
Currently translated at 100.0% (726 of 726 strings)

Translation: Tiny Tiny RSS/messages
Translate-URL: https://weblate.tt-rss.org/projects/tt-rss/messages/es/
2019-07-20 18:11:39 +00:00
Andrew Dolgov
5829ee9498 main toolbar: set order values for main toolbar elements 2019-07-15 13:43:32 +03:00
Andrew Dolgov
e8523733b0 filter dialog: add inline regexp checker 2019-07-12 12:40:42 +03:00
Andrew Dolgov
86a014f23b add placeholder Filters.filterDlgCheckRegExp 2019-07-12 10:47:18 +03:00
3521 changed files with 400376 additions and 155001 deletions

104
.docker/app/Dockerfile Normal file
View File

@@ -0,0 +1,104 @@
ARG PROXY_REGISTRY
FROM ${PROXY_REGISTRY}alpine:3.22
EXPOSE 9000/tcp
ARG ALPINE_MIRROR
ENV SCRIPT_ROOT=/opt/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} ] && \
sed -i.bak "s#dl-cdn.alpinelinux.org#${ALPINE_MIRROR}#" /etc/apk/repositories ; \
apk add --no-cache ca-certificates dcron git postgresql-client rsync sudo tzdata \
php${PHP_SUFFIX} \
$(for p in ctype curl dom exif fileinfo fpm gd iconv intl json mbstring opcache \
openssl pcntl pdo pdo_pgsql pecl-apcu pecl-xdebug phar posix session simplexml sockets sodium tokenizer xml xmlwriter zip; do \
php_pkgs="$php_pkgs php${PHP_SUFFIX}-$p"; \
done; \
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/' \
-e 's/;\(clear_env\) = .*/\1 = no/i' \
-e 's/;\(pm.status_path = \/status\)/\1/i' \
-e 's/;\(pm.status_listen\) = .*/\1 = 9001/i' \
-e 's/^\(user\|group\) = .*/\1 = app/i' \
-e 's/;\(php_admin_value\[error_log\]\) = .*/\1 = \/tmp\/error.log/' \
-e 's/;\(php_admin_flag\[log_errors\]\) = .*/\1 = on/' \
/etc/php${PHP_SUFFIX}/php-fpm.d/www.conf && \
mkdir -p /var/www ${SCRIPT_ROOT}/config.d ${SCRIPT_ROOT}/sql/post-init.d
ARG CI_COMMIT_BRANCH
ENV CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH}
ARG CI_COMMIT_SHORT_SHA
ENV CI_COMMIT_SHORT_SHA=${CI_COMMIT_SHORT_SHA}
ARG CI_COMMIT_TIMESTAMP
ENV CI_COMMIT_TIMESTAMP=${CI_COMMIT_TIMESTAMP}
ARG CI_COMMIT_SHA
ENV CI_COMMIT_SHA=${CI_COMMIT_SHA}
ADD .docker/app/startup.sh ${SCRIPT_ROOT}
ADD .docker/app/update.sh ${SCRIPT_ROOT}
ADD .docker/app/updater.sh ${SCRIPT_ROOT}
ADD .docker/app/dcron.sh ${SCRIPT_ROOT}
ADD .docker/app/backup.sh /etc/periodic/weekly/backup
RUN chmod 0755 ${SCRIPT_ROOT}/*.sh /etc/periodic/weekly/backup
ADD .docker/app/index.php ${SCRIPT_ROOT}
ADD .docker/app/config.docker.php ${SCRIPT_ROOT}
COPY . ${SRC_DIR}
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
ENV OWNER_UID=1000
ENV OWNER_GID=1000
ENV PHP_WORKER_MAX_CHILDREN=5
ENV PHP_WORKER_MEMORY_LIMIT=256M
# these are applied on every startup, if set
ENV ADMIN_USER_PASS=""
# see classes/UserHelper.php ACCESS_LEVEL_*
# setting this to -2 would effectively disable built-in admin user
# unless single user mode is enabled
ENV ADMIN_USER_ACCESS_LEVEL=""
# these are applied unless user already exists
ENV AUTO_CREATE_USER=""
ENV AUTO_CREATE_USER_PASS=""
ENV AUTO_CREATE_USER_ACCESS_LEVEL="0"
ENV AUTO_CREATE_USER_ENABLE_API=""
# TODO: remove prefix from container variables not used by tt-rss itself:
#
# - TTRSS_NO_STARTUP_PLUGIN_UPDATES -> NO_STARTUP_PLUGIN_UPDATES
# - TTRSS_XDEBUG_... -> XDEBUG_...
# don't try to update local plugins on startup
ENV TTRSS_NO_STARTUP_PLUGIN_UPDATES=""
# TTRSS_XDEBUG_HOST defaults to host IP if unset
ENV TTRSS_XDEBUG_ENABLED=""
ENV TTRSS_XDEBUG_HOST=""
ENV TTRSS_XDEBUG_PORT="9000"
ENV TTRSS_DB_HOST="db"
ENV TTRSS_DB_PORT="5432"
ENV TTRSS_PHP_EXECUTABLE="/usr/bin/php${PHP_SUFFIX}"
ENV TTRSS_PLUGINS="auth_internal, note, nginx_xaccel"
CMD ${SCRIPT_ROOT}/startup.sh

31
.docker/app/backup.sh Normal file
View File

@@ -0,0 +1,31 @@
#!/bin/sh -e
DST_DIR=/backups
KEEP_DAYS=28
APP_ROOT=$APP_INSTALL_BASE_DIR/tt-rss
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
echo backing up tt-rss database to $DST_DIR/$DST_FILE...
export PGPASSWORD=$TTRSS_DB_PASS
pg_dump --clean -h $TTRSS_DB_HOST -U $TTRSS_DB_USER $TTRSS_DB_NAME | gzip > $DST_DIR/$DST_FILE
DST_FILE=ttrss-backup-$(date +%Y%m%d).tar.gz
echo backing up tt-rss local directories to $DST_DIR/$DST_FILE...
tar -cz -f $DST_DIR/$DST_FILE $APP_ROOT/*.local \
$APP_ROOT/cache/feed-icons/ \
$APP_ROOT/config.php
echo cleaning up...
find $DST_DIR -type f -name '*.gz' -mtime +$KEEP_DAYS -delete
echo done.
else
echo backup failed: database is not ready.
fi

View File

@@ -0,0 +1,8 @@
<?php
$snippets = glob(getenv("SCRIPT_ROOT")."/config.d/*.php");
foreach ($snippets as $snippet) {
require_once $snippet;
}

5
.docker/app/dcron.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
# https://github.com/dubiousjim/dcron/issues/13
set -e
/usr/sbin/crond "$@"

3
.docker/app/index.php Normal file
View File

@@ -0,0 +1,3 @@
<?php
header("Location: /tt-rss/");
return;

170
.docker/app/startup.sh Normal file
View File

@@ -0,0 +1,170 @@
#!/bin/sh -e
#
# this script initializes the working copy on a persistent volume and starts PHP FPM
#
# 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...
sleep 3
done
# 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
[ -e $DST_DIR ] && rm -f $DST_DIR/.app_is_ready
export PGPASSWORD=$TTRSS_DB_PASS
[ ! -e $APP_INSTALL_BASE_DIR/index.php ] && cp ${SCRIPT_ROOT}/index.php $APP_INSTALL_BASE_DIR
if [ -z $SKIP_RSYNC_ON_STARTUP ]; then
if [ ! -d $DST_DIR ]; then
mkdir -p $DST_DIR
chown $OWNER_UID:$OWNER_GID $DST_DIR
sudo -u app rsync -a --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
chown -R $OWNER_UID:$OWNER_GID $DST_DIR \
/var/log/php${PHP_SUFFIX}
if [ -z "$TTRSS_NO_STARTUP_PLUGIN_UPDATES" ]; then
echo updating all local plugins...
find $DST_DIR/plugins.local -mindepth 1 -maxdepth 1 -type d | while read PLUGIN; do
if [ -d $PLUGIN/.git ]; then
echo updating $PLUGIN...
cd $PLUGIN && \
sudo -u app git config core.filemode false && \
sudo -u app git config pull.rebase false && \
sudo -u app git pull origin main || sudo -u app git pull origin master || echo warning: attempt to update plugin $PLUGIN failed.
fi
done
else
echo skipping local plugin updates, disabled.
fi
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"
# this was previously generated
rm -f $DST_DIR/config.php.bak
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 --update-schema=force-yes
if [ ! -z "$ADMIN_USER_PASS" ]; then
sudo -Eu app php${PHP_SUFFIX} $DST_DIR/update.php --user-set-password "admin:$ADMIN_USER_PASS"
else
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 '')
echo "*****************************************************************************"
echo "* Setting initial built-in admin user password to '$RANDOM_PASS' *"
echo "* If you want to set it manually, use ADMIN_USER_PASS environment variable. *"
echo "*****************************************************************************"
sudo -Eu app php${PHP_SUFFIX} $DST_DIR/update.php --user-set-password "admin:$RANDOM_PASS"
fi
fi
if [ ! -z "$ADMIN_USER_ACCESS_LEVEL" ]; then
sudo -Eu app php${PHP_SUFFIX} $DST_DIR/update.php --user-set-access-level "admin:$ADMIN_USER_ACCESS_LEVEL"
fi
if [ ! -z "$AUTO_CREATE_USER" ]; then
sudo -Eu app /bin/sh -c "php${PHP_SUFFIX} $DST_DIR/update.php --user-exists $AUTO_CREATE_USER ||
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
# TODO: remove || true later
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
rm -f /tmp/error.log && mkfifo /tmp/error.log && chown app:app /tmp/error.log
(tail -q -f /tmp/error.log >> /proc/1/fd/2) &
unset ADMIN_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
exec /usr/sbin/php-fpm${PHP_SUFFIX} --nodaemonize --force-stderr

86
.docker/app/update.sh Normal file
View 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 "$@"

44
.docker/app/updater.sh Normal file
View File

@@ -0,0 +1,44 @@
#!/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)
unset HTTP_PORT
unset HTTP_HOST
unset ADMIN_USER_PASS
unset AUTO_CREATE_USER_PASS
# wait for the app container to delete .app_is_ready and perform rsync, etc.
sleep 30
if ! id app; then
addgroup -g $OWNER_GID app
adduser -D -h $APP_INSTALL_BASE_DIR -G app -u $OWNER_UID app
fi
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...
sleep 3
done
sed -i.bak "s/^\(memory_limit\) = \(.*\)/\1 = ${PHP_WORKER_MEMORY_LIMIT}/" \
/etc/php${PHP_SUFFIX}/php.ini
DST_DIR=$APP_INSTALL_BASE_DIR/tt-rss
while [ ! -s $DST_DIR/config.php -a -e $DST_DIR/.app_is_ready ]; do
echo waiting for app container...
sleep 3
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
sudo -E -u app "${TTRSS_PHP_EXECUTABLE}" $APP_INSTALL_BASE_DIR/tt-rss/update_daemon2.php "$@"

View File

@@ -0,0 +1,5 @@
ARG PROXY_REGISTRY
FROM ${PROXY_REGISTRY}nginxinc/nginx-unprivileged:1-alpine
COPY ./phpdoc /usr/share/nginx/html/ttrss-docs
COPY .docker/phpdoc/redirects.conf /etc/nginx/conf.d/

View File

@@ -0,0 +1,2 @@
port_in_redirect off;
absolute_redirect off;

View File

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

View File

@@ -0,0 +1,78 @@
worker_processes auto;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
error_log /dev/stderr warn;
sendfile on;
index index.php;
resolver ${RESOLVER} valid=5s;
server {
listen 80;
listen [::]:80;
root ${APP_WEB_ROOT};
location ${APP_BASE}/cache {
aio threads;
internal;
}
location ${APP_BASE}/backups {
internal;
}
rewrite ${APP_BASE}/healthz ${APP_BASE}/public.php?op=healthcheck;
# Regular PHP handling (without PATH_INFO)
location ~ \.php$ {
# regex to split $uri to $fastcgi_script_name and $fastcgi_path
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
# Check that the PHP script exists before passing it
try_files $fastcgi_script_name =404;
fastcgi_index index.php;
include fastcgi.conf;
set $backend "${APP_UPSTREAM}:9000";
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
location ~ /plugins\.local/.*/api/.*\.php(/|$) {
# regex to split $uri to $fastcgi_script_name and $fastcgi_path
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
# Check that the PHP script exists before passing it
try_files $fastcgi_script_name =404;
# Bypass the fact that try_files resets $fastcgi_path_info
# see: http://trac.nginx.org/nginx/ticket/321
set $path_info $fastcgi_path_info;
fastcgi_param PATH_INFO $path_info;
fastcgi_index index.php;
include fastcgi.conf;
set $backend "${APP_UPSTREAM}:9000";
fastcgi_pass ${APP_FASTCGI_PASS};
}
location / {
try_files $uri $uri/ =404;
}
}
}

5
.dockerignore Normal file
View File

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

View File

@@ -4,3 +4,6 @@ insert_final_newline = true
[*.php]
indent_style = tab
[*.js]
indent_style = tab

47
.env-dist Normal file
View File

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

300
.eslintrc.js Normal file
View File

@@ -0,0 +1,300 @@
module.exports = {
"env": {
"browser": true,
"es6": true,
"jquery": false,
"webextensions": false
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2020
},
"rules": {
"accessor-pairs": "error",
"array-bracket-newline": "off",
"array-bracket-spacing": "off",
"array-callback-return": "error",
"array-element-newline": "off",
"arrow-body-style": "error",
"arrow-parens": "error",
"arrow-spacing": "error",
"block-scoped-var": "off",
"block-spacing": [
"error",
"always"
],
"brace-style": "off",
"callback-return": "off",
"camelcase": "off",
"capitalized-comments": "off",
"class-methods-use-this": "error",
"comma-dangle": "off",
"comma-spacing": "off",
"comma-style": [
"error",
"last"
],
"complexity": "off",
"computed-property-spacing": [
"error",
"never"
],
"consistent-return": "off",
"consistent-this": "off",
"curly": "off",
"default-case": "off",
"dot-location": "off",
"dot-notation": "off",
"eol-last": "error",
"eqeqeq": "off",
"func-call-spacing": "error",
"func-name-matching": "error",
"func-names": "off",
"func-style": "off",
"function-paren-newline": "off",
"generator-star-spacing": "error",
"global-require": "error",
"guard-for-in": "off",
"handle-callback-err": "off",
"id-blacklist": "error",
"id-length": "off",
"id-match": "error",
"implicit-arrow-linebreak": "off",
"indent": "off",
"indent-legacy": "off",
"init-declarations": "off",
"jsx-quotes": "error",
"key-spacing": "off",
"keyword-spacing": [
"error",
{
"after": true,
"before": true
}
],
"line-comment-position": "off",
"linebreak-style": [
"error",
"unix"
],
"lines-around-comment": "off",
"lines-around-directive": "error",
"lines-between-class-members": "error",
"max-classes-per-file": "off",
"max-depth": "off",
"max-len": "off",
"max-lines": "off",
"max-lines-per-function": "off",
"max-nested-callbacks": "error",
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": [ "warn", { "max" : 2 } ],
"multiline-comment-style": "off",
"multiline-ternary": "off",
"new-cap": "warn",
"new-parens": "error",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "off",
"no-alert": "off",
"no-array-constructor": "error",
"no-async-promise-executor": "off",
"no-await-in-loop": "warn",
"no-bitwise": "off",
"no-buffer-constructor": "error",
"no-caller": "error",
"no-catch-shadow": "off",
"no-confusing-arrow": "error",
"no-continue": "off",
"no-console": "off",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": "off",
"no-empty": [
"error",
{
"allowEmptyCatch": true
}
],
"no-empty-function": "error",
"no-eq-null": "off",
"no-eval": "error",
"no-extend-native": "off",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-extra-parens": "off",
"no-floating-decimal": "error",
"no-implicit-globals": "off",
"no-implied-eval": "off",
"no-inline-comments": "off",
"no-inner-declarations": [
"error",
"functions"
],
"no-invalid-this": "error",
"no-iterator": "error",
"no-label-var": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "off",
"no-magic-numbers": "off",
"no-misleading-character-class": "off",
"no-mixed-operators": "off",
"no-mixed-requires": "error",
"no-multi-assign": "error",
"no-multi-spaces": "off",
"no-multi-str": "error",
"no-multiple-empty-lines": "error",
"no-native-reassign": "error",
"no-negated-condition": "off",
"no-negated-in-lhs": "error",
"no-nested-ternary": "error",
"no-new": "warn",
"no-new-func": "error",
"no-new-object": "off",
"no-new-require": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-path-concat": "error",
"no-plusplus": "off",
"no-process-env": "error",
"no-process-exit": "error",
"no-proto": "error",
"no-prototype-builtins": "warn",
"no-restricted-globals": "error",
"no-restricted-imports": "error",
"no-restricted-modules": "error",
"no-restricted-properties": "error",
"no-restricted-syntax": "error",
"no-return-assign": [
"error",
"except-parens"
],
"no-return-await": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "off",
"no-shadow-restricted-names": "error",
"no-spaced-func": "error",
"no-sync": "error",
"no-tabs": "off",
"no-template-curly-in-string": "error",
"no-ternary": "off",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-undefined": "off",
"no-undef": "warn",
"no-underscore-dangle": "off",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": [
"error",
{
"defaultAssignment": true
}
],
"no-unused-expressions": "off",
"no-unused-vars": "warn",
"no-use-before-define": "off",
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "error",
"no-useless-rename": "error",
"no-useless-return": "off",
"no-var": "warn",
"no-void": "error",
"no-warning-comments": "off",
"no-whitespace-before-property": "error",
"no-with": "error",
"nonblock-statement-body-position": [
"error",
"any"
],
"object-curly-newline": "off",
"object-curly-spacing": "off",
"object-property-newline": "off",
"object-shorthand": "off",
"one-var": "off",
"one-var-declaration-per-line": "error",
"operator-assignment": "off",
"operator-linebreak": [
"error",
"after"
],
"padded-blocks": "off",
"padding-line-between-statements": "error",
"prefer-arrow-callback": "off",
"prefer-const": "error",
"prefer-destructuring": "off",
"prefer-numeric-literals": "error",
"prefer-object-spread": "off",
"prefer-promise-reject-errors": "error",
"prefer-reflect": "off",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "off",
"quote-props": "off",
"quotes": "off",
"radix": [
"error",
"as-needed"
],
"require-atomic-updates": "off",
"require-await": "warn",
"require-jsdoc": "off",
"require-unicode-regexp": "off",
"rest-spread-spacing": "error",
"semi": "off",
"semi-spacing": [
"error",
{
"after": true,
"before": false
}
],
"semi-style": [
"error",
"last"
],
"sort-imports": "error",
"sort-keys": "off",
"sort-vars": "off",
"space-before-blocks": "off",
"space-before-function-paren": "off",
"space-in-parens": "off",
"space-infix-ops": "off",
"space-unary-ops": [
"error",
{
"nonwords": false,
"words": false
}
],
"spaced-comment": "off",
"strict": [
"off",
"never"
],
"switch-colon-spacing": "error",
"symbol-description": "error",
"template-curly-spacing": "error",
"template-tag-spacing": "error",
"unicode-bom": [
"error",
"never"
],
"valid-jsdoc": "error",
"vars-on-top": "off",
"wrap-iife": "error",
"wrap-regex": "error",
"yield-star-spacing": "error",
"yoda": [
"error",
"never"
]
}
};

54
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View 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

View 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
View 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
View File

@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: composer
directory: /
schedule:
interval: weekly

25
.github/pull_request_template.md vendored Normal file
View 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
View 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
View 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

35
.gitignore vendored
View File

@@ -1,21 +1,18 @@
Thumbs.db
/deploy.exclude
/deploy.sh
/.env
/docker-compose.override.yml
/.app_is_ready
/messages.mo
*~
*.DS_Store
#*
.idea/*
plugins.local/*
themes.local/*
config.php
feed-icons/*
cache/*/*
lock/*
tags
cache/htmlpurifier/*/*ser
lib/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/*/*ser
web.config
/.save.cson
/.tags*
/.gutentags
/node_modules
/locale/**/*.po~
/plugins.local/*
/themes.local/*
/config.php
/feed-icons/*
/cache/*/*
/lock/*
/.vscode/settings.json
/vendor/**/.git
/.phpunit.result.cache
/.phpstan-tmp
/.tools/

View File

@@ -1,44 +1,266 @@
phpmd:
image: php:5.6
when: manual
script:
- sh utils/gitlab-ci/php-lint.sh
- curl -o /usr/bin/phpmd -L http://static.phpmd.org/php/2.6.0/phpmd.phar
- chmod +x /usr/bin/phpmd
- sh utils/gitlab-ci/phpmd.sh
schema:
image: fox/selenium-ci
when: manual
script:
- /etc/init.d/postgresql start
- /usr/local/sbin/init-database.sh
- sh ./utils/gitlab-ci/check-schema.sh
phpunit_basic:
image: fox/selenium-ci
when: manual
script:
- /etc/init.d/postgresql start
- /usr/local/sbin/init-database.sh
- sh ./utils/gitlab-ci/check-schema.sh
- cp utils/gitlab-ci/config-template.php config.php
- su -s /bin/bash www-data -c "php ./update.php --debug-feed 1"
- wget -O /usr/bin/phpunit https://phar.phpunit.de/phpunit-5.7.phar
- chmod +x /usr/bin/phpunit
- phpunit tests/*.php
phpunit_functional:
image: fox/selenium-ci
when: manual
script:
- /etc/init.d/postgresql start
- /etc/init.d/nginx start
- /etc/init.d/php5-fpm start
- /usr/local/sbin/init-database.sh
- sh ./utils/gitlab-ci/check-schema.sh
- ln -s `pwd` ../../tt-rss
- cp utils/gitlab-ci/config-template.php config.php
- chmod -R 777 cache lock feed-icons
- /usr/local/sbin/init-selenium.sh
- phpunit tests/functional/*.php
stages:
- lint
- build
- push
- test
- publish
variables:
ESLINT_PATHS: js plugins
REGISTRY_PROJECT: cthulhoo
IMAGE_TAR_FPM: image-fpm.tar
IMAGE_TAR_WEB: image-web.tar
include:
- project: 'ci/ci-templates'
ref: master
file: .ci-build-docker-kaniko.yml
- project: 'ci/ci-templates'
ref: master
file: .ci-registry-push.yml
- project: 'ci/ci-templates'
ref: master
file: .ci-lint-common.yml
- project: 'ci/ci-templates'
ref: master
file: .ci-update-helm-imagetag.yml
phpunit:
extends: .phpunit
variables:
PHPUNIT_ARGS: --exclude integration --coverage-filter classes --coverage-filter include
eslint:
extends: .eslint
phpstan:
extends: .phpstan
ttrss-fpm-pgsql-static:build:
extends: .build-docker-kaniko-no-push
variables:
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/app/Dockerfile
IMAGE_TAR: ${IMAGE_TAR_FPM}
ttrss-fpm-pgsql-static:push-commit-only-gitlab:
extends: .crane-image-registry-push-commit-only-gitlab
variables:
IMAGE_TAR: ${IMAGE_TAR_FPM}
needs:
- job: ttrss-fpm-pgsql-static:build
ttrss-web-nginx:build:
extends: .build-docker-kaniko-no-push
variables:
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/web-nginx/Dockerfile
IMAGE_TAR: ${IMAGE_TAR_WEB}
ttrss-web-nginx:push-commit-only-gitlab:
extends: .crane-image-registry-push-commit-only-gitlab
variables:
IMAGE_TAR: ${IMAGE_TAR_WEB}
needs:
- job: ttrss-web-nginx:build
phpdoc:build:
image: ${PHP_IMAGE}
stage: publish
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- php84 /phpDocumentor.phar -d classes -d include -t phpdoc --visibility=public
artifacts:
paths:
- phpdoc
phpdoc:publish:
extends: .build-docker-kaniko
stage: publish
needs:
- job: phpdoc:build
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $REGISTRY_USER != null && $REGISTRY_PASSWORD != null
variables:
DOCKERFILE: ${CI_PROJECT_DIR}/.docker/phpdoc/Dockerfile
NAME: ttrss-phpdoc
VERSION: latest
phpunit-integration:
image: ${PHP_IMAGE}
variables:
POSTGRES_DB: postgres
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:
- cp tests/integration/feed.xml ${APP_WEB_ROOT}/${APP_BASE}/
- php84 vendor/bin/phpunit --group integration --do-not-cache-result --log-junit phpunit-report.xml --coverage-cobertura phpunit-coverage.xml --coverage-text --colors=never
artifacts:
when: always
reports:
junit: phpunit-report.xml
coverage_report:
coverage_format: cobertura
path: phpunit-coverage.xml
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
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:
extends: phpunit-integration
image: ${SELENIUM_IMAGE}
variables:
SELENIUM_GRID_ENDPOINT: http://selenium:4444/wd/hub
services:
- *svc_db
- *svc_app
- *svc_web
- name: registry.fakecake.org/docker.io/selenium/standalone-chrome:4.32.0-20250515
alias: selenium
script:
- |
for i in `seq 1 10`; do
echo attempt $i...
python3 tests/integration/selenium_test.py && break
sleep 10
done
artifacts:
when: always
reports:
junit: selenium-report.xml
ttrss-fpm-pgsql-static:publish:
stage: publish
extends: .crane-image-registry-push-master
variables:
IMAGE_TAR: ${IMAGE_TAR_FPM}
needs:
- job: ttrss-fpm-pgsql-static:build
- job: phpunit-integration
- job: selenium
ttrss-fpm-pgsql-static:publish-docker-hub:
stage: publish
extends: .crane-image-registry-push-master-docker-hub
variables:
IMAGE_TAR: ${IMAGE_TAR_FPM}
needs:
- job: ttrss-fpm-pgsql-static:build
- job: phpunit-integration
- job: selenium
ttrss-fpm-pgsql-static:publish-gitlab:
stage: publish
extends: .crane-image-registry-push-master-gitlab
variables:
IMAGE_TAR: ${IMAGE_TAR_FPM}
needs:
- job: ttrss-fpm-pgsql-static:build
- job: phpunit-integration
- job: selenium
ttrss-web-nginx:publish:
stage: publish
extends: .crane-image-registry-push-master
variables:
IMAGE_TAR: ${IMAGE_TAR_WEB}
needs:
- job: ttrss-web-nginx:build
- job: phpunit-integration
- job: selenium
ttrss-web-nginx:publish-docker-hub:
stage: publish
extends: .crane-image-registry-push-master-docker-hub
variables:
IMAGE_TAR: ${IMAGE_TAR_WEB}
needs:
- job: ttrss-web-nginx:build
- job: phpunit-integration
- job: selenium
ttrss-web-nginx:publish-gitlab:
stage: publish
extends: .crane-image-registry-push-master-gitlab
variables:
IMAGE_TAR: ${IMAGE_TAR_WEB}
needs:
- job: ttrss-web-nginx:build
- job: phpunit-integration
- job: selenium
update-demo:
stage: publish
extends: .update-helm-imagetag
variables:
CHART_REPO: gitlab.fakecake.org/git/helm-charts/tt-rss.git
CHART_VALUES: values-demo.yaml
ACCESS_TOKEN: ${DEMO_HELM_TOKEN}
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $DEMO_HELM_TOKEN != null
update-prod:
stage: publish
extends: .update-helm-imagetag
variables:
CHART_REPO: gitlab.fakecake.org/git/helm-charts/tt-rss-prod.git
CHART_VALUES: values-prod.yaml
ACCESS_TOKEN: ${PROD_HELM_TOKEN}
rules:
- 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

24
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,24 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"pathMappings": {
"/var/www/html/tt-rss": "${workspaceRoot}",
},
"port": 9000
},
{
"name": "Launch Chrome",
"request": "launch",
"type": "chrome",
"pathMapping": {
"/tt-rss/": "${workspaceFolder}"
},
"urlFilter": "*/tt-rss/*",
"runtimeExecutable": "chrome.exe",
}
]
}

46
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,46 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "phpstan (watcher)",
"isBackground": true,
"problemMatcher": {
"fileLocation": [
"relative",
"${workspaceRoot}"
],
"owner": "phpstan-watcher",
"pattern": {
"regexp": "^/app/(.*?):([0-9\\?]*):(.*)$",
"file": 1,
"line": 2,
"message": 3
},
"background": {
"activeOnStart": true,
"beginsPattern": "Using configuration file",
"endsPattern": "All done"
}
},
"command": "chmod +x ${workspaceRoot}/utils/phpstan-watcher.sh && ${workspaceRoot}/utils/phpstan-watcher.sh"
},
{
"type": "shell",
"label": "phpunit",
"command": "chmod +x ${workspaceRoot}/utils/phpunit.sh && ${workspaceRoot}/utils/phpunit.sh",
"problemMatcher": []
},
{
"type": "gulp",
"task": "default",
"problemMatcher": [],
"label": "gulp: default",
"options": {
"env": {
"PATH": "${workspaceRoot}/node_modules/.bin:$PATH"
}
}
}
]
}

View File

@@ -1,20 +1,5 @@
## Contributing code the right way
Contributions (code, translations, reporting issues, etc.) are welcome.
New user accounts on Gogs are not allowed to fork repositories because of spam. To get
initial fork access, do the following:
1. Register on the forums and on Gogs
2. Create a thread describing your proposed changes in Development subforum while
including your Gogs username
3. If your changes make sense to me, I'll update your repo limit and you'll be able to
fork things and file pull requests
If you already have a fully functional Gogs account it works pretty much like Github:
1. Fork the repository you're interested in
2. Do the needful
3. File a pull request with your changes against master branch
That's it. 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
> [!NOTE]
> The original tt-rss project handled translations via Weblate.
> It's yet to be determined how this project will handle things.

View File

@@ -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
any location, while feeling as close to a real desktop application as possible.
Tiny Tiny RSS (tt-rss) is a free, flexible, open-source, web-based news feed (RSS/Atom/other) reader and aggregator.
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
it under the terms of the GNU General Public License as published by
@@ -20,6 +46,3 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Copyright (c) 2005 Andrew Dolgov (unless explicitly stated otherwise).
Uses Silk icons by Mark James: http://www.famfamfam.com/lab/icons/silk/

View File

@@ -1,91 +1,68 @@
<?php
error_reporting(E_ERROR | E_PARSE);
require_once "../config.php";
set_include_path(dirname(__FILE__) . PATH_SEPARATOR .
dirname(dirname(__FILE__)) . PATH_SEPARATOR .
dirname(dirname(__FILE__)) . "/include" . PATH_SEPARATOR .
set_include_path(__DIR__ . PATH_SEPARATOR .
dirname(__DIR__) . PATH_SEPARATOR .
dirname(__DIR__) . "/include" . PATH_SEPARATOR .
get_include_path());
chdir("..");
define('TTRSS_SESSION_NAME', 'ttrss_api_sid');
define('NO_SESSION_AUTOSTART', true);
require_once "autoload.php";
require_once "db.php";
require_once "db-prefs.php";
require_once "functions.php";
require_once "sessions.php";
ini_set('session.use_cookies', 0);
ini_set("session.gc_maxlifetime", 86400);
ini_set('session.use_cookies', "0");
ini_set("session.gc_maxlifetime", "86400");
define('AUTH_DISABLE_OTP', true);
ob_start();
if (defined('ENABLE_GZIP_OUTPUT') && ENABLE_GZIP_OUTPUT &&
function_exists("ob_gzhandler")) {
$_REQUEST = json_decode((string)file_get_contents("php://input"), true);
ob_start("ob_gzhandler");
} else {
ob_start();
}
$input = file_get_contents("php://input");
if (defined('_API_DEBUG_HTTP_ENABLED') && _API_DEBUG_HTTP_ENABLED) {
// Override $_REQUEST with JSON-encoded data if available
// fallback on HTTP parameters
if ($input) {
$input = json_decode($input, true);
if ($input) $_REQUEST = $input;
}
} else {
// Accept JSON only
$input = json_decode($input, true);
$_REQUEST = $input;
}
if ($_REQUEST["sid"]) {
if (!empty($_REQUEST["sid"])) {
session_id($_REQUEST["sid"]);
@session_start();
} else if (defined('_API_DEBUG_HTTP_ENABLED')) {
@session_start();
session_start();
}
startup_gettext();
if (!init_plugins()) return;
if ($_SESSION["uid"]) {
if (!validate_session()) {
header("Content-Type: text/json");
if (!empty($_SESSION["uid"])) {
if (!Sessions::validate_session()) {
header("Content-Type: application/json");
print json_encode(array("seq" => -1,
"status" => 1,
"content" => array("error" => "NOT_LOGGED_IN")));
print json_encode([
"seq" => -1,
"status" => API::STATUS_ERR,
"content" => [ "error" => API::E_NOT_LOGGED_IN ]
]);
return;
}
load_user_plugins( $_SESSION["uid"]);
UserHelper::load_user_plugins($_SESSION["uid"]);
}
$method = strtolower($_REQUEST["op"]);
$method = strtolower($_REQUEST["op"] ?? "");
$handler = new API($_REQUEST);
if ($handler->before($method)) {
if ($method && method_exists($handler, $method)) {
$handler->$method();
} else if (method_exists($handler, 'index')) {
} else /* if (method_exists($handler, 'index')) */ {
$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();

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>
<xsl:template match="/atom:feed">
<html>
<head>
<title><xsl:value-of select="atom:title"/></title>
<link rel="stylesheet" type="text/css" href="css/default.css"/>
<script language="javascript" src="lib/xsl_mop-up.js"></script>
</head>
<body onload="go_decoding()" class="ttrss_utility">
<div id="cometestme" style="display:none;">
<xsl:text disable-output-escaping="yes">&amp;amp;</xsl:text>
</div>
<div class="rss">
<h1><xsl:value-of select="atom:title"/></h1>
<p class="description">This feed has been exported from
<a target="_new" class="extlink" href="http://tt-rss.org">Tiny Tiny RSS</a>.
It contains the following items:</p>
<xsl:for-each select="atom:entry">
<h2><a target="_new" href="{atom:link/@href}"><xsl:value-of select="atom:title"/></a></h2>
<div name="decodeme" class="content">
<xsl:value-of select="atom:content" disable-output-escaping="yes"/>
</div>
<xsl:if test="enclosure">
<p><a href="{enclosure/@url}">Extra...</a></p>
</xsl:if>
</xsl:for-each>
</div>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View File

@@ -1,24 +1,11 @@
<?php
set_include_path(dirname(__FILE__) ."/include" . PATH_SEPARATOR .
set_include_path(__DIR__ ."/include" . PATH_SEPARATOR .
get_include_path());
/* remove ill effects of magic quotes */
if (get_magic_quotes_gpc()) {
function stripslashes_deep($value) {
$value = is_array($value) ?
array_map('stripslashes_deep', $value) : stripslashes($value);
return $value;
}
$_POST = array_map('stripslashes_deep', $_POST);
$_GET = array_map('stripslashes_deep', $_GET);
$_COOKIE = array_map('stripslashes_deep', $_COOKIE);
$_REQUEST = array_map('stripslashes_deep', $_REQUEST);
}
$op = $_REQUEST["op"];
@$method = $_REQUEST['subop'] ? $_REQUEST['subop'] : $_REQUEST["method"];
$op = $_REQUEST['op'] ?? '';
$method = !empty($_REQUEST['subop']) ?
$_REQUEST['subop'] :
$_REQUEST["method"] ?? false;
if (!$method)
$method = 'index';
@@ -27,52 +14,53 @@
/* Public calls compatibility shim */
$public_calls = array("globalUpdateFeeds", "rss", "getUnread", "getProfiles", "share",
"fbexport", "logout", "pubsub");
$public_calls = array("rss", "getUnread", "getProfiles", "share");
if (array_search($op, $public_calls) !== false) {
header("Location: public.php?" . $_SERVER['QUERY_STRING']);
return;
}
@$csrf_token = $_REQUEST['csrf_token'];
$csrf_token = $_POST['csrf_token'] ?? "";
require_once "autoload.php";
require_once "sessions.php";
require_once "functions.php";
require_once "config.php";
require_once "db.php";
require_once "db-prefs.php";
$op = (string)clean($op);
$method = (string)clean($method);
startup_gettext();
$script_started = microtime(true);
if (!init_plugins()) return;
header("Content-Type: text/json; charset=utf-8");
if (ENABLE_GZIP_OUTPUT && function_exists("ob_gzhandler")) {
ob_start("ob_gzhandler");
if (!init_plugins()) {
return;
}
if (SINGLE_USER_MODE) {
authenticate_user( "admin", null);
header("Content-Type: application/json; charset=utf-8");
if (Config::get(Config::SINGLE_USER_MODE)) {
UserHelper::authenticate("admin", null);
}
if ($_SESSION["uid"]) {
if (!validate_session()) {
header("Content-Type: text/json");
print error_json(6);
if (!empty($_SESSION["uid"])) {
if (!Sessions::validate_session()) {
header("Content-Type: application/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
return;
}
load_user_plugins( $_SESSION["uid"]);
UserHelper::load_user_plugins($_SESSION["uid"]);
}
if (Config::is_migration_needed()) {
print Errors::to_json(Errors::E_SCHEMA_MISMATCH);
return;
}
$purge_intervals = array(
0 => __("Use default"),
-1 => __("Never purge"),
5 => __("1 week old"),
7 => __("1 week old"),
14 => __("2 weeks old"),
31 => __("1 month old"),
60 => __("2 months old"),
@@ -99,49 +87,93 @@
1440 => __("Daily"),
10080 => __("Weekly"));
$access_level_names = array(
0 => __("User"),
5 => __("Power User"),
10 => __("Administrator"));
$access_level_names = [
UserHelper::ACCESS_LEVEL_DISABLED => __("Disabled"),
UserHelper::ACCESS_LEVEL_READONLY => __("Read Only"),
UserHelper::ACCESS_LEVEL_USER => __("User"),
UserHelper::ACCESS_LEVEL_POWERUSER => __("Power User"),
UserHelper::ACCESS_LEVEL_ADMIN => __("Administrator")
];
$op = str_replace("-", "_", $op);
// shortcut syntax for plugin methods (?op=plugin--pmethod&...params)
/* if (str_contains($op, PluginHost::PUBLIC_METHOD_DELIMITER)) {
list ($plugin, $pmethod) = explode(PluginHost::PUBLIC_METHOD_DELIMITER, $op, 2);
// TODO: better implementation that won't modify $_REQUEST
$_REQUEST["plugin"] = $plugin;
$method = $pmethod;
$op = "pluginhandler";
} */
// $op = str_replace(, "_", $op);
$override = PluginHost::getInstance()->lookup_handler($op, $method);
if (class_exists($op) || $override) {
if (str_starts_with($method, "_")) {
user_error("Refusing to invoke method $method of handler $op which starts with underscore.", E_USER_WARNING);
header("Content-Type: application/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
return;
}
if ($override) {
$handler = $override;
} else {
$handler = new $op($_REQUEST);
$reflection = new ReflectionClass($op);
$handler = $reflection->newInstanceWithoutConstructor();
}
if ($handler && implements_interface($handler, 'IHandler')) {
if (implements_interface($handler, 'IHandler')) {
$handler->__construct($_REQUEST);
if (validate_csrf($csrf_token) || $handler->csrf_ignore($method)) {
if ($handler->before($method)) {
$before = $handler->before($method);
if ($before) {
if ($method && method_exists($handler, $method)) {
$handler->$method();
$reflection = new ReflectionMethod($handler, $method);
if ($reflection->getNumberOfRequiredParameters() == 0) {
$handler->$method();
} else {
user_error("Refusing to invoke method $method of handler $op which has required parameters.", E_USER_WARNING);
header("Content-Type: application/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
}
} else {
if (method_exists($handler, "catchall")) {
$handler->catchall($method);
} else {
header("Content-Type: application/json");
print Errors::to_json(Errors::E_UNKNOWN_METHOD, ["info" => get_class($handler) . "->$method"]);
}
}
$handler->after();
return;
} else {
header("Content-Type: text/json");
print error_json(6);
header("Content-Type: application/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
return;
}
} else {
header("Content-Type: text/json");
print error_json(6);
user_error("Refusing to invoke method $method of handler $op with invalid CSRF token.", E_USER_WARNING);
header("Content-Type: application/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
return;
}
}
}
header("Content-Type: text/json");
print error_json(13);
header("Content-Type: application/json");
print Errors::to_json(Errors::E_UNKNOWN_METHOD, [ "info" => (isset($handler) ? get_class($handler) : "UNKNOWN:".$op) . "->$method"]);
?>

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

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

948
classes/API.php Normal file
View File

@@ -0,0 +1,948 @@
<?php
class API extends Handler {
const API_LEVEL = 23;
const STATUS_OK = 0;
const STATUS_ERR = 1;
const E_API_DISABLED = "API_DISABLED";
const E_NOT_LOGGED_IN = "NOT_LOGGED_IN";
const E_LOGIN_ERROR = "LOGIN_ERROR";
const E_INCORRECT_USAGE = "INCORRECT_USAGE";
const E_UNKNOWN_METHOD = "UNKNOWN_METHOD";
const E_OPERATION_FAILED = "E_OPERATION_FAILED";
const E_NOT_FOUND = "E_NOT_FOUND";
private ?int $seq = null;
/**
* @param array<int|string, mixed> $reply
*/
private function _wrap(int $status, array $reply): bool {
print json_encode([
"seq" => $this->seq,
"status" => $status,
"content" => $reply
]);
return true;
}
function before(string $method): bool {
if (parent::before($method)) {
header("Content-Type: application/json");
if (empty($_SESSION["uid"]) && $method != "login" && $method != "isloggedin") {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_NOT_LOGGED_IN));
return false;
}
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));
return false;
}
$this->seq = (int) clean($_REQUEST['seq'] ?? 0);
return true;
}
return false;
}
function getVersion(): bool {
$rv = array("version" => Config::get_version());
return $this->_wrap(self::STATUS_OK, $rv);
}
function getApiLevel(): bool {
$rv = array("level" => self::API_LEVEL);
return $this->_wrap(self::STATUS_OK, $rv);
}
function login(): bool {
if (session_status() == PHP_SESSION_ACTIVE) {
session_destroy();
}
session_start();
$login = clean($_REQUEST["user"]);
$password = clean($_REQUEST["password"]);
if (Config::get(Config::SINGLE_USER_MODE)) $login = "admin";
if ($uid = UserHelper::find_user_by_login($login)) {
if (Prefs::get(Prefs::ENABLE_API_ACCESS, $uid)) {
if (UserHelper::authenticate($login, $password, false, Auth_Base::AUTH_SERVICE_API)) {
// needed for _get_config()
UserHelper::load_user_plugins($_SESSION['uid']);
return $this->_wrap(self::STATUS_OK, array("session_id" => session_id(),
"config" => $this->_get_config(),
"api_level" => self::API_LEVEL));
} else {
return $this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
}
} else {
return $this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED));
}
}
return $this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
}
function logout(): bool {
UserHelper::logout();
return $this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
function isLoggedIn(): bool {
return $this->_wrap(self::STATUS_OK, array("status" => (bool)($_SESSION["uid"] ?? '')));
}
function getUnread(): bool {
$feed_id = clean($_REQUEST["feed_id"] ?? "");
$is_cat = self::_param_to_bool($_REQUEST["is_cat"] ?? false);
if ($feed_id) {
return $this->_wrap(self::STATUS_OK, array("unread" => Feeds::_get_counters($feed_id, $is_cat, true)));
} else {
return $this->_wrap(self::STATUS_OK, array("unread" => Feeds::_get_global_unread()));
}
}
/* Method added for ttrss-reader for Android */
function getCounters(): bool {
return $this->_wrap(self::STATUS_OK, Counters::get_all());
}
function getFeeds(): bool {
$cat_id = (int) clean($_REQUEST["cat_id"]);
$unread_only = self::_param_to_bool($_REQUEST["unread_only"] ?? false);
$limit = (int) clean($_REQUEST["limit"] ?? 0);
$offset = (int) clean($_REQUEST["offset"] ?? 0);
$include_nested = self::_param_to_bool($_REQUEST["include_nested"] ?? false);
$feeds = $this->_api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested);
return $this->_wrap(self::STATUS_OK, $feeds);
}
function getCategories(): bool {
$unread_only = self::_param_to_bool($_REQUEST["unread_only"] ?? false);
$enable_nested = self::_param_to_bool($_REQUEST["enable_nested"] ?? false);
$include_empty = self::_param_to_bool($_REQUEST["include_empty"] ?? false);
// TODO do not return empty categories, return Uncategorized and standard virtual cats
$categories = ORM::for_table('ttrss_feed_categories')
->select_many('id', 'title', 'order_id')
->select_many_expr([
'num_feeds' => '(SELECT COUNT(id) FROM ttrss_feeds WHERE ttrss_feed_categories.id IS NOT NULL AND cat_id = ttrss_feed_categories.id)',
'num_cats' => '(SELECT COUNT(id) FROM ttrss_feed_categories AS c2 WHERE c2.parent_cat = ttrss_feed_categories.id)',
])
->where('owner_uid', $_SESSION['uid']);
if ($enable_nested) {
$categories->where_null('parent_cat');
}
$cats = [];
foreach ($categories->find_many() as $category) {
if ($include_empty || $category->num_feeds > 0 || $category->num_cats > 0) {
$unread = Feeds::_get_counters($category->id, true, true);
if ($enable_nested)
$unread += Feeds::_get_cat_children_unread($category->id);
if ($unread || !$unread_only) {
array_push($cats, [
'id' => (int) $category->id,
'title' => $category->title,
'unread' => (int) $unread,
'order_id' => (int) $category->order_id,
]);
}
}
}
foreach ([Feeds::CATEGORY_LABELS, Feeds::CATEGORY_SPECIAL, Feeds::CATEGORY_UNCATEGORIZED] as $cat_id) {
if ($include_empty || !$this->_is_cat_empty($cat_id)) {
$unread = Feeds::_get_counters($cat_id, true, true);
if ($unread || !$unread_only) {
array_push($cats, [
'id' => $cat_id,
'title' => Feeds::_get_cat_title($cat_id, $_SESSION['uid']),
'unread' => (int) $unread,
]);
}
}
}
return $this->_wrap(self::STATUS_OK, $cats);
}
function getHeadlines(): bool {
$feed_id = clean($_REQUEST["feed_id"] ?? "");
if (!empty($feed_id) || is_numeric($feed_id)) { // is_numeric for feed_id "0"
$limit = (int)clean($_REQUEST["limit"] ?? 0 );
if (!$limit || $limit >= 200) $limit = 200;
$offset = (int)clean($_REQUEST["skip"] ?? 0);
$filter = clean($_REQUEST["filter"] ?? "");
$is_cat = self::_param_to_bool($_REQUEST["is_cat"] ?? false);
$show_excerpt = self::_param_to_bool($_REQUEST["show_excerpt"] ?? false);
$show_content = self::_param_to_bool($_REQUEST["show_content"] ?? false);
/* all_articles, unread, adaptive, marked, updated */
$view_mode = clean($_REQUEST["view_mode"] ?? null);
$include_attachments = self::_param_to_bool($_REQUEST["include_attachments"] ?? false);
$since_id = (int)clean($_REQUEST["since_id"] ?? 0);
$include_nested = self::_param_to_bool($_REQUEST["include_nested"] ?? false);
$sanitize_content = self::_param_to_bool($_REQUEST["sanitize"] ?? true);
$force_update = self::_param_to_bool($_REQUEST["force_update"] ?? false);
$has_sandbox = self::_param_to_bool($_REQUEST["has_sandbox"] ?? false);
$excerpt_length = (int)clean($_REQUEST["excerpt_length"] ?? 0);
$check_first_id = (int)clean($_REQUEST["check_first_id"] ?? 0);
$include_header = self::_param_to_bool($_REQUEST["include_header"] ?? false);
$_SESSION['hasSandbox'] = $has_sandbox;
list($override_order, $skip_first_id_check) = Feeds::_order_to_override_query(clean($_REQUEST["order_by"] ?? ""));
/* do not rely on params below */
$search = clean($_REQUEST["search"] ?? "");
list($headlines, $headlines_header) = $this->_api_get_headlines($feed_id, $limit, $offset,
$filter, $is_cat, $show_excerpt, $show_content, $view_mode, $override_order,
$include_attachments, $since_id, $search,
$include_nested, $sanitize_content, $force_update, $excerpt_length, $check_first_id, $skip_first_id_check);
if ($include_header) {
return $this->_wrap(self::STATUS_OK, array($headlines_header, $headlines));
} else {
return $this->_wrap(self::STATUS_OK, $headlines);
}
} else {
return $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
}
}
function updateArticle(): bool {
$article_ids = array_filter(explode(",", clean($_REQUEST["article_ids"] ?? "")));
$mode = (int) clean($_REQUEST["mode"]);
$data = clean($_REQUEST["data"] ?? "");
$field_raw = (int)clean($_REQUEST["field"]);
$field = "";
$additional_fields = "";
switch ($field_raw) {
case 0:
$field = "marked";
$additional_fields = ",last_marked = NOW()";
break;
case 1:
$field = "published";
$additional_fields = ",last_published = NOW()";
break;
case 2:
$field = "unread";
$additional_fields = ",last_read = NOW()";
break;
case 3:
$field = "note";
break;
case 4:
$field = "score";
break;
};
$set_to = match ($mode) {
0 => 'false',
1 => 'true',
2 => "NOT $field",
default => null,
};
if ($field == 'note')
$set_to = $this->pdo->quote($data);
elseif ($field == 'score')
$set_to = (int) $data;
if ($field && $set_to && count($article_ids) > 0) {
$article_qmarks = arr_qmarks($article_ids);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
$field = $set_to $additional_fields
WHERE ref_id IN ($article_qmarks) AND owner_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();
return $this->_wrap(self::STATUS_OK, array("status" => "OK",
"updated" => $num_updated));
} else {
return $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
}
}
function getArticle(): bool {
$article_ids = array_filter(explode(',', clean($_REQUEST['article_id'] ?? '')));
$sanitize_content = self::_param_to_bool($_REQUEST['sanitize'] ?? true);
if (count($article_ids)) {
$entries = ORM::for_table('ttrss_entries')
->table_alias('e')
->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')
->select_many_expr([
'updated' => 'SUBSTRING_FOR_DATE(updated,1,16)',
'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)',
'hide_images' => '(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id)',
])
->join('ttrss_user_entries', [ 'ue.ref_id', '=', 'e.id'], 'ue')
->where_in('e.id', array_map('intval', $article_ids))
->where('ue.owner_uid', $_SESSION['uid'])
->find_many();
$articles = [];
foreach ($entries as $entry) {
$article = [
'id' => $entry->id,
'guid' => $entry->guid,
'title' => $entry->title,
'link' => $entry->link,
'labels' => Article::_get_labels($entry->id),
'unread' => self::_param_to_bool($entry->unread),
'marked' => self::_param_to_bool($entry->marked),
'published' => self::_param_to_bool($entry->published),
'comments' => $entry->comments,
'author' => $entry->author,
'updated' => (int) strtotime($entry->updated ?? ''),
'feed_id' => $entry->feed_id,
'attachments' => Article::_get_enclosures($entry->id),
'score' => (int) $entry->score,
'feed_title' => $entry->feed_title,
'note' => $entry->note,
'lang' => $entry->lang,
];
if ($sanitize_content) {
$article['content'] = Sanitizer::sanitize(
$entry->content,
self::_param_to_bool($entry->hide_images),
null, $entry->site_url, null, $entry->id);
} else {
$article['content'] = $entry->content;
}
$hook_object = ['article' => &$article];
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_API,
function ($result) use (&$article) {
$article = $result;
},
$hook_object);
$article['content'] = DiskCache::rewrite_urls($article['content']);
array_push($articles, $article);
}
return $this->_wrap(self::STATUS_OK, $articles);
} else {
return $this->_wrap(self::STATUS_ERR, ['error' => self::E_INCORRECT_USAGE]);
}
}
/**
* @see RPC::_make_init_params()
* @see RPC::_make_runtime_info()
* @return array<string, array<string, string>|bool|int|string>
*/
private function _get_config(): array {
return [
'custom_sort_types' => $this->_get_custom_sort_types(),
'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(),
];
}
function getConfig(): bool {
$config = $this->_get_config();
return $this->_wrap(self::STATUS_OK, $config);
}
function updateFeed(): bool {
$feed_id = (int) clean($_REQUEST["feed_id"]);
if (!ini_get("open_basedir")) {
RSSUtils::update_rss_feed($feed_id);
}
return $this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
function catchupFeed(): bool {
$feed_id = clean($_REQUEST["feed_id"]);
$is_cat = self::_param_to_bool($_REQUEST["is_cat"] ?? false);
$mode = clean($_REQUEST["mode"] ?? "");
$search_query = clean($_REQUEST["search_query"] ?? "");
$search_lang = clean($_REQUEST["search_lang"] ?? "");
if (!in_array($mode, ["all", "1day", "1week", "2week"]))
$mode = "all";
Feeds::_catchup($feed_id, $is_cat, $_SESSION["uid"], $mode, [$search_query, $search_lang]);
return $this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
function getPref(): bool {
$pref_name = clean($_REQUEST["pref_name"]);
return $this->_wrap(self::STATUS_OK, array("value" => Prefs::get($pref_name, $_SESSION["uid"], $_SESSION["profile"] ?? null)));
}
function getLabels(): bool {
$article_id = (int)clean($_REQUEST['article_id'] ?? -1);
$rv = [];
$labels = ORM::for_table('ttrss_labels2')
->where('owner_uid', $_SESSION['uid'])
->order_by_asc('caption')
->find_many();
if ($article_id)
$article_labels = Article::_get_labels($article_id);
else
$article_labels = [];
foreach ($labels as $label) {
$checked = false;
foreach ($article_labels as $al) {
if (Labels::feed_to_label_id($al[0]) == $label->id) {
$checked = true;
break;
}
}
array_push($rv, [
'id' => (int) Labels::label_to_feed_id($label->id),
'caption' => $label->caption,
'fg_color' => $label->fg_color,
'bg_color' => $label->bg_color,
'checked' => $checked,
]);
}
return $this->_wrap(self::STATUS_OK, $rv);
}
function setArticleLabel(): bool {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$label_id = (int) clean($_REQUEST['label_id']);
$assign = self::_param_to_bool(clean($_REQUEST['assign']));
$label = Labels::find_caption(Labels::feed_to_label_id($label_id), $_SESSION["uid"]);
$num_updated = 0;
if ($label) {
foreach ($article_ids as $id) {
if ($assign)
Labels::add_article((int)$id, $label, $_SESSION["uid"]);
else
Labels::remove_article((int)$id, $label, $_SESSION["uid"]);
++$num_updated;
}
}
return $this->_wrap(self::STATUS_OK, array("status" => "OK",
"updated" => $num_updated));
}
function index(string $method): bool {
$plugin = PluginHost::getInstance()->get_api_method(strtolower($method));
if ($plugin && method_exists($plugin, $method)) {
$reply = $plugin->$method();
return $this->_wrap($reply[0], $reply[1]);
} else {
return $this->_wrap(self::STATUS_ERR, array("error" => self::E_UNKNOWN_METHOD, "method" => $method));
}
}
function shareToPublished(): bool {
$title = clean($_REQUEST["title"]);
$url = clean($_REQUEST["url"]);
$sanitize_content = self::_param_to_bool($_REQUEST["sanitize"] ?? true);
if ($sanitize_content)
$content = clean($_REQUEST["content"]);
else
$content = $_REQUEST["content"];
if (Article::_create_published_article($title, $url, $content, "", $_SESSION["uid"])) {
return $this->_wrap(self::STATUS_OK, array("status" => 'OK'));
} else {
return $this->_wrap(self::STATUS_ERR, array("error" => self::E_OPERATION_FAILED));
}
}
/**
* @return array<int, array{'id': int, 'title': string, 'unread': int, 'cat_id': int}>
*/
private static function _api_get_feeds(int $cat_id, bool $unread_only, int $limit, int $offset, bool $include_nested = false): array {
$feeds = [];
/* Labels */
/* API only: -4 (Feeds::CATEGORY_ALL) All feeds, including virtual feeds */
if ($cat_id == Feeds::CATEGORY_ALL || $cat_id == Feeds::CATEGORY_LABELS) {
$counters = Counters::get_labels();
foreach (array_values($counters) as $cv) {
$unread = $cv['counter'];
if ($unread || !$unread_only) {
$row = [
'id' => (int) $cv['id'],
'title' => $cv['description'],
'unread' => $cv['counter'],
'cat_id' => Feeds::CATEGORY_LABELS,
];
array_push($feeds, $row);
}
}
}
/* Virtual feeds */
foreach (PluginHost::getInstance()->get_feeds(Feeds::CATEGORY_SPECIAL) as $feed) {
if (!implements_interface($feed['sender'], 'IVirtualFeed'))
continue;
/** @var IVirtualFeed $feed['sender'] */
$unread = $feed['sender']->get_unread($feed['id']);
if ($unread || !$unread_only) {
$row = [
'id' => PluginHost::pfeed_to_feed_id($feed['id']),
'title' => $feed['title'],
'unread' => $unread,
'cat_id' => Feeds::CATEGORY_SPECIAL,
];
array_push($feeds, $row);
}
}
if ($cat_id == Feeds::CATEGORY_ALL || $cat_id == Feeds::CATEGORY_SPECIAL) {
foreach ([Feeds::FEED_STARRED, Feeds::FEED_PUBLISHED, Feeds::FEED_FRESH,
Feeds::FEED_ALL, Feeds::FEED_RECENTLY_READ, Feeds::FEED_ARCHIVED] as $i) {
$unread = Feeds::_get_counters($i, false, true);
if ($unread || !$unread_only) {
$title = Feeds::_get_title($i, $_SESSION['uid']);
$row = [
'id' => $i,
'title' => $title,
'unread' => $unread,
'cat_id' => Feeds::CATEGORY_SPECIAL,
];
array_push($feeds, $row);
}
}
}
/* Child cats */
if ($include_nested && $cat_id) {
$categories = ORM::for_table('ttrss_feed_categories')
->where(['parent_cat' => $cat_id, 'owner_uid' => $_SESSION['uid']])
->order_by_asc('order_id')
->order_by_asc('title')
->find_many();
foreach ($categories as $category) {
$unread = Feeds::_get_counters($category->id, true, true) +
Feeds::_get_cat_children_unread($category->id);
if ($unread || !$unread_only) {
$row = [
'id' => (int) $category->id,
'title' => $category->title,
'unread' => $unread,
'is_cat' => true,
'order_id' => (int) $category->order_id,
];
array_push($feeds, $row);
}
}
}
/* Real feeds */
/* 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')
->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')
->where('owner_uid', $_SESSION['uid'])
->order_by_asc('order_id')
->order_by_asc('title');
if ($limit) $feeds_obj->limit($limit);
if ($offset) $feeds_obj->offset($offset);
if ($cat_id != Feeds::CATEGORY_ALL_EXCEPT_VIRTUAL && $cat_id != Feeds::CATEGORY_ALL) {
$feeds_obj->where_raw('(cat_id = ? OR (? = 0 AND cat_id IS NULL))', [$cat_id, $cat_id]);
}
foreach ($feeds_obj->find_many() as $feed) {
$unread = Feeds::_get_counters($feed->id, false, true);
$has_icon = Feeds::_has_icon($feed->id);
if ($unread || !$unread_only) {
$row = [
'feed_url' => $feed->feed_url,
'title' => $feed->title,
'id' => (int) $feed->id,
'unread' => (int) $unread,
'has_icon' => $has_icon,
'cat_id' => (int) $feed->cat_id,
'last_updated' => (int) strtotime($feed->last_updated ?? ''),
'order_id' => (int) $feed->order_id,
'last_error' => $feed->last_error,
'update_interval' => (int) $feed->update_interval,
];
array_push($feeds, $row);
}
}
return $feeds;
}
/**
* @return array{0: array<int, array<string, mixed>>, 1: array<string, mixed>} $headlines, $headlines_header
*/
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,
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 $skip_first_id_check = false): array {
if ($force_update && is_numeric($feed_id) && $feed_id > 0) {
// Update the feed if required with some basic flood control
$feed = ORM::for_table('ttrss_feeds')
->select_many('id', 'cache_images')
->select_expr('SUBSTRING_FOR_DATE(last_updated,1,19)', 'last_updated')
->find_one($feed_id);
if ($feed) {
$last_updated = strtotime($feed->last_updated ?? '');
$cache_images = self::_param_to_bool($feed->cache_images);
if (!$cache_images && time() - $last_updated > 120) {
RSSUtils::update_rss_feed($feed_id, true);
} else {
$feed->last_updated = '1970-01-01';
$feed->last_update_started = '1970-01-01';
$feed->save();
}
}
}
$qfh_ret = [];
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);
$handler = PluginHost::getInstance()->get_feed_handler($pfeed_id);
if ($handler) {
$params = array(
"feed" => $feed_id,
"limit" => $limit,
"view_mode" => $view_mode,
"cat_view" => $is_cat,
"search" => $search,
"override_order" => $order,
"offset" => $offset,
"since_id" => 0,
"include_children" => $include_nested,
"check_first_id" => $check_first_id,
"skip_first_id_check" => $skip_first_id_check
);
$qfh_ret = $handler->get_headlines($pfeed_id, $params);
}
} else {
$params = array(
"feed" => $feed_id,
"limit" => $limit,
"view_mode" => $view_mode,
"cat_view" => $is_cat,
"search" => $search,
"override_order" => $order,
"offset" => $offset,
"since_id" => $since_id,
"include_children" => $include_nested,
"check_first_id" => $check_first_id,
"skip_first_id_check" => $skip_first_id_check
);
$qfh_ret = Feeds::_get_headlines($params);
}
$result = $qfh_ret[0];
$feed_title = $qfh_ret[1];
$first_id = $qfh_ret[6];
$headlines = array();
$headlines_header = array(
'id' => $feed_id,
'first_id' => $first_id,
'is_cat' => $is_cat);
if (!is_numeric($result)) {
while ($line = $result->fetch()) {
$line["content_preview"] = truncate_string(strip_tags($line["content"]), $excerpt_length);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
function ($result) use (&$line) {
$line = $result;
},
$line, $excerpt_length);
$is_updated = ($line["last_read"] == "" &&
($line["unread"] != "t" && $line["unread"] != "1"));
$tags = explode(",", $line["tag_cache"]);
$label_cache = $line["label_cache"];
$labels = false;
if ($label_cache) {
$label_cache = json_decode($label_cache, true);
if ($label_cache) {
if (($label_cache["no-labels"] ?? 0) == 1)
$labels = [];
else
$labels = $label_cache;
}
}
if (!is_array($labels)) $labels = Article::_get_labels($line["id"]);
$headline_row = array(
"id" => (int)$line["id"],
"guid" => $line["guid"],
"unread" => self::_param_to_bool($line["unread"]),
"marked" => self::_param_to_bool($line["marked"]),
"published" => self::_param_to_bool($line["published"]),
"updated" => (int)strtotime($line["updated"] ?? ''),
"is_updated" => $is_updated,
"title" => $line["title"],
"link" => $line["link"],
"feed_id" => $line["feed_id"] ? $line['feed_id'] : 0,
"tags" => $tags,
);
$enclosures = Article::_get_enclosures($line['id']);
if ($include_attachments)
$headline_row['attachments'] = $enclosures;
if ($show_excerpt)
$headline_row["excerpt"] = $line["content_preview"];
if ($show_content) {
if ($sanitize_content) {
$headline_row["content"] = Sanitizer::sanitize(
$line["content"],
self::_param_to_bool($line['hide_images']),
null, $line["site_url"], null, $line["id"]);
} else {
$headline_row["content"] = $line["content"];
}
}
// unify label output to ease parsing
if (($labels["no-labels"] ?? 0) == 1) $labels = [];
$headline_row["labels"] = $labels;
$headline_row["feed_title"] = $line["feed_title"] ?? $feed_title;
$headline_row["comments_count"] = (int)$line["num_comments"];
$headline_row["comments_link"] = $line["comments"];
$headline_row["always_display_attachments"] = self::_param_to_bool($line["always_display_enclosures"]);
$headline_row["author"] = $line["author"];
$headline_row["score"] = (int)$line["score"];
$headline_row["note"] = $line["note"];
$headline_row["lang"] = $line["lang"];
$headline_row["site_url"] = $line["site_url"];
if ($show_content) {
$hook_object = ["headline" => &$headline_row];
list ($flavor_image, $flavor_stream, $flavor_kind) = Article::_get_image($enclosures,
$line["content"], // unsanitized
$line["site_url"] ?? "", // could be null if archived article
$headline_row);
$headline_row["flavor_image"] = $flavor_image;
$headline_row["flavor_stream"] = $flavor_stream;
/* optional */
if ($flavor_kind)
$headline_row["flavor_kind"] = $flavor_kind;
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_API,
function ($result) use (&$headline_row) {
$headline_row = $result;
},
$hook_object);
$headline_row["content"] = DiskCache::rewrite_urls($headline_row['content']);
}
array_push($headlines, $headline_row);
}
} else if ($result == -1) {
$headlines_header['first_id_changed'] = true;
}
return array($headlines, $headlines_header);
}
function unsubscribeFeed(): bool {
$feed_id = (int) clean($_REQUEST["feed_id"]);
$feed_exists = ORM::for_table('ttrss_feeds')
->where(['id' => $feed_id, 'owner_uid' => $_SESSION['uid']])
->count();
if ($feed_exists) {
Pref_Feeds::remove_feed($feed_id, $_SESSION['uid']);
return $this->_wrap(self::STATUS_OK, ['status' => 'OK']);
} else {
return $this->_wrap(self::STATUS_ERR, ['error' => self::E_OPERATION_FAILED]);
}
}
function subscribeToFeed(): bool {
$feed_url = clean($_REQUEST["feed_url"]);
$category_id = (int) clean($_REQUEST["category_id"]);
$login = clean($_REQUEST["login"] ?? "");
$password = clean($_REQUEST["password"] ?? "");
if ($feed_url) {
$rc = Feeds::_subscribe($feed_url, $category_id, $login, $password);
return $this->_wrap(self::STATUS_OK, array("status" => $rc));
} else {
return $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
}
}
function getFeedTree(): bool {
$include_empty = self::_param_to_bool($_REQUEST['include_empty'] ?? false);
$pf = new Pref_Feeds($_REQUEST);
$_REQUEST['mode'] = 2;
$_REQUEST['force_show_empty'] = $include_empty;
return $this->_wrap(self::STATUS_OK,
array("categories" => $pf->_makefeedtree()));
}
function getFeedIcon(): bool {
$id = (int)$_REQUEST['id'];
$cache = DiskCache::instance('feed-icons');
if ($cache->exists((string)$id)) {
return $cache->send((string)$id) > 0;
} else {
return $this->_wrap(self::STATUS_ERR, array("error" => self::E_NOT_FOUND));
}
}
// only works for labels or uncategorized for the time being
private function _is_cat_empty(int $id): bool {
if ($id == Feeds::CATEGORY_LABELS) {
$label_count = ORM::for_table('ttrss_labels2')
->where('owner_uid', $_SESSION['uid'])
->count();
return $label_count == 0;
} else if ($id == Feeds::CATEGORY_UNCATEGORIZED) {
$uncategorized_count = ORM::for_table('ttrss_feeds')
->where_null('cat_id')
->where('owner_uid', $_SESSION['uid'])
->count();
return $uncategorized_count == 0;
}
return false;
}
/** @return array<string, string> */
private function _get_custom_sort_types(): array {
$ret = [];
PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP, function ($result) use (&$ret) {
foreach ($result as $sort_value => $sort_title) {
$ret[$sort_value] = $sort_title;
}
});
return $ret;
}
}

706
classes/Article.php Normal file
View File

@@ -0,0 +1,706 @@
<?php
class Article extends Handler_Protected {
const ARTICLE_KIND_ALBUM = 1;
const ARTICLE_KIND_VIDEO = 2;
const ARTICLE_KIND_YOUTUBE = 3;
const CATCHUP_MODE_MARK_AS_READ = 0;
const CATCHUP_MODE_MARK_AS_UNREAD = 1;
const CATCHUP_MODE_TOGGLE = 2;
function redirect(): void {
$article = ORM::for_table('ttrss_entries')
->table_alias('e')
->join('ttrss_user_entries', [ 'ref_id', '=', 'e.id'], 'ue')
->where('ue.owner_uid', $_SESSION['uid'])
->find_one((int)$_REQUEST['id']);
if ($article) {
$article_url = UrlHelper::validate($article->link);
if ($article_url) {
header("Location: $article_url");
return;
}
}
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
print "Article not found or has an empty URL.";
}
static function _create_published_article(string $title, string $url, string $content, string $labels_str, int $owner_uid): bool {
$guid = 'SHA1:' . sha1("ttshared:" . $url . $owner_uid); // include owner_uid to prevent global GUID clash
if (!$content) {
$pluginhost = new PluginHost();
$pluginhost->load_all(PluginHost::KIND_ALL, $owner_uid);
//$pluginhost->load_data();
$pluginhost->run_hooks_callback(PluginHost::HOOK_GET_FULL_TEXT,
function ($result) use (&$content) {
if ($result) {
$content = $result;
return true;
}
},
$url);
}
$content_hash = sha1($content);
if ($labels_str != "") {
$labels = explode(",", $labels_str);
} else {
$labels = array();
}
$rc = false;
if (!$title) $title = $url;
if (!$title && !$url) return false;
if (filter_var($url, FILTER_VALIDATE_URL) === false) return false;
$pdo = Db::pdo();
$pdo->beginTransaction();
// only check for our user data here, others might have shared this with different content etc
$sth = $pdo->prepare("SELECT id FROM ttrss_entries, ttrss_user_entries WHERE
guid = ? AND ref_id = id AND owner_uid = ? LIMIT 1");
$sth->execute([$guid, $owner_uid]);
if ($row = $sth->fetch()) {
$ref_id = $row['id'];
$sth = $pdo->prepare("SELECT int_id FROM ttrss_user_entries WHERE
ref_id = ? AND owner_uid = ? LIMIT 1");
$sth->execute([$ref_id, $owner_uid]);
if ($row = $sth->fetch()) {
$int_id = $row['int_id'];
$sth = $pdo->prepare("UPDATE ttrss_entries SET
content = ?, content_hash = ? WHERE id = ?");
$sth->execute([$content, $content_hash, $ref_id]);
$sth = $pdo->prepare("UPDATE ttrss_entries
SET tsvector_combined = to_tsvector( :ts_content)
WHERE id = :id");
$params = [
":ts_content" => mb_substr(\Soundasleep\Html2Text::convert($content), 0, 900000),
":id" => $ref_id];
$sth->execute($params);
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET published = true,
last_published = NOW() WHERE
int_id = ? AND owner_uid = ?");
$sth->execute([$int_id, $owner_uid]);
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_PUBLISH_TOGGLED, [$ref_id]);
} else {
$sth = $pdo->prepare("INSERT INTO ttrss_user_entries
(ref_id, uuid, feed_id, orig_feed_id, owner_uid, published, tag_cache, label_cache,
last_read, note, unread, last_published)
VALUES
(?, '', NULL, NULL, ?, true, '', '', NOW(), '', false, NOW())");
$sth->execute([$ref_id, $owner_uid]);
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_PUBLISH_TOGGLED, [$ref_id]);
}
if (count($labels) != 0) {
foreach ($labels as $label) {
Labels::add_article($ref_id, trim($label), $owner_uid);
}
}
$rc = true;
} else {
$sth = $pdo->prepare("INSERT INTO ttrss_entries
(title, guid, link, updated, content, content_hash, date_entered, date_updated)
VALUES
(?, ?, ?, NOW(), ?, ?, NOW(), NOW()) RETURNING id");
$sth->execute([$title, $guid, $url, $content, $content_hash]);
if ($row = $sth->fetch()) {
$ref_id = $row["id"];
$sth = $pdo->prepare("UPDATE ttrss_entries
SET tsvector_combined = to_tsvector( :ts_content)
WHERE id = :id");
$params = [
":ts_content" => mb_substr(\Soundasleep\Html2Text::convert($content), 0, 900000),
":id" => $ref_id];
$sth->execute($params);
$sth = $pdo->prepare("INSERT INTO ttrss_user_entries
(ref_id, uuid, feed_id, orig_feed_id, owner_uid, published, tag_cache, label_cache,
last_read, note, unread, last_published)
VALUES
(?, '', NULL, NULL, ?, true, '', '', NOW(), '', false, NOW())");
$sth->execute([$ref_id, $owner_uid]);
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_PUBLISH_TOGGLED, [$ref_id]);
if (count($labels) != 0) {
foreach ($labels as $label) {
Labels::add_article($ref_id, trim($label), $owner_uid);
}
}
$rc = true;
}
}
$pdo->commit();
return $rc;
}
function printArticleTags(): void {
$id = (int) clean($_REQUEST['id'] ?? 0);
print json_encode(["id" => $id,
"tags" => self::_get_tags($id)]);
}
function setScore(): void {
$ids = array_map("intval", clean($_REQUEST['ids'] ?? []));
$score = (int)clean($_REQUEST['score']);
$ids_qmarks = arr_qmarks($ids);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
score = ? WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
$sth->execute([$score, ...$ids, $_SESSION['uid']]);
print json_encode(["id" => $ids, "score" => $score]);
}
function setArticleTags(): void {
$id = clean($_REQUEST["id"]);
//$tags_str = clean($_REQUEST["tags_str"]);
//$tags = array_unique(array_map('trim', explode(",", $tags_str)));
$tags = FeedItem_Common::normalize_categories(explode(",", clean($_REQUEST["tags_str"] ?? "")));
$this->pdo->beginTransaction();
$sth = $this->pdo->prepare("SELECT int_id FROM ttrss_user_entries WHERE
ref_id = ? AND owner_uid = ? LIMIT 1");
$sth->execute([$id, $_SESSION['uid']]);
if ($row = $sth->fetch()) {
$tags_to_cache = array();
$int_id = $row['int_id'];
$dsth = $this->pdo->prepare("DELETE FROM ttrss_tags WHERE
post_int_id = ? AND owner_uid = ?");
$dsth->execute([$int_id, $_SESSION['uid']]);
$csth = $this->pdo->prepare("SELECT post_int_id FROM ttrss_tags
WHERE post_int_id = ? AND owner_uid = ? AND tag_name = ?");
$usth = $this->pdo->prepare("INSERT INTO ttrss_tags
(post_int_id, owner_uid, tag_name)
VALUES (?, ?, ?)");
foreach ($tags as $tag) {
$csth->execute([$int_id, $_SESSION['uid'], $tag]);
if (!$csth->fetch()) {
$usth->execute([$int_id, $_SESSION['uid'], $tag]);
}
array_push($tags_to_cache, $tag);
}
/* update tag cache */
$tags_str = join(",", $tags_to_cache);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries
SET tag_cache = ? WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$tags_str, $id, $_SESSION['uid']]);
}
$this->pdo->commit();
// get latest tags from the database, original $tags is sometimes JSON-encoded as a hash ({}) - ???
print json_encode(["id" => (int)$id, "tags" => $this->_get_tags($id)]);
}
function completeTags(): void {
$search = clean($_REQUEST["search"]);
$sth = $this->pdo->prepare("SELECT DISTINCT tag_name FROM ttrss_tags
WHERE owner_uid = ? AND
tag_name LIKE ? ORDER BY tag_name
LIMIT 10");
$sth->execute([$_SESSION['uid'], "$search%"]);
$results = [];
while ($line = $sth->fetch()) {
array_push($results, $line["tag_name"]);
}
print json_encode($results);
}
function assigntolabel(): void {
$this->_label_ops(true);
}
function removefromlabel(): void {
$this->_label_ops(false);
}
private function _label_ops(bool $assign): void {
$reply = array();
$ids = array_map("intval", array_filter(explode(",", clean($_REQUEST["ids"] ?? "")), "strlen"));
$label_id = clean($_REQUEST["lid"]);
$label = Labels::find_caption($label_id, $_SESSION["uid"]);
$reply["labels-for"] = [];
if ($label) {
foreach ($ids as $id) {
if ($assign)
Labels::add_article($id, $label, $_SESSION["uid"]);
else
Labels::remove_article($id, $label, $_SESSION["uid"]);
array_push($reply["labels-for"],
["id" => (int)$id, "labels" => $this->_get_labels($id)]);
}
}
$reply["message"] = "UPDATE_COUNTERS";
print json_encode($reply);
}
/**
* @param int $id article id
* @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 {
$enclosures = self::_get_enclosures($id);
$enclosures_formatted = "";
/*foreach ($enclosures as &$enc) {
array_push($enclosures, [
"type" => $enc["content_type"],
"filename" => basename($enc["content_url"]),
"url" => $enc["content_url"],
"title" => $enc["title"],
"width" => (int) $enc["width"],
"height" => (int) $enc["height"]
]);
}*/
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_FORMAT_ENCLOSURES,
function ($result) use (&$enclosures_formatted, &$enclosures) {
if (is_array($result)) {
$enclosures_formatted = $result[0];
$enclosures = $result[1];
} else {
$enclosures_formatted = $result;
}
},
$enclosures_formatted, $enclosures, $id, $always_display_enclosures, $article_content, $hide_images);
if (!empty($enclosures_formatted)) {
return [
'formatted' => $enclosures_formatted,
'entries' => []
];
}
$rv = [
'formatted' => '',
'entries' => []
];
$rv['can_inline'] = isset($_SESSION["uid"]) &&
empty($_SESSION["bw_limit"]) &&
!Prefs::get(Prefs::STRIP_IMAGES, $_SESSION["uid"], $_SESSION["profile"] ?? null) &&
($always_display_enclosures || !preg_match("/<img/i", $article_content));
$rv['inline_text_only'] = $hide_images && $rv['can_inline'];
foreach ($enclosures as $enc) {
// this is highly approximate
$enc["filename"] = basename($enc["content_url"]);
$rendered_enc = "";
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ENCLOSURE,
function ($result) use (&$rendered_enc) {
$rendered_enc = $result;
},
$enc, $id, $rv);
if ($rendered_enc) {
$rv['formatted'] .= $rendered_enc;
} else {
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ENCLOSURE_ENTRY,
function ($result) use (&$enc) {
$enc = $result;
},
$enc, $id, $rv);
array_push($rv['entries'], $enc);
}
}
return $rv;
}
/**
* @return array<int, string>
*/
static function _get_tags(int $id, int $owner_uid = 0, ?string $tag_cache = null): array {
$a_id = $id;
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT DISTINCT tag_name,
owner_uid as owner FROM ttrss_tags
WHERE post_int_id = (SELECT int_id FROM ttrss_user_entries WHERE
ref_id = ? AND owner_uid = ? LIMIT 1) ORDER BY tag_name");
$tags = array();
/* check cache first */
if (!$tag_cache) {
$csth = $pdo->prepare("SELECT tag_cache FROM ttrss_user_entries
WHERE ref_id = ? AND owner_uid = ?");
$csth->execute([$id, $owner_uid]);
if ($row = $csth->fetch()) {
$tag_cache = $row["tag_cache"];
}
}
if ($tag_cache) {
$tags = explode(",", $tag_cache);
} else {
/* do it the hard way */
$sth->execute([$a_id, $owner_uid]);
while ($tmp_line = $sth->fetch()) {
array_push($tags, $tmp_line["tag_name"]);
}
/* update the cache */
$tags_str = join(",", $tags);
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET tag_cache = ? WHERE ref_id = ?
AND owner_uid = ?");
$sth->execute([$tags_str, $id, $owner_uid]);
}
return $tags;
}
function getmetadatabyid(): void {
$article = ORM::for_table('ttrss_entries')
->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue')
->where('ue.owner_uid', $_SESSION['uid'])
->find_one((int)$_REQUEST['id']);
if ($article) {
echo json_encode(["link" => $article->link, "title" => $article->title]);
} else {
echo json_encode([]);
}
}
/**
* @return array<int, array<string, mixed>>
*/
static function _get_enclosures(int $id): array {
$encs = ORM::for_table('ttrss_enclosures')
->where('post_id', $id)
->find_many();
$rv = [];
$cache = DiskCache::instance("images");
foreach ($encs as $enc) {
$cache_key = sha1($enc->content_url);
if ($cache->exists($cache_key)) {
$enc->content_url = $cache->get_url($cache_key);
}
array_push($rv, $enc->as_array());
}
return $rv;
}
static function _purge_orphans(): void {
$pdo = Db::pdo();
$res = $pdo->query("DELETE FROM ttrss_entries WHERE
NOT EXISTS (SELECT ref_id FROM ttrss_user_entries WHERE ref_id = id)");
if (Debug::enabled()) {
$rows = $res->rowCount();
Debug::log("Purged $rows orphaned posts.");
}
}
/**
* @param array<int, int> $ids
* @param int $cmode Article::CATCHUP_MODE_*
*/
static function _catchup_by_id($ids, int $cmode, ?int $owner_uid = null): void {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
$ids_qmarks = arr_qmarks($ids);
if ($cmode == Article::CATCHUP_MODE_MARK_AS_UNREAD) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
unread = true
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else if ($cmode == Article::CATCHUP_MODE_TOGGLE) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
unread = NOT unread,last_read = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else {
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
unread = false,last_read = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
}
$sth->execute([...$ids, $owner_uid]);
}
/**
* @return array<int, array<int, int|string>>
*/
static function _get_labels(int $id, ?int $owner_uid = null): array {
$rv = array();
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT label_cache FROM
ttrss_user_entries WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$id, $owner_uid]);
if ($row = $sth->fetch()) {
$label_cache = $row["label_cache"];
if ($label_cache) {
$tmp = json_decode($label_cache, true);
if (empty($tmp) || ($tmp["no-labels"] ?? 0) == 1)
return $rv;
else
return $tmp;
}
}
$sth = $pdo->prepare("SELECT DISTINCT label_id,caption,fg_color,bg_color
FROM ttrss_labels2, ttrss_user_labels2
WHERE id = label_id
AND article_id = ?
AND owner_uid = ?
ORDER BY caption");
$sth->execute([$id, $owner_uid]);
while ($line = $sth->fetch()) {
$rk = array(Labels::label_to_feed_id($line["label_id"]),
$line["caption"], $line["fg_color"],
$line["bg_color"]);
array_push($rv, $rk);
}
if (count($rv) > 0)
// PHPStan has issues with the shape of $rv for some reason (array vs non-empty-array).
// @phpstan-ignore-next-line
Labels::update_cache($owner_uid, $id, $rv);
else
Labels::update_cache($owner_uid, $id, array("no-labels" => 1));
return $rv;
}
/**
* @param array<int, array<string, mixed>> $enclosures
* @param array<string, mixed> $headline
*
* @return array<int, Article::ARTICLE_KIND_*|string>
*/
static function _get_image(array $enclosures, string $content, string $site_url, array $headline): array {
$article_image = "";
$article_stream = "";
$article_kind = 0;
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_IMAGE,
function ($result, $plugin) use (&$article_image, &$article_stream, &$content) {
list ($article_image, $article_stream, $content) = $result;
// run until first hard match
return !empty($article_image);
},
$enclosures, $content, $site_url, $headline);
if (!$article_image && !$article_stream) {
$tmpdoc = new DOMDocument();
if (@$tmpdoc->loadHTML('<?xml encoding="UTF-8">' . mb_substr($content, 0, 131070))) {
$tmpxpath = new DOMXPath($tmpdoc);
$elems = $tmpxpath->query('(//img[@src]|//video[@poster]|//iframe[contains(@src , "youtube.com/embed/")])');
/** @var DOMElement $e */
foreach ($elems as $e) {
if ($e->nodeName == "iframe") {
$matches = [];
if (preg_match("/\/embed\/([\w-]+)/", $e->getAttribute("src"), $matches)) {
$article_image = "https://img.youtube.com/vi/" . $matches[1] . "/hqdefault.jpg";
$article_stream = "https://youtu.be/" . $matches[1];
$article_kind = Article::ARTICLE_KIND_YOUTUBE;
break;
}
} else if ($e->nodeName == "video") {
$article_image = $e->getAttribute("poster");
/** @var DOMElement|null $src */
$src = $tmpxpath->query("//source[@src]", $e)->item(0);
if ($src) {
$article_stream = $src->getAttribute("src");
$article_kind = Article::ARTICLE_KIND_VIDEO;
}
break;
} else if ($e->nodeName == 'img') {
if (mb_strpos($e->getAttribute("src"), "data:") !== 0) {
$article_image = $e->getAttribute("src");
}
break;
}
}
}
if (!$article_image)
foreach ($enclosures as $enc) {
if (str_contains($enc["content_type"], "image/")) {
$article_image = $enc["content_url"];
break;
}
}
if ($article_image) {
$article_image = UrlHelper::rewrite_relative($site_url, $article_image);
if (!$article_kind && (count($enclosures) > 1 || (isset($elems) && $elems->length > 1)))
$article_kind = Article::ARTICLE_KIND_ALBUM;
}
if ($article_stream)
$article_stream = UrlHelper::rewrite_relative($site_url, $article_stream);
}
$cache = DiskCache::instance("images");
if ($article_image && $cache->exists(sha1($article_image)))
$article_image = $cache->get_url(sha1($article_image));
if ($article_stream && $cache->exists(sha1($article_stream)))
$article_stream = $cache->get_url(sha1($article_stream));
return [$article_image, $article_stream, $article_kind];
}
/**
* only cached, returns label ids (not label feed ids)
*
* @param array<int, int> $article_ids
* @return array<int, int>
*/
static function _labels_of(array $article_ids) {
if (count($article_ids) == 0)
return [];
$entries = ORM::for_table('ttrss_entries')
->table_alias('e')
->select('ue.label_cache')
->join('ttrss_user_entries', ['ue.ref_id', '=', 'e.id'], 'ue')
->where_in('e.id', $article_ids)
->where('ue.owner_uid', $_SESSION['uid'])
->find_many();
$rv = [];
foreach ($entries as $entry) {
$labels = json_decode($entry->label_cache);
if (isset($labels) && is_array($labels)) {
foreach ($labels as $label) {
if (empty($label["no-labels"]))
array_push($rv, Labels::feed_to_label_id($label[0]));
}
}
}
return array_unique($rv);
}
/**
* @param array<int, int> $article_ids
* @return array<int, int>
*/
static function _feeds_of(array $article_ids) {
if (count($article_ids) == 0)
return [];
$entries = ORM::for_table('ttrss_entries')
->table_alias('e')
->select('ue.feed_id')
->join('ttrss_user_entries', ['ue.ref_id', '=', 'e.id'], 'ue')
->where_in('e.id', $article_ids)
->where('ue.owner_uid', $_SESSION['uid'])
->find_many();
$rv = [];
foreach ($entries as $entry) {
array_push($rv, $entry->feed_id);
}
return array_unique($rv);
}
}

56
classes/Auth_Base.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
abstract class Auth_Base extends Plugin implements IAuthModule {
protected $pdo;
const AUTH_SERVICE_API = '_api';
function __construct() {
$this->pdo = Db::pdo();
}
function hook_auth_user($login, $password, $service = '') {
return $this->authenticate($login, $password, $service);
}
/** Auto-creates specified user if allowed by system configuration.
* Can be used instead of find_user_by_login() by external auth modules
* @param string $login
* @param null|string|false $password
* @throws Exception
* @throws PDOException
*/
function auto_create_user(string $login, null|false|string $password = false): ?int {
if ($login && Config::get(Config::AUTH_AUTO_CREATE)) {
$user_id = UserHelper::find_user_by_login($login);
if (!$user_id) {
if (!$password) $password = make_password();
$user = ORM::for_table('ttrss_users')->create();
$user->salt = UserHelper::get_salt();
$user->login = mb_strtolower($login);
$user->pwd_hash = UserHelper::hash_password($password, $user->salt);
$user->access_level = 0;
$user->created = Db::NOW();
$user->save();
return UserHelper::find_user_by_login($login);
} else {
return $user_id;
}
}
return UserHelper::find_user_by_login($login);
}
/** replaced with UserHelper::find_user_by_login()
* @deprecated
*/
function find_user_by_login(string $login): ?int {
return UserHelper::find_user_by_login($login);
}
}

36
classes/Cache_Adapter.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
interface Cache_Adapter {
public function set_dir(string $dir) : void;
public function get_dir(): string;
public function make_dir(): bool;
public function is_writable(?string $filename = null): bool;
public function exists(string $filename): bool;
/**
* @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);
/**
* @return int|false -1 if the file doesn't exist, false if an error occurred, timestamp otherwise
*/
public function get_mtime(string $filename);
/**
* @param mixed $data
*
* @return int|false Bytes written or false if an error occurred.
*/
public function put(string $filename, $data);
public function get(string $filename): ?string;
public function get_full_path(string $filename): string;
public function remove(string $filename) : bool;
/**
* @return false|null|string false if detection failed, null if the file doesn't exist, string mime content type otherwise
*/
public function get_mime_type(string $filename);
/**
* @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
*/
public function send(string $filename);
/** Catchall function to expire all subfolders/prefixes in the cache, invoked on the backend */
public function expire_all(): void;
}

155
classes/Cache_Local.php Normal file
View File

@@ -0,0 +1,155 @@
<?php
class Cache_Local implements Cache_Adapter {
private string $dir;
public function remove(string $filename): bool {
return unlink($this->get_full_path($filename));
}
public function get_mtime(string $filename) {
return filemtime($this->get_full_path($filename));
}
public function set_dir(string $dir) : void {
$cache_dir = Config::get(Config::CACHE_DIR);
// use absolute path local to current dir if CACHE_DIR is relative
// TODO: maybe add a special method to Config() for this?
if ($cache_dir[0] != '/')
$cache_dir = dirname(__DIR__) . "/$cache_dir";
$this->dir = $cache_dir . "/" . basename(clean($dir));
$this->make_dir();
}
public function get_dir(): string {
return $this->dir;
}
public function make_dir(): bool {
if (!is_dir($this->dir)) {
return mkdir($this->dir);
}
return false;
}
public function is_writable(?string $filename = null): bool {
if ($filename) {
if (file_exists($this->get_full_path($filename)))
return is_writable($this->get_full_path($filename));
else
return is_writable($this->dir);
} else {
return is_writable($this->dir);
}
}
public function exists(string $filename): bool {
return file_exists($this->get_full_path($filename));
}
/**
* @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) {
if ($this->exists($filename))
return filesize($this->get_full_path($filename));
else
return -1;
}
public function get_full_path(string $filename): string {
return $this->dir . "/" . basename(clean($filename));
}
public function get(string $filename): ?string {
if ($this->exists($filename))
return file_get_contents($this->get_full_path($filename));
else
return null;
}
/**
* @param mixed $data
*
* @return int|false Bytes written or false if an error occurred.
*/
public function put(string $filename, $data) {
return file_put_contents($this->get_full_path($filename), $data);
}
/**
* @return false|null|string false if detection failed, null if the file doesn't exist, string mime content type otherwise
*/
public function get_mime_type(string $filename) {
if ($this->exists($filename))
return mime_content_type($this->get_full_path($filename));
else
return null;
}
/**
* @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
*/
public function send(string $filename) {
return $this->send_local_file($this->get_full_path($filename));
}
public function expire_all(): void {
$dirs = array_filter(glob(Config::get(Config::CACHE_DIR) . "/*"), "is_dir");
foreach ($dirs as $cache_dir) {
$num_deleted = 0;
if (is_writable($cache_dir) && !file_exists("$cache_dir/.no-auto-expiry")) {
$files = glob("$cache_dir/*");
if ($files) {
foreach ($files as $file) {
if (time() - filemtime($file) > 86400 * Config::get(Config::CACHE_MAX_DAYS)) {
unlink($file);
++$num_deleted;
}
}
}
Debug::log("Expired $cache_dir: removed $num_deleted files.");
}
}
}
/**
* this is essentially a wrapper for readfile() which allows plugins to hook
* output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
*
* hook function should return true if request was handled (or at least attempted to)
*
* note that this can be called without user context so the plugin to handle this
* should be loaded systemwide in config.php
*
* @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
*/
private function send_local_file(string $filename) {
if (file_exists($filename)) {
if (is_writable($filename) && !$this->exists('.no-auto-expiry')) {
touch($filename);
}
$tmppluginhost = new PluginHost();
$tmppluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_SYSTEM);
//$tmppluginhost->load_data();
if ($tmppluginhost->run_hooks_until(PluginHost::HOOK_SEND_LOCAL_FILE, true, $filename))
return true;
return readfile($filename);
} else {
return false;
}
}
}

682
classes/Config.php Normal file
View File

@@ -0,0 +1,682 @@
<?php
class Config {
private const _ENVVAR_PREFIX = "TTRSS_";
const T_BOOL = 1;
const T_STRING = 2;
const T_INT = 3;
const SCHEMA_VERSION = 151;
/** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX:
*
* DB_TYPE becomes:
*
* .env (docker environment):
*
* TTRSS_DB_TYPE=pgsql
*
* or config.php:
*
* putenv('TTRSS_DB_HOST=my-patroni.example.com');
*
* note lack of quotes and spaces before and after "=".
*
*/
/** 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";
/** database server hostname */
const DB_HOST = "DB_HOST";
/** database user */
const DB_USER = "DB_USER";
/** database name */
const DB_NAME = "DB_NAME";
/** database password */
const DB_PASS = "DB_PASS";
/** database server port */
const DB_PORT = "DB_PORT";
/** PostgreSQL SSL mode (prefer, require, disabled) */
const DB_SSLMODE = "DB_SSLMODE";
/** this is a fallback falue for the CLI SAPI, it should be set to a fully-qualified tt-rss URL */
const SELF_URL_PATH = "SELF_URL_PATH";
/** operate in single user mode, disables all functionality related to
* multiple users and authentication. enabling this assumes you have
* your tt-rss directory protected by other means (e.g. http auth). */
const SINGLE_USER_MODE = "SINGLE_USER_MODE";
/** use this PHP CLI executable to start various tasks */
const PHP_EXECUTABLE = "PHP_EXECUTABLE";
/** base directory for lockfiles (must be writable) */
const LOCK_DIRECTORY = "LOCK_DIRECTORY";
/** base directory for local cache (must be writable) */
const CACHE_DIR = "CACHE_DIR";
/** auto create users authenticated via external modules */
const AUTH_AUTO_CREATE = "AUTH_AUTO_CREATE";
/** auto log in users authenticated via external modules i.e. auth_remote */
const AUTH_AUTO_LOGIN = "AUTH_AUTO_LOGIN";
/** unconditinally purge all articles older than this amount, in days
* overrides user-controlled purge interval */
const FORCE_ARTICLE_PURGE = "FORCE_ARTICLE_PURGE";
/** default lifetime of a session (e.g. login) cookie. In seconds,
* 0 means cookie will be deleted when browser closes. */
const SESSION_COOKIE_LIFETIME = "SESSION_COOKIE_LIFETIME";
/** send email using this name */
const SMTP_FROM_NAME = "SMTP_FROM_NAME";
/** send email using this address */
const SMTP_FROM_ADDRESS = "SMTP_FROM_ADDRESS";
/** default subject for email digest */
const DIGEST_SUBJECT = "DIGEST_SUBJECT";
/** enable built-in update checker, both for core code and plugins (using git) */
const CHECK_FOR_UPDATES = "CHECK_FOR_UPDATES";
/** system plugins enabled for all users, comma separated list, no quotes
* keep at least one auth module in there (i.e. auth_internal) */
const PLUGINS = "PLUGINS";
/** available options: sql (default, event log), syslog, stdout (for debugging) */
const LOG_DESTINATION = "LOG_DESTINATION";
/** link this stylesheet on all pages (if it exists), should be placed in themes.local */
const LOCAL_OVERRIDE_STYLESHEET = "LOCAL_OVERRIDE_STYLESHEET";
/** same but this javascript file (you can use that for polyfills), should be placed in themes.local */
const LOCAL_OVERRIDE_JS = "LOCAL_OVERRIDE_JS";
/** in seconds, terminate update tasks that ran longer than this interval */
const DAEMON_MAX_CHILD_RUNTIME = "DAEMON_MAX_CHILD_RUNTIME";
/** max concurrent update jobs forking update daemon starts */
const DAEMON_MAX_JOBS = "DAEMON_MAX_JOBS";
/** log level for update daemon */
const DAEMON_LOG_LEVEL = "DAEMON_LOG_LEVEL";
/** How long to wait for response when requesting feed from a site (seconds) */
const FEED_FETCH_TIMEOUT = "FEED_FETCH_TIMEOUT";
/** How long to wait for response when requesting uncached feed from a site (seconds) */
const FEED_FETCH_NO_CACHE_TIMEOUT = "FEED_FETCH_NO_CACHE_TIMEOUT";
/** Default timeout when fetching files from remote sites */
const FILE_FETCH_TIMEOUT = "FILE_FETCH_TIMEOUT";
/** How long to wait for initial response from website when fetching remote files */
const FILE_FETCH_CONNECT_TIMEOUT = "FILE_FETCH_CONNECT_TIMEOUT";
/** stop updating feeds if user haven't logged in for X days */
const DAEMON_UPDATE_LOGIN_LIMIT = "DAEMON_UPDATE_LOGIN_LIMIT";
/** how many feeds to update in one batch */
const DAEMON_FEED_LIMIT = "DAEMON_FEED_LIMIT";
/** default sleep interval between feed updates (sec) */
const DAEMON_SLEEP_INTERVAL = "DAEMON_SLEEP_INTERVAL";
/** do not cache files larger than that (bytes) */
const MAX_CACHE_FILE_SIZE = "MAX_CACHE_FILE_SIZE";
/** do not download files larger than that (bytes) */
const MAX_DOWNLOAD_FILE_SIZE = "MAX_DOWNLOAD_FILE_SIZE";
/** max file size for downloaded favicons (bytes) */
const MAX_FAVICON_FILE_SIZE = "MAX_FAVICON_FILE_SIZE";
/** max age in days for various automatically cached (temporary) files */
const CACHE_MAX_DAYS = "CACHE_MAX_DAYS";
/** max interval between forced unconditional updates for servers
* not complying with http if-modified-since (seconds) */
const MAX_CONDITIONAL_INTERVAL = "MAX_CONDITIONAL_INTERVAL";
/** automatically disable updates for feeds which failed to
* update for this amount of days; 0 disables */
const DAEMON_UNSUCCESSFUL_DAYS_LIMIT = "DAEMON_UNSUCCESSFUL_DAYS_LIMIT";
/** log all sent emails in the event log */
const LOG_SENT_MAIL = "LOG_SENT_MAIL";
/** use HTTP proxy for requests */
const HTTP_PROXY = "HTTP_PROXY";
/** prevent users from changing passwords */
const FORBID_PASSWORD_CHANGES = "FORBID_PASSWORD_CHANGES";
/** default session cookie name */
const SESSION_NAME = "SESSION_NAME";
/** enable plugin update checker (using git) */
const CHECK_FOR_PLUGIN_UPDATES = "CHECK_FOR_PLUGIN_UPDATES";
/** allow installing first party plugins using plugin installer in prefs */
const ENABLE_PLUGIN_INSTALLER = "ENABLE_PLUGIN_INSTALLER";
/** minimum amount of seconds required between authentication attempts */
const AUTH_MIN_INTERVAL = "AUTH_MIN_INTERVAL";
/** http user agent (changing this is not recommended) */
const HTTP_USER_AGENT = "HTTP_USER_AGENT";
/** 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";
/** disables login form controls except HOOK_LOGINFORM_ADDITIONAL_BUTTONS (for SSO providers), also prevents logging in through auth_internal */
const DISABLE_LOGIN_FORM = "DISABLE_LOGIN_FORM";
/** optional key to transparently encrypt sensitive data (currently limited to sessions and feed passwords),
* 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 */
private const _DEFAULTS = [
Config::DB_TYPE => [ "pgsql", Config::T_STRING ],
Config::DB_HOST => [ "db", Config::T_STRING ],
Config::DB_USER => [ "", Config::T_STRING ],
Config::DB_NAME => [ "", Config::T_STRING ],
Config::DB_PASS => [ "", Config::T_STRING ],
Config::DB_PORT => [ "5432", Config::T_STRING ],
Config::DB_SSLMODE => [ "prefer", Config::T_STRING ],
Config::SELF_URL_PATH => [ "https://example.com/tt-rss", Config::T_STRING ],
Config::SINGLE_USER_MODE => [ "", Config::T_BOOL ],
Config::PHP_EXECUTABLE => [ "/usr/bin/php", Config::T_STRING ],
Config::LOCK_DIRECTORY => [ "lock", Config::T_STRING ],
Config::CACHE_DIR => [ "cache", Config::T_STRING ],
Config::AUTH_AUTO_CREATE => [ "true", Config::T_BOOL ],
Config::AUTH_AUTO_LOGIN => [ "true", Config::T_BOOL ],
Config::FORCE_ARTICLE_PURGE => [ 0, Config::T_INT ],
Config::SESSION_COOKIE_LIFETIME => [ 86400, Config::T_INT ],
Config::SMTP_FROM_NAME => [ "Tiny Tiny RSS", Config::T_STRING ],
Config::SMTP_FROM_ADDRESS => [ "noreply@localhost", Config::T_STRING ],
Config::DIGEST_SUBJECT => [ "[tt-rss] New headlines for last 24 hours",
Config::T_STRING ],
Config::CHECK_FOR_UPDATES => [ "true", Config::T_BOOL ],
Config::PLUGINS => [ "auth_internal", Config::T_STRING ],
Config::LOG_DESTINATION => [ Logger::LOG_DEST_SQL, Config::T_STRING ],
Config::LOCAL_OVERRIDE_STYLESHEET => [ "local-overrides.css",
Config::T_STRING ],
Config::LOCAL_OVERRIDE_JS => [ "local-overrides.js",
Config::T_STRING ],
Config::DAEMON_MAX_CHILD_RUNTIME => [ 1800, Config::T_INT ],
Config::DAEMON_MAX_JOBS => [ 2, Config::T_INT ],
Config::DAEMON_LOG_LEVEL => [ Debug::LOG_NORMAL, Config::T_INT ],
Config::FEED_FETCH_TIMEOUT => [ 45, Config::T_INT ],
Config::FEED_FETCH_NO_CACHE_TIMEOUT => [ 15, Config::T_INT ],
Config::FILE_FETCH_TIMEOUT => [ 45, Config::T_INT ],
Config::FILE_FETCH_CONNECT_TIMEOUT => [ 15, Config::T_INT ],
Config::DAEMON_UPDATE_LOGIN_LIMIT => [ 30, Config::T_INT ],
Config::DAEMON_FEED_LIMIT => [ 50, Config::T_INT ],
Config::DAEMON_SLEEP_INTERVAL => [ 120, Config::T_INT ],
Config::MAX_CACHE_FILE_SIZE => [ 64*1024*1024, Config::T_INT ],
Config::MAX_DOWNLOAD_FILE_SIZE => [ 16*1024*1024, Config::T_INT ],
Config::MAX_FAVICON_FILE_SIZE => [ 1*1024*1024, Config::T_INT ],
Config::CACHE_MAX_DAYS => [ 7, Config::T_INT ],
Config::MAX_CONDITIONAL_INTERVAL => [ 3600*12, Config::T_INT ],
Config::DAEMON_UNSUCCESSFUL_DAYS_LIMIT => [ 30, Config::T_INT ],
Config::LOG_SENT_MAIL => [ "", Config::T_BOOL ],
Config::HTTP_PROXY => [ "", Config::T_STRING ],
Config::FORBID_PASSWORD_CHANGES => [ "", Config::T_BOOL ],
Config::SESSION_NAME => [ "ttrss_sid", Config::T_STRING ],
Config::CHECK_FOR_PLUGIN_UPDATES => [ "true", Config::T_BOOL ],
Config::ENABLE_PLUGIN_INSTALLER => [ "true", Config::T_BOOL ],
Config::AUTH_MIN_INTERVAL => [ 5, Config::T_INT ],
Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://github.com/tt-rss/tt-rss)',
Config::T_STRING ],
Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ],
Config::DISABLE_LOGIN_FORM => [ "", Config::T_BOOL ],
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],
];
private static ?Config $instance = null;
/** @var array<string, array<bool|int|string>> */
private array $params = [];
/** @var array<string, mixed> */
private array $version = [];
private Db_Migrations $migrations;
public static function get_instance() : Config {
if (self::$instance == null)
self::$instance = new self();
return self::$instance;
}
private function __clone() {
//
}
function __construct() {
$ref = new ReflectionClass(get_class($this));
foreach ($ref->getConstants() as $const => $cvalue) {
if (isset(self::_DEFAULTS[$const])) {
$override = getenv(self::_ENVVAR_PREFIX . $const);
list ($defval, $deftype) = self::_DEFAULTS[$const];
$this->params[$cvalue] = [ self::cast_to($override !== false ? $override : $defval, $deftype), $deftype ];
}
}
}
/** determine tt-rss version (using git)
*
* package maintainers who don't use git: if version_static.txt exists in tt-rss root
* directory, its contents are displayed instead of git commit-based version, this could be generated
* based on source git tree commit used when creating the package
* @return array<string, mixed>|string
*/
static function get_version(bool $as_string = true): array|string {
return self::get_instance()->_get_version($as_string);
}
// returns version showing (if possible) full timestamp of commit id
static function get_version_html() : string {
$version = self::get_version(false);
return sprintf("<span title=\"%s\n%s\n%s\">%s</span>",
date("Y-m-d H:i:s", ($version['timestamp'] ?? 0)),
$version['commit'] ?? '',
$version['branch'] ?? '',
$version['version']);
}
/**
* @return array<string, mixed>|string
*/
private function _get_version(bool $as_string = true): array|string {
$root_dir = self::get_self_dir();
if (empty($this->version)) {
$this->version["status"] = -1;
if (getenv("CI_COMMIT_SHORT_SHA") && getenv("CI_COMMIT_TIMESTAMP")) {
$this->version["branch"] = getenv("CI_COMMIT_BRANCH");
$this->version["timestamp"] = strtotime(getenv("CI_COMMIT_TIMESTAMP"));
$this->version["version"] = sprintf("%s-%s", date("y.m", $this->version["timestamp"]), getenv("CI_COMMIT_SHORT_SHA"));
$this->version["commit"] = getenv("CI_COMMIT_SHORT_SHA");
$this->version["status"] = 0;
} else if (PHP_OS === "Darwin") {
$this->version["version"] = "UNKNOWN (Unsupported, Darwin)";
} else if (file_exists("$root_dir/version_static.txt")) {
$this->version["version"] = trim(file_get_contents("$root_dir/version_static.txt")) . " (Unsupported)";
} else if (ini_get("open_basedir")) {
$this->version["version"] = "UNKNOWN (Unsupported, open_basedir)";
} else if (is_dir("$root_dir/.git")) {
$this->version = self::get_version_from_git($root_dir);
if ($this->version["status"] != 0) {
user_error("Unable to determine version: " . $this->version["version"], E_USER_WARNING);
$this->version["version"] = "UNKNOWN (Unsupported, Git error)";
} else if (!getenv("SCRIPT_ROOT") || !file_exists("/.dockerenv")) {
$this->version["version"] .= " (Unsupported)";
}
} else {
$this->version["version"] = "UNKNOWN (Unsupported)";
}
}
return $as_string ? $this->version["version"] : $this->version;
}
/**
* @return array<string, int|string>
*/
static function get_version_from_git(string $dir): array {
$descriptorspec = [
1 => ["pipe", "w"], // STDOUT
2 => ["pipe", "w"], // STDERR
];
$rv = [
"status" => -1,
"version" => "",
"branch" => "",
"commit" => "",
"timestamp" => 0,
];
$proc = proc_open("git --no-pager log --pretty=\"version-%ct-%h\" -n1 HEAD",
$descriptorspec, $pipes, $dir);
if (is_resource($proc)) {
$stdout = trim(stream_get_contents($pipes[1]));
$stderr = trim(stream_get_contents($pipes[2]));
$status = proc_close($proc);
$rv["status"] = $status;
list($check, $timestamp, $commit) = explode("-", $stdout);
if ($check == "version") {
$rv["version"] = sprintf("%s-%s", date("y.m", (int)$timestamp), $commit);
$rv["commit"] = $commit;
$rv["timestamp"] = $timestamp;
// proc_close() may return -1 even if command completed successfully
// so if it looks like we got valid data, we ignore it
if ($rv["status"] == -1)
$rv["status"] = 0;
} else {
$rv["version"] = T_sprintf("Git error [RC=%d]: %s", $status, $stderr);
}
}
return $rv;
}
static function get_migrations() : Db_Migrations {
return self::get_instance()->_get_migrations();
}
private function _get_migrations() : Db_Migrations {
if (empty($this->migrations)) {
$this->migrations = new Db_Migrations();
$this->migrations->initialize(self::get_self_dir() . "/sql", "ttrss_version", true, self::SCHEMA_VERSION);
}
return $this->migrations;
}
static function is_migration_needed() : bool {
return self::get_migrations()->is_migration_needed();
}
static function get_schema_version() : int {
return self::get_migrations()->get_version();
}
static function cast_to(string $value, int $type_hint): bool|int|string {
return match ($type_hint) {
self::T_BOOL => sql_bool_to_bool($value),
self::T_INT => (int) $value,
default => $value,
};
}
private function _get(string $param): bool|int|string {
list ($value, $type_hint) = $this->params[$param];
return $this->cast_to($value, $type_hint);
}
private function _add(string $param, string $default, int $type_hint): void {
$override = getenv(self::_ENVVAR_PREFIX . $param);
$this->params[$param] = [ self::cast_to($override !== false ? $override : $default, $type_hint), $type_hint ];
}
static function add(string $param, string $default, int $type_hint = Config::T_STRING): void {
$instance = self::get_instance();
$instance->_add($param, $default, $type_hint);
}
static function get(string $param): bool|int|string {
$instance = self::get_instance();
return $instance->_get($param);
}
static function is_server_https() : bool {
return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
(!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https');
}
/** returns fully-qualified external URL to tt-rss (no trailing slash)
* SELF_URL_PATH configuration variable is used as a fallback for the CLI SAPI
* */
static function get_self_url(bool $always_detect = false) : string {
if (!$always_detect && php_sapi_name() == "cli") {
return self::get(Config::SELF_URL_PATH);
} else {
$proto = self::is_server_https() ? 'https' : 'http';
$self_url_path = $proto . '://' . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$self_url_path = preg_replace("/(\/api\/{1,})?(\w+\.php)?(\?.*$)?$/", "", $self_url_path);
$self_url_path = preg_replace("/(\/plugins(.local)?)\/.{1,}$/", "", $self_url_path);
return rtrim($self_url_path, "/");
}
}
/* sanity check stuff */
static function sanity_check(): void {
/*
we don't actually need the DB object right now but some checks below might use ORM which won't be initialized
because it is set up in the Db constructor, which is why it's a good idea to invoke it as early as possible
it is a bit of a hack, maybe ORM should be initialized somewhere else (functions.php?)
*/
$pdo = Db::pdo();
$errors = [];
if (!str_contains(self::get(Config::PLUGINS), "auth_")) {
array_push($errors, "Please enable at least one authentication module via PLUGINS");
}
/* we assume our dependencies are sane under docker, so some sanity checks are skipped.
this also allows tt-rss process to run under root if requested (I'm using this for development
under podman because of uidmapping issues with rootless containers, don't use in production -fox) */
if (!getenv("container")) {
if (function_exists('posix_getuid') && posix_getuid() == 0) {
array_push($errors, "Please don't run this script as root.");
}
if (version_compare(PHP_VERSION, '8.2.0', '<')) {
array_push($errors, "PHP version 8.2.0 or newer required. You're using " . PHP_VERSION . ".");
}
if (!class_exists("UConverter")) {
array_push($errors, "PHP UConverter class is missing, it's provided by the Internationalization (intl) module.");
}
if (!function_exists("curl_init") && !ini_get("allow_url_fopen")) {
array_push($errors, "PHP configuration option allow_url_fopen is disabled, and CURL functions are not present. Either enable allow_url_fopen or install PHP extension for CURL.");
}
if (!function_exists("json_encode")) {
array_push($errors, "PHP support for JSON is required, but was not found.");
}
if (!function_exists("flock")) {
array_push($errors, "PHP support for flock() function is required.");
}
if (!class_exists("PDO")) {
array_push($errors, "PHP support for PDO is required but was not found.");
}
if (!function_exists("mb_strlen")) {
array_push($errors, "PHP support for mbstring functions is required but was not found.");
}
if (!function_exists("hash")) {
array_push($errors, "PHP support for hash() function is required but was not found.");
}
if (ini_get("safe_mode")) {
array_push($errors, "PHP safe mode setting is obsolete and not supported by tt-rss.");
}
if (!function_exists("mime_content_type")) {
array_push($errors, "PHP function mime_content_type() is missing, try enabling fileinfo module.");
}
if (!class_exists("DOMDocument")) {
array_push($errors, "PHP support for DOMDocument is required, but was not found.");
}
}
if (!is_writable(self::get(Config::CACHE_DIR) . "/images")) {
array_push($errors, "Image cache is not writable (chmod -R 777 ".self::get(Config::CACHE_DIR)."/images)");
}
if (!is_writable(self::get(Config::CACHE_DIR) . "/upload")) {
array_push($errors, "Upload cache is not writable (chmod -R 777 ".self::get(Config::CACHE_DIR)."/upload)");
}
if (!is_writable(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::LOCK_DIRECTORY))) {
array_push($errors, "LOCK_DIRECTORY is not writable (chmod -R 777 ".self::get(Config::LOCK_DIRECTORY).").\n");
}
// ttrss_users won't be there on initial startup (before migrations are done)
if (!Config::is_migration_needed() && self::get(Config::SINGLE_USER_MODE)) {
if (UserHelper::get_login_by_id(1) != "admin") {
array_push($errors, "SINGLE_USER_MODE is enabled but default admin account (ID: 1) is not found.");
}
}
// skip check for CLI scripts so that we could install database schema if it is missing.
if (php_sapi_name() != "cli") {
if (self::get_schema_version() < 0) {
array_push($errors, "Base database schema is missing. Either load it manually or perform a migration (<code>update.php --update-schema</code>)");
}
}
if (count($errors) > 0 && php_sapi_name() != "cli") {
http_response_code(503); ?>
<!DOCTYPE html>
<html>
<head>
<title>Startup failed</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="themes/light.css">
</head>
<body class="sanity_failed flat ttrss_utility">
<div class="content">
<h1>Startup failed</h1>
<p>Please fix errors indicated by the following messages:</p>
<?php foreach ($errors as $error) { echo self::format_error($error); } ?>
<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://github.com/tt-rss/tt-rss/discussions">discussions</a> for more information.
Please search before creating a new topic for your question.</p>
</div>
</body>
</html>
<?php
die;
} else if (count($errors) > 0) {
echo "Please fix errors indicated by the following messages:\n\n";
foreach ($errors as $error) {
echo " * " . strip_tags($error)."\n";
}
echo "\nYou might want to check the tt-rss wiki or forums for more information.\n";
echo "Please search the forums before creating a new topic for your question.\n";
exit(1);
}
}
private static function format_error(string $msg): string {
return "<div class=\"alert alert-danger\">$msg</div>";
}
static function get_override_links(): string {
$rv = "";
$local_css = get_theme_path(self::get(self::LOCAL_OVERRIDE_STYLESHEET));
if ($local_css) $rv .= stylesheet_tag($local_css);
$local_js = get_theme_path(self::get(self::LOCAL_OVERRIDE_JS));
if ($local_js) $rv .= javascript_tag($local_js);
return $rv;
}
static function get_user_agent(): string {
return sprintf(self::get(self::HTTP_USER_AGENT), self::get_version());
}
static function get_self_dir() : string {
return dirname(__DIR__); # we're in classes/Config.php
}
}

327
classes/Counters.php Normal file
View File

@@ -0,0 +1,327 @@
<?php
class Counters {
/**
* @return array<int, array<string, int|string>>
*/
static function get_all(): array {
return [
...self::get_global(),
...self::get_virt(),
...self::get_labels(),
...self::get_feeds(),
...self::get_cats(),
];
}
/**
* @param array<int>|null $feed_ids
* @param array<int>|null $label_ids
* @return array<int, array<string, int|string>>
*/
static function get_conditional(?array $feed_ids = null, ?array $label_ids = null): array {
return [
...self::get_global(),
...self::get_virt(),
...self::get_labels($label_ids),
...self::get_feeds($feed_ids),
...self::get_cats(is_array($feed_ids) ? Feeds::_cats_of($feed_ids, $_SESSION["uid"], true) : null)
];
}
/**
* @return array<int, int>
*/
static private function get_cat_children(int $cat_id, int $owner_uid): array {
$unread = 0;
$marked = 0;
$published = 0;
$cats = ORM::for_table('ttrss_feed_categories')
->where('owner_uid', $owner_uid)
->where('parent_cat', $cat_id)
->find_many();
foreach ($cats as $cat) {
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);
$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, $published];
}
/**
* @param array<int>|null $cat_ids
* @return array<int, array<string, int|string>>
*/
private static function get_cats(?array $cat_ids = null): array {
$ret = [];
/* Labels category */
$cv = array("id" => Feeds::CATEGORY_LABELS, "kind" => "cat",
"counter" => Feeds::_get_cat_unread(Feeds::CATEGORY_LABELS));
array_push($ret, $cv);
$pdo = Db::pdo();
if (is_array($cat_ids)) {
if (count($cat_ids) == 0)
return [];
$cat_ids_qmarks = arr_qmarks($cat_ids);
$sth = $pdo->prepare("SELECT fc.id,
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 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
FROM ttrss_feed_categories fc
LEFT JOIN ttrss_feeds f ON (f.cat_id = fc.id)
LEFT JOIN ttrss_user_entries ue ON (ue.feed_id = f.id)
WHERE fc.owner_uid = ? AND fc.id IN ($cat_ids_qmarks)
GROUP BY fc.id
UNION
SELECT 0,
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 published THEN 1 ELSE 0 END) AS count_published,
0
FROM ttrss_feeds f, ttrss_user_entries ue
WHERE f.cat_id IS NULL AND
ue.feed_id = f.id AND
ue.owner_uid = ?");
$sth->execute([$_SESSION['uid'], ...$cat_ids, $_SESSION['uid']]);
} else {
$sth = $pdo->prepare("SELECT fc.id,
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 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
FROM ttrss_feed_categories fc
LEFT JOIN ttrss_feeds f ON (f.cat_id = fc.id)
LEFT JOIN ttrss_user_entries ue ON (ue.feed_id = f.id)
WHERE fc.owner_uid = :uid
GROUP BY fc.id
UNION
SELECT 0,
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 published THEN 1 ELSE 0 END) AS count_published,
0
FROM ttrss_feeds f, ttrss_user_entries ue
WHERE f.cat_id IS NULL AND
ue.feed_id = f.id AND
ue.owner_uid = :uid");
$sth->execute(["uid" => $_SESSION['uid']]);
}
while ($line = $sth->fetch()) {
if ($line["num_children"] > 0) {
list ($child_counter, $child_marked_counter, $child_published_counter) = self::get_cat_children($line["id"], $_SESSION["uid"]);
} else {
$child_counter = 0;
$child_marked_counter = 0;
$child_published_counter = 0;
}
$cv = [
"id" => (int)$line["id"],
"kind" => "cat",
"markedcounter" => (int) $line["count_marked"] + $child_marked_counter,
"publishedcounter" => (int) $line["count_published"] + $child_published_counter,
"counter" => (int) $line["count"] + $child_counter
];
array_push($ret, $cv);
}
return $ret;
}
/**
* @param array<int>|null $feed_ids
* @return array<int, array<string, int|string>>
*/
private static function get_feeds(?array $feed_ids = null): array {
$ret = [];
if (is_array($feed_ids) && count($feed_ids) === 0)
return $ret;
$feeds = ORM::for_table('ttrss_feeds')
->table_alias('f')
->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');
if (is_array($feed_ids))
$feeds->where_in('f.id', $feed_ids);
foreach ($feeds->find_many() as $feed) {
$ret[] = [
'id' => $feed->id,
'title' => truncate_string($feed->title, 30),
'error' => $feed->last_error,
'updated' => TimeHelper::make_local_datetime($feed->last_updated),
'counter' => (int) $feed->count,
'markedcounter' => (int) $feed->count_marked,
'publishedcounter' => (int) $feed->count_published,
'ts' => Feeds::_has_icon($feed->id) ? (int) filemtime(Feeds::_get_icon_file($feed->id)) : 0,
];
}
return $ret;
}
/**
* @return array<int, array<string, int|string>>
*/
private static function get_global(): array {
$ret = [
[
"id" => "global-unread",
"counter" => (int) Feeds::_get_global_unread()
]
];
$subcribed_feeds = ORM::for_table('ttrss_feeds')
->where('owner_uid', $_SESSION['uid'])
->count();
array_push($ret, [
"id" => "subscribed-feeds",
"counter" => $subcribed_feeds
]);
return $ret;
}
/**
* @return array<int, array<string, int|string>>
*/
private static function get_virt(): array {
$ret = [];
foreach ([Feeds::FEED_ARCHIVED, Feeds::FEED_STARRED, Feeds::FEED_PUBLISHED,
Feeds::FEED_FRESH, Feeds::FEED_ALL] as $feed_id) {
$count = Feeds::_get_counters($feed_id, false, true);
if (in_array($feed_id, [Feeds::FEED_ARCHIVED, Feeds::FEED_STARRED, Feeds::FEED_PUBLISHED]))
$auxctr = Feeds::_get_counters($feed_id, false);
else
$auxctr = 0;
$cv = [
"id" => $feed_id,
"counter" => (int) $count,
"auxcounter" => (int) $auxctr
];
if ($feed_id == Feeds::FEED_STARRED)
$cv["markedcounter"] = $auxctr;
if ($feed_id == Feeds::FEED_PUBLISHED)
$cv["publishedcounter"] = $auxctr;
array_push($ret, $cv);
}
foreach (PluginHost::getInstance()->get_feeds(Feeds::CATEGORY_SPECIAL) as $feed) {
if (!implements_interface($feed['sender'], 'IVirtualFeed'))
continue;
/** @var Plugin&IVirtualFeed $feed['sender'] */
$cv = [
"id" => PluginHost::pfeed_to_feed_id($feed['id']),
"counter" => $feed['sender']->get_unread($feed['id'])
];
if (method_exists($feed['sender'], 'get_total'))
$cv["auxcounter"] = $feed['sender']->get_total($feed['id']);
array_push($ret, $cv);
}
return $ret;
}
/**
* @param array<int>|null $label_ids
* @return array<int, array<string, int|string>>
*/
static function get_labels(?array $label_ids = null): array {
$ret = [];
$pdo = Db::pdo();
if (is_array($label_ids)) {
if (count($label_ids) == 0)
return [];
$label_ids_qmarks = arr_qmarks($label_ids);
$sth = $pdo->prepare("SELECT id,
caption,
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.published = true THEN 1 ELSE 0 END) AS count_published,
COUNT(u1.unread) AS total
FROM ttrss_labels2 LEFT JOIN ttrss_user_labels2 ON
(ttrss_labels2.id = label_id)
LEFT JOIN ttrss_user_entries AS u1 ON u1.ref_id = article_id AND u1.owner_uid = ?
WHERE ttrss_labels2.owner_uid = ? AND ttrss_labels2.id IN ($label_ids_qmarks)
GROUP BY ttrss_labels2.id, ttrss_labels2.caption");
$sth->execute([$_SESSION["uid"], $_SESSION["uid"], ...$label_ids]);
} else {
$sth = $pdo->prepare("SELECT id,
caption,
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.published = true THEN 1 ELSE 0 END) AS count_published,
COUNT(u1.unread) AS total
FROM ttrss_labels2 LEFT JOIN ttrss_user_labels2 ON
(ttrss_labels2.id = label_id)
LEFT JOIN ttrss_user_entries AS u1 ON u1.ref_id = article_id AND u1.owner_uid = :uid
WHERE ttrss_labels2.owner_uid = :uid
GROUP BY ttrss_labels2.id, ttrss_labels2.caption");
$sth->execute([":uid" => $_SESSION['uid']]);
}
while ($line = $sth->fetch()) {
$id = Labels::label_to_feed_id($line["id"]);
$cv = [
"id" => $id,
"counter" => (int) $line["count_unread"],
"auxcounter" => (int) $line["total"],
"markedcounter" => (int) $line["count_marked"],
"publishedcounter" => (int) $line["count_published"],
"description" => $line["caption"]
];
array_push($ret, $cv);
}
return $ret;
}
}

62
classes/Crypt.php Normal file
View 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']);
}
}

80
classes/Db.php Normal file
View File

@@ -0,0 +1,80 @@
<?php
class Db {
private static ?Db $instance = null;
private ?PDO $pdo = null;
function __construct() {
ORM::configure(self::get_dsn());
ORM::configure('username', Config::get(Config::DB_USER));
ORM::configure('password', Config::get(Config::DB_PASS));
ORM::configure('return_result_sets', true);
}
/**
* @param int $delta adjust generated timestamp by this value in seconds (either positive or negative)
* @return string
*/
static function NOW(int $delta = 0): string {
return date("Y-m-d H:i:s", time() + $delta);
}
private function __clone() {
//
}
public static function get_dsn(): string {
$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_sslmode = Config::get(Config::DB_SSLMODE);
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
// normal usage is Db::pdo()->prepare(...) etc
public function pdo_connect() : PDO {
try {
$pdo = new PDO(self::get_dsn(),
Config::get(Config::DB_USER),
Config::get(Config::DB_PASS));
} catch (Exception $e) {
print "<pre>Exception while creating PDO object:" . $e->getMessage() . "</pre>";
exit(101);
}
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->query("set client_encoding = 'UTF-8'");
$pdo->query("set datestyle = 'ISO, european'");
$pdo->query("set TIME ZONE 0");
$pdo->query("set cpu_tuple_cost = 0.5");
return $pdo;
}
public static function instance() : Db {
if (self::$instance == null)
self::$instance = new self();
return self::$instance;
}
public static function pdo() : PDO {
if (self::$instance == null)
self::$instance = new self();
if (empty(self::$instance->pdo)) {
self::$instance->pdo = self::$instance->pdo_connect();
}
return self::$instance->pdo;
}
/** @deprecated usages should be replaced with `RANDOM()` */
public static function sql_random_function(): string {
return "RANDOM()";
}
}

200
classes/Db_Migrations.php Normal file
View File

@@ -0,0 +1,200 @@
<?php
class Db_Migrations {
private string $base_filename = "schema.sql";
private string $base_path;
private string $migrations_path;
private string $migrations_table;
private bool $base_is_latest;
private PDO $pdo;
private int $cached_version = 0;
private int $cached_max_version = 0;
private int $max_version_override;
function __construct() {
$this->pdo = Db::pdo();
}
function initialize_for_plugin(Plugin $plugin, bool $base_is_latest = true, string $schema_suffix = "sql"): void {
$plugin_dir = PluginHost::getInstance()->get_plugin_dir($plugin);
$this->initialize("{$plugin_dir}/{$schema_suffix}",
strtolower("ttrss_migrations_plugin_" . get_class($plugin)),
$base_is_latest);
}
function initialize(string $root_path, string $migrations_table, bool $base_is_latest = true, int $max_version_override = 0): void {
$this->base_path = "$root_path/pgsql";
$this->migrations_path = $this->base_path . "/migrations";
$this->migrations_table = $migrations_table;
$this->base_is_latest = $base_is_latest;
$this->max_version_override = $max_version_override;
}
private function set_version(int $version): void {
Debug::log("Updating table {$this->migrations_table} with version {$version}...", Debug::LOG_EXTENDED);
$sth = $this->pdo->query("SELECT * FROM {$this->migrations_table}");
if ($sth->fetch()) {
$sth = $this->pdo->prepare("UPDATE {$this->migrations_table} SET schema_version = ?");
} else {
$sth = $this->pdo->prepare("INSERT INTO {$this->migrations_table} (schema_version) VALUES (?)");
}
$sth->execute([$version]);
$this->cached_version = $version;
}
function get_version() : int {
if ($this->cached_version)
return $this->cached_version;
try {
$sth = $this->pdo->query("SELECT * FROM {$this->migrations_table}");
if ($res = $sth->fetch()) {
return (int) $res['schema_version'];
} else {
return -1;
}
} catch (PDOException $e) {
$this->create_migrations_table();
return -1;
}
}
private function create_migrations_table(): void {
$this->pdo->query("CREATE TABLE IF NOT EXISTS {$this->migrations_table} (schema_version integer not null)");
}
/**
* @throws PDOException
* @return bool false if the migration failed, otherwise true (or an exception)
*/
private function migrate_to(int $version): bool {
try {
if ($version <= $this->get_version()) {
Debug::log("Refusing to apply version $version: current version is higher", Debug::LOG_VERBOSE);
return false;
}
if ($version == 0)
Debug::log("Loading base database schema...", Debug::LOG_VERBOSE);
else
Debug::log("Starting migration to $version...", Debug::LOG_VERBOSE);
$lines = $this->get_lines($version);
if (count($lines) > 0) {
$this->pdo->beginTransaction();
foreach ($lines as $line) {
Debug::log($line, Debug::LOG_EXTENDED);
try {
$this->pdo->query($line);
} catch (PDOException $e) {
Debug::log("Failed on line: $line", Debug::LOG_VERBOSE);
throw $e;
}
}
if ($version == 0 && $this->base_is_latest)
$this->set_version($this->get_max_version());
else
$this->set_version($version);
$this->pdo->commit();
Debug::log("Migration finished, current version: " . $this->get_version(), Debug::LOG_VERBOSE);
Logger::log(E_USER_NOTICE, "Applied migration to version $version for {$this->migrations_table}");
return true;
} else {
Debug::log("Migration failed: schema file is empty or missing.", Debug::LOG_VERBOSE);
return false;
}
} catch (PDOException $e) {
Debug::log("Migration failed: " . $e->getMessage(), Debug::LOG_VERBOSE);
try {
$this->pdo->rollback();
} catch (PDOException $ie) {
//
}
throw $e;
}
}
function get_max_version() : int {
if ($this->max_version_override > 0)
return $this->max_version_override;
if ($this->cached_max_version)
return $this->cached_max_version;
$migrations = glob("{$this->migrations_path}/*.sql");
if (count($migrations) > 0) {
natsort($migrations);
$this->cached_max_version = (int) basename(array_pop($migrations), ".sql");
} else {
$this->cached_max_version = 0;
}
return $this->cached_max_version;
}
function is_migration_needed() : bool {
return $this->get_version() != $this->get_max_version();
}
function migrate() : bool {
if ($this->get_version() == -1) {
try {
$this->migrate_to(0);
} catch (PDOException $e) {
user_error("Failed to load base schema for {$this->migrations_table}: " . $e->getMessage(), E_USER_WARNING);
return false;
}
}
for ($i = $this->get_version() + 1; $i <= $this->get_max_version(); $i++) {
try {
$this->migrate_to($i);
} catch (PDOException $e) {
user_error("Failed to apply migration {$i} for {$this->migrations_table}: " . $e->getMessage(), E_USER_WARNING);
return false;
//throw $e;
}
}
return !$this->is_migration_needed();
}
/**
* @return array<int, string>
*/
private function get_lines(int $version) : array {
if ($version > 0)
$filename = "{$this->migrations_path}/{$version}.sql";
else
$filename = "{$this->base_path}/{$this->base_filename}";
if (file_exists($filename)) {
$lines = array_filter(preg_split("/[\r\n]/", file_get_contents($filename)),
fn($line) => strlen(trim($line)) > 0 && !str_starts_with($line, "--"));
return array_filter(explode(";", implode("", $lines)),
fn($line) => strlen(trim($line)) > 0 && !in_array(strtolower($line), ["begin", "commit"]));
} else {
user_error("Requested schema file {$filename} not found.", E_USER_ERROR);
return [];
}
}
}

12
classes/Db_Prefs.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
class Db_Prefs {
// 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 Prefs::get($pref_name, $user_id ?: $_SESSION['uid'], $_SESSION['profile'] ?? null);
}
function write(string $pref_name, mixed $value, ?int $user_id = null, bool $strip_tags = true): bool {
return Prefs::set($pref_name, $value, $user_id ?: $_SESSION['uid'], $_SESSION['profile'] ?? null, $strip_tags);
}
}

161
classes/Debug.php Normal file
View File

@@ -0,0 +1,161 @@
<?php
class Debug {
const LOG_DISABLED = -1;
const LOG_NORMAL = 0;
const LOG_VERBOSE = 1;
const LOG_EXTENDED = 2;
const SEPARATOR = "<-{log-separator}->";
const ALL_LOG_LEVELS = [
Debug::LOG_DISABLED,
Debug::LOG_NORMAL,
Debug::LOG_VERBOSE,
Debug::LOG_EXTENDED,
];
/**
* @deprecated
*/
public static int $LOG_DISABLED = self::LOG_DISABLED;
/**
* @deprecated
*/
public static int $LOG_NORMAL = self::LOG_NORMAL;
/**
* @deprecated
*/
public static int $LOG_VERBOSE = self::LOG_VERBOSE;
/**
* @deprecated
*/
public static int $LOG_EXTENDED = self::LOG_EXTENDED;
private static bool $enabled = false;
private static bool $quiet = false;
private static ?string $logfile = null;
private static bool $enable_html = false;
private static int $loglevel = self::LOG_NORMAL;
public static function set_logfile(string $logfile): void {
self::$logfile = $logfile;
}
public static function enabled(): bool {
return self::$enabled;
}
public static function set_enabled(bool $enable): void {
self::$enabled = $enable;
}
public static function set_quiet(bool $quiet): void {
self::$quiet = $quiet;
}
/**
* @param Debug::LOG_* $level
*/
public static function set_loglevel(int $level): void {
self::$loglevel = $level;
}
/**
* @return int Debug::LOG_*
*/
public static function get_loglevel(): int {
return self::$loglevel;
}
/**
* @param int $level integer loglevel value
* @return Debug::LOG_* if valid, warn and return LOG_DISABLED otherwise
*/
public static function map_loglevel(int $level) : int {
if (in_array($level, self::ALL_LOG_LEVELS)) {
/** @phpstan-ignore return.type (yes it is a Debug::LOG_* value) */
return $level;
} else {
user_error("Passed invalid debug log level: $level", E_USER_WARNING);
return self::LOG_DISABLED;
}
}
public static function enable_html(bool $enable) : void {
self::$enable_html = $enable;
}
/**
* @param Debug::LOG_* $level log level
*/
public static function log(string $message, int $level = Debug::LOG_NORMAL): bool {
if (!self::$enabled || self::$loglevel < $level) return false;
$ts = date("H:i:s", time());
if (function_exists('posix_getpid')) {
$ts = "$ts/" . posix_getpid();
}
$orig_message = $message;
if ($message === self::SEPARATOR) {
$message = self::$enable_html ? "<hr/>" :
"=================================================================================================================================";
}
if (self::$logfile) {
$fp = fopen(self::$logfile, 'a+');
if ($fp) {
$locked = false;
if (function_exists("flock")) {
$tries = 0;
// try to lock logfile for writing
while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB)) {
sleep(1);
++$tries;
}
if (!$locked) {
fclose($fp);
user_error("Unable to lock debugging log file: " . self::$logfile, E_USER_WARNING);
return false;
}
}
fputs($fp, "[$ts] $message\n");
if (function_exists("flock")) {
flock($fp, LOCK_UN);
}
fclose($fp);
if (self::$quiet)
return false;
} else {
user_error("Unable to open debugging log file: " . self::$logfile, E_USER_WARNING);
}
}
if (self::$enable_html) {
if ($orig_message === self::SEPARATOR) {
print "$message\n";
} else {
print "<span class='log-timestamp'>$ts</span> <span class='log-message'>$message</span>\n";
}
} else {
print "[$ts] $message\n";
}
return true;
}
}

193
classes/Digest.php Normal file
View File

@@ -0,0 +1,193 @@
<?php
class Digest
{
static function send_headlines_digests(): void {
$user_limit = 15; // amount of users to process (e.g. emails to send out)
$limit = 1000; // maximum amount of headlines to include
Debug::log("Sending digests, batch of max $user_limit users, headline limit = $limit");
$pdo = Db::pdo();
$res = $pdo->query("SELECT id, login, email FROM ttrss_users
WHERE email != '' AND (last_digest_sent IS NULL OR last_digest_sent < NOW() - INTERVAL '1 day')");
while ($line = $res->fetch()) {
if (Prefs::get(Prefs::DIGEST_ENABLE, $line['id'])) {
$preferred_ts = strtotime(Prefs::get(Prefs::DIGEST_PREFERRED_TIME, $line['id']) ?? '');
// try to send digests within 2 hours of preferred time
if ($preferred_ts && time() >= $preferred_ts &&
time() - $preferred_ts <= 7200
) {
Debug::log("Sending digest for UID:" . $line['id'] . " - " . $line["email"]);
$do_catchup = Prefs::get(Prefs::DIGEST_CATCHUP, $line['id']);
global $tz_offset;
// reset tz_offset global to prevent tz cache clash between users
$tz_offset = -1;
$tuple = Digest::prepare_headlines_digest($line["id"], 1, $limit);
$digest = $tuple[0];
$headlines_count = $tuple[1];
$affected_ids = $tuple[2];
$digest_text = $tuple[3];
if ($headlines_count > 0) {
$mailer = new Mailer();
//$rc = $mail->quickMail($line["email"], $line["login"], Config::get(Config::DIGEST_SUBJECT), $digest, $digest_text);
$rc = $mailer->mail(["to_name" => $line["login"],
"to_address" => $line["email"],
"subject" => Config::get(Config::DIGEST_SUBJECT),
"message" => $digest_text,
"message_html" => $digest]);
//if (!$rc && $debug) Debug::log("ERROR: " . $mailer->lastError());
Debug::log("RC=$rc");
if ($rc && $do_catchup) {
Debug::log("Marking affected articles as read...");
Article::_catchup_by_id($affected_ids, Article::CATCHUP_MODE_MARK_AS_READ, $line["id"]);
}
} else {
Debug::log("No headlines");
}
$sth = $pdo->prepare("UPDATE ttrss_users SET last_digest_sent = NOW()
WHERE id = ?");
$sth->execute([$line["id"]]);
}
}
}
Debug::log("All done.");
}
/**
* @return array{0: string, 1: int, 2: array<int>, 3: string}
*/
static function prepare_headlines_digest(int $user_id, int $days = 1, int $limit = 1000) {
$tpl = new Templator();
$tpl_t = new Templator();
$tpl->readTemplateFromFile("digest_template_html.txt");
$tpl_t->readTemplateFromFile("digest_template.txt");
$user_tz_string = Prefs::get(Prefs::USER_TIMEZONE, $user_id);
$min_score = Prefs::get(Prefs::DIGEST_MIN_SCORE, $user_id);
if ($user_tz_string == 'Automatic')
$user_tz_string = 'GMT';
$local_ts = TimeHelper::convert_timestamp(time(), 'UTC', $user_tz_string);
$tpl->setVariable('CUR_DATE', date('Y/m/d', $local_ts));
$tpl->setVariable('CUR_TIME', date('G:i', $local_ts));
$tpl->setVariable('TTRSS_HOST', Config::get_self_url());
$tpl_t->setVariable('CUR_DATE', date('Y/m/d', $local_ts));
$tpl_t->setVariable('CUR_TIME', date('G:i', $local_ts));
$tpl_t->setVariable('TTRSS_HOST', Config::get_self_url());
$affected_ids = array();
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT ttrss_entries.title,
ttrss_feeds.title AS feed_title,
COALESCE(ttrss_feed_categories.title, '" . __('Uncategorized') . "') AS cat_title,
date_updated,
ttrss_user_entries.ref_id,
link,
score,
content,
SUBSTRING_FOR_DATE(last_updated,1,19) AS last_updated
FROM
ttrss_user_entries,ttrss_entries,ttrss_feeds
LEFT JOIN
ttrss_feed_categories ON (cat_id = ttrss_feed_categories.id)
WHERE
ref_id = ttrss_entries.id AND feed_id = ttrss_feeds.id
AND include_in_digest = true
AND ttrss_entries.date_updated > NOW() - INTERVAL '$days day'
AND ttrss_user_entries.owner_uid = :user_id
AND unread = true
AND score >= :min_score
ORDER BY ttrss_feed_categories.title, ttrss_feeds.title, score DESC, date_updated DESC
LIMIT " . (int)$limit);
$sth->execute([':user_id' => $user_id, ':min_score' => $min_score]);
$headlines_count = 0;
$headlines = array();
while ($line = $sth->fetch()) {
array_push($headlines, $line);
$headlines_count++;
}
for ($i = 0; $i < sizeof($headlines); $i++) {
$line = $headlines[$i];
array_push($affected_ids, $line["ref_id"]);
$updated = TimeHelper::make_local_datetime($line['last_updated'], owner_uid: $user_id);
if (Prefs::get(Prefs::ENABLE_FEED_CATS, $user_id)) {
$line['feed_title'] = $line['cat_title'] . " / " . $line['feed_title'];
}
$article_labels = Article::_get_labels($line["ref_id"], $user_id);
$article_labels_formatted = "";
if (count($article_labels) > 0) {
$article_labels_formatted = implode(", ", array_map(fn($a) => $a[1], $article_labels));
}
$tpl->setVariable('FEED_TITLE', $line["feed_title"]);
$tpl->setVariable('ARTICLE_TITLE', $line["title"]);
$tpl->setVariable('ARTICLE_LINK', $line["link"]);
$tpl->setVariable('ARTICLE_UPDATED', $updated);
$tpl->setVariable('ARTICLE_EXCERPT',
truncate_string(strip_tags($line["content"]), 300));
// $tpl->setVariable('ARTICLE_CONTENT',
// strip_tags($article_content));
$tpl->setVariable('ARTICLE_LABELS', $article_labels_formatted, true);
$tpl->addBlock('article');
$tpl_t->setVariable('FEED_TITLE', $line["feed_title"]);
$tpl_t->setVariable('ARTICLE_TITLE', $line["title"]);
$tpl_t->setVariable('ARTICLE_LINK', $line["link"]);
$tpl_t->setVariable('ARTICLE_UPDATED', $updated);
$tpl_t->setVariable('ARTICLE_LABELS', $article_labels_formatted, true);
$tpl_t->setVariable('ARTICLE_EXCERPT',
truncate_string(strip_tags($line["content"]), 300, "..."), true);
$tpl_t->addBlock('article');
if (!isset($headlines[$i + 1]) || $headlines[$i]['feed_title'] != $headlines[$i + 1]['feed_title']) {
$tpl->addBlock('feed');
$tpl_t->addBlock('feed');
}
}
$tpl->addBlock('digest');
$tpl->generateOutputToString($tmp);
$tpl_t->addBlock('digest');
$tpl_t->generateOutputToString($tmp_t);
return array($tmp, $headlines_count, $affected_ids, $tmp_t);
}
}

458
classes/DiskCache.php Normal file
View File

@@ -0,0 +1,458 @@
<?php
class DiskCache implements Cache_Adapter {
private Cache_Adapter $adapter;
/** @var array<string, DiskCache> $instances */
private static array $instances = [];
/**
* https://stackoverflow.com/a/53662733
*
* @var array<string, string>
*/
private array $mimeMap = [
'video/3gpp2' => '3g2',
'video/3gp' => '3gp',
'video/3gpp' => '3gp',
'application/x-compressed' => '7zip',
'audio/x-acc' => 'aac',
'audio/ac3' => 'ac3',
'application/postscript' => 'ai',
'audio/x-aiff' => 'aif',
'audio/aiff' => 'aif',
'audio/x-au' => 'au',
'video/x-msvideo' => 'avi',
'video/msvideo' => 'avi',
'video/avi' => 'avi',
'application/x-troff-msvideo' => 'avi',
'application/macbinary' => 'bin',
'application/mac-binary' => 'bin',
'application/x-binary' => 'bin',
'application/x-macbinary' => 'bin',
'image/bmp' => 'bmp',
'image/x-bmp' => 'bmp',
'image/x-bitmap' => 'bmp',
'image/x-xbitmap' => 'bmp',
'image/x-win-bitmap' => 'bmp',
'image/x-windows-bmp' => 'bmp',
'image/ms-bmp' => 'bmp',
'image/x-ms-bmp' => 'bmp',
'application/bmp' => 'bmp',
'application/x-bmp' => 'bmp',
'application/x-win-bitmap' => 'bmp',
'application/cdr' => 'cdr',
'application/coreldraw' => 'cdr',
'application/x-cdr' => 'cdr',
'application/x-coreldraw' => 'cdr',
'image/cdr' => 'cdr',
'image/x-cdr' => 'cdr',
'zz-application/zz-winassoc-cdr' => 'cdr',
'application/mac-compactpro' => 'cpt',
'application/pkix-crl' => 'crl',
'application/pkcs-crl' => 'crl',
'application/x-x509-ca-cert' => 'crt',
'application/pkix-cert' => 'crt',
'text/css' => 'css',
'text/x-comma-separated-values' => 'csv',
'text/comma-separated-values' => 'csv',
'application/vnd.msexcel' => 'csv',
'application/x-director' => 'dcr',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
'application/x-dvi' => 'dvi',
'message/rfc822' => 'eml',
'application/x-msdownload' => 'exe',
'video/x-f4v' => 'f4v',
'audio/x-flac' => 'flac',
'video/x-flv' => 'flv',
'image/gif' => 'gif',
'application/gpg-keys' => 'gpg',
'application/x-gtar' => 'gtar',
'application/x-gzip' => 'gzip',
'application/mac-binhex40' => 'hqx',
'application/mac-binhex' => 'hqx',
'application/x-binhex40' => 'hqx',
'application/x-mac-binhex40' => 'hqx',
'text/html' => 'html',
'image/x-icon' => 'ico',
'image/x-ico' => 'ico',
'image/vnd.microsoft.icon' => 'ico',
'text/calendar' => 'ics',
'application/java-archive' => 'jar',
'application/x-java-application' => 'jar',
'application/x-jar' => 'jar',
'image/jp2' => 'jp2',
'video/mj2' => 'jp2',
'image/jpx' => 'jp2',
'image/jpm' => 'jp2',
'image/jpeg' => 'jpg',
'image/pjpeg' => 'jpg',
'application/x-javascript' => 'js',
'application/json' => 'json',
'text/json' => 'json',
'application/vnd.google-earth.kml+xml' => 'kml',
'application/vnd.google-earth.kmz' => 'kmz',
'text/x-log' => 'log',
'audio/x-m4a' => 'm4a',
'audio/mp4' => 'm4a',
'application/vnd.mpegurl' => 'm4u',
'audio/midi' => 'mid',
'application/vnd.mif' => 'mif',
'video/quicktime' => 'mov',
'video/x-sgi-movie' => 'movie',
'audio/mpeg' => 'mp3',
'audio/mpg' => 'mp3',
'audio/mpeg3' => 'mp3',
'audio/mp3' => 'mp3',
'video/mp4' => 'mp4',
'video/mpeg' => 'mpeg',
'application/oda' => 'oda',
'audio/ogg' => 'ogg',
'video/ogg' => 'ogg',
'application/ogg' => 'ogg',
'font/otf' => 'otf',
'application/x-pkcs10' => 'p10',
'application/pkcs10' => 'p10',
'application/x-pkcs12' => 'p12',
'application/x-pkcs7-signature' => 'p7a',
'application/pkcs7-mime' => 'p7c',
'application/x-pkcs7-mime' => 'p7c',
'application/x-pkcs7-certreqresp' => 'p7r',
'application/pkcs7-signature' => 'p7s',
'application/pdf' => 'pdf',
'application/octet-stream' => 'pdf',
'application/x-x509-user-cert' => 'pem',
'application/x-pem-file' => 'pem',
'application/pgp' => 'pgp',
'application/x-httpd-php' => 'php',
'application/php' => 'php',
'application/x-php' => 'php',
'text/php' => 'php',
'text/x-php' => 'php',
'application/x-httpd-php-source' => 'php',
'image/png' => 'png',
'image/x-png' => 'png',
'application/powerpoint' => 'ppt',
'application/vnd.ms-powerpoint' => 'ppt',
'application/vnd.ms-office' => 'ppt',
'application/msword' => 'ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
'application/x-photoshop' => 'psd',
'image/vnd.adobe.photoshop' => 'psd',
'audio/x-realaudio' => 'ra',
'audio/x-pn-realaudio' => 'ram',
'application/x-rar' => 'rar',
'application/rar' => 'rar',
'application/x-rar-compressed' => 'rar',
'audio/x-pn-realaudio-plugin' => 'rpm',
'application/x-pkcs7' => 'rsa',
'text/rtf' => 'rtf',
'text/richtext' => 'rtx',
'video/vnd.rn-realvideo' => 'rv',
'application/x-stuffit' => 'sit',
'application/smil' => 'smil',
'text/srt' => 'srt',
'image/svg+xml' => 'svg',
'application/x-shockwave-flash' => 'swf',
'application/x-tar' => 'tar',
'application/x-gzip-compressed' => 'tgz',
'image/tiff' => 'tiff',
'font/ttf' => 'ttf',
'text/plain' => 'txt',
'text/x-vcard' => 'vcf',
'application/videolan' => 'vlc',
'text/vtt' => 'vtt',
'audio/x-wav' => 'wav',
'audio/wave' => 'wav',
'audio/wav' => 'wav',
'application/wbxml' => 'wbxml',
'video/webm' => 'webm',
'image/webp' => 'webp',
'audio/x-ms-wma' => 'wma',
'application/wmlc' => 'wmlc',
'video/x-ms-wmv' => 'wmv',
'video/x-ms-asf' => 'wmv',
'font/woff' => 'woff',
'font/woff2' => 'woff2',
'application/xhtml+xml' => 'xhtml',
'application/excel' => 'xl',
'application/msexcel' => 'xls',
'application/x-msexcel' => 'xls',
'application/x-ms-excel' => 'xls',
'application/x-excel' => 'xls',
'application/x-dos_ms_excel' => 'xls',
'application/xls' => 'xls',
'application/x-xls' => 'xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
'application/vnd.ms-excel' => 'xlsx',
'application/xml' => 'xml',
'text/xml' => 'xml',
'text/xsl' => 'xsl',
'application/xspf+xml' => 'xspf',
'application/x-compress' => 'z',
'application/x-zip' => 'zip',
'application/zip' => 'zip',
'application/x-zip-compressed' => 'zip',
'application/s-compressed' => 'zip',
'multipart/x-zip' => 'zip',
'text/x-scriptzsh' => 'zsh'
];
public static function instance(string $dir) : DiskCache {
if ((self::$instances[$dir] ?? null) == null)
self::$instances[$dir] = new self($dir);
return self::$instances[$dir];
}
public function __construct(string $dir) {
foreach (PluginHost::getInstance()->get_plugins() as $p) {
if (implements_interface($p, "Cache_Adapter")) {
/** @var Cache_Adapter $p */
$this->adapter = clone $p; // we need separate object instances for separate directories
$this->adapter->set_dir($dir);
return;
}
}
$this->adapter = new Cache_Local();
$this->adapter->set_dir($dir);
}
public function remove(string $filename): bool {
$rc = $this->adapter->remove($filename);
return $rc;
}
public function set_dir(string $dir) : void {
$this->adapter->set_dir($dir);
}
/**
* @return int|false -1 if the file doesn't exist, false if an error occurred, timestamp otherwise
*/
public function get_mtime(string $filename) {
return $this->adapter->get_mtime(basename($filename));
}
public function make_dir(): bool {
return $this->adapter->make_dir();
}
/** @param string|null $filename null means check that cache directory itself is writable */
public function is_writable(?string $filename = null): bool {
return $this->adapter->is_writable($filename ? basename($filename) : null);
}
public function exists(string $filename): bool {
$rc = $this->adapter->exists(basename($filename));
return $rc;
}
/**
* @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) {
$rc = $this->adapter->get_size(basename($filename));
return $rc;
}
/**
* @param mixed $data
*
* @return int|false Bytes written or false if an error occurred.
*/
public function put(string $filename, $data) {
return $this->adapter->put(basename($filename), $data);
}
/** @deprecated we can't assume cached files are local, and other storages
* might not support this operation (object metadata may be immutable) */
public function touch(string $filename): bool {
user_error("DiskCache: called unsupported method touch() for $filename", E_USER_DEPRECATED);
return false;
}
public function get(string $filename): ?string {
return $this->adapter->get(basename($filename));
}
public function expire_all(): void {
$this->adapter->expire_all();
}
public function get_dir(): string {
return $this->adapter->get_dir();
}
/** Downloads $url to cache as $local_filename if its missing (unless $force-ed)
* @param string $url
* @param string $local_filename
* @param array<string,string|int|false> $options (additional params to UrlHelper::fetch())
* @param bool $force
* @return bool
*/
public function download(string $url, string $local_filename, array $options = [], bool $force = false) : bool {
if ($this->exists($local_filename) && !$force)
return true;
$data = UrlHelper::fetch([
'url' => $url,
'max_size' => Config::get(Config::MAX_CACHE_FILE_SIZE),
...$options,
]);
if ($data)
return $this->put($local_filename, $data) > 0;
return false;
}
public function send(string $filename) {
$filename = basename($filename);
if (!$this->exists($filename)) {
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
echo "File not found.";
return false;
}
$file_mtime = $this->get_mtime($filename);
$gmt_modified = gmdate("D, d M Y H:i:s", (int)$file_mtime) . " GMT";
if (($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '') == $gmt_modified || ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') == $file_mtime) {
header('HTTP/1.1 304 Not Modified');
return false;
}
$mimetype = $this->get_mime_type($filename);
if ($mimetype == "application/octet-stream")
$mimetype = "video/mp4";
# block SVG because of possible embedded javascript (.....)
$mimetype_blacklist = [ "image/svg+xml" ];
/* only serve video and images */
if (!preg_match("/(image|audio|video)\//", (string)$mimetype) || in_array($mimetype, $mimetype_blacklist)) {
http_response_code(400);
header("Content-type: text/plain");
print "Stored file has disallowed content type ($mimetype)";
return false;
}
$fake_extension = $this->get_fake_extension($filename);
if ($fake_extension)
$fake_extension = ".$fake_extension";
header("Content-Disposition: inline; filename=\"{$filename}{$fake_extension}\"");
header("Content-type: $mimetype");
$stamp_expires = gmdate("D, d M Y H:i:s",
(int)$this->get_mtime($filename) + 86400 * Config::get(Config::CACHE_MAX_DAYS)) . " GMT";
header("Expires: $stamp_expires", true);
header("Last-Modified: $gmt_modified", true);
header("Cache-Control: no-cache");
header("ETag: $file_mtime");
header_remove("Pragma");
return $this->adapter->send($filename);
}
public function get_full_path(string $filename): string {
return $this->adapter->get_full_path(basename($filename));
}
public function get_mime_type(string $filename) {
return $this->adapter->get_mime_type(basename($filename));
}
public function get_fake_extension(string $filename): string {
$mimetype = $this->adapter->get_mime_type(basename($filename));
if ($mimetype)
return $this->mimeMap[$mimetype] ?? "";
else
return "";
}
public function get_url(string $filename): string {
return Config::get_self_url() . "/public.php?op=cached&file=" . basename($this->adapter->get_dir()) . "/" . basename($filename);
}
// check for locally cached (media) URLs and rewrite to local versions
// this is called separately after sanitize() and plugin render article hooks to allow
// plugins work on original source URLs used before caching
// NOTE: URLs should be already absolutized because this is called after sanitize()
static public function rewrite_urls(string $str): string {
$res = trim($str);
if (!$res) {
return '';
}
$doc = new DOMDocument();
if (@$doc->loadHTML('<?xml encoding="UTF-8">' . $res)) {
$xpath = new DOMXPath($doc);
$cache = DiskCache::instance("images");
$need_saving = false;
$entries = $xpath->query('(//img[@src]|//source[@src|@srcset]|//video[@poster|@src])');
/** @var DOMElement $entry */
foreach ($entries as $entry) {
foreach (array('src', 'poster') as $attr) {
if ($entry->hasAttribute($attr)) {
$url = $entry->getAttribute($attr);
$cached_filename = sha1($url);
if ($cache->exists($cached_filename)) {
$url = $cache->get_url($cached_filename);
$entry->setAttribute($attr, $url);
$entry->removeAttribute("srcset");
$need_saving = true;
}
}
}
if ($entry->hasAttribute("srcset")) {
$matches = RSSUtils::decode_srcset($entry->getAttribute('srcset'));
for ($i = 0; $i < count($matches); $i++) {
$cached_filename = sha1($matches[$i]["url"]);
if ($cache->exists($cached_filename)) {
$matches[$i]["url"] = $cache->get_url($cached_filename);
$need_saving = true;
}
}
$entry->setAttribute("srcset", RSSUtils::encode_srcset($matches));
}
}
if ($need_saving) {
if (isset($doc->firstChild))
$doc->removeChild($doc->firstChild); //remove doctype
$res = $doc->saveHTML();
}
}
return $res;
}
}

40
classes/Errors.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
class Errors {
const E_SUCCESS = "E_SUCCESS";
const E_UNAUTHORIZED = "E_UNAUTHORIZED";
const E_UNKNOWN_METHOD = "E_UNKNOWN_METHOD";
const E_UNKNOWN_PLUGIN = "E_UNKNOWN_PLUGIN";
const E_SCHEMA_MISMATCH = "E_SCHEMA_MISMATCH";
const E_URL_SCHEME_MISMATCH = "E_URL_SCHEME_MISMATCH";
/**
* @param Errors::E_* $code
* @param array<string, string> $params
*/
static function to_json(string $code, array $params = []): string {
return json_encode(["error" => ["code" => $code, "params" => $params]]);
}
static function libxml_last_error() : string {
$error = libxml_get_last_error();
$error_formatted = "";
if ($error) {
foreach (libxml_get_errors() as $error) {
if ($error->level == LIBXML_ERR_FATAL) {
// currently only the first error is reported
$error_formatted = self::format_libxml_error($error);
break;
}
}
}
return UConverter::transcode($error_formatted, 'UTF-8', 'UTF-8');
}
static function format_libxml_error(LibXMLError $error) : string {
return sprintf("LibXML error %s at line %d (column %d): %s",
$error->code, $error->line, $error->column,
$error->message);
}
}

View File

@@ -0,0 +1,9 @@
<?php
class FeedEnclosure {
public string $link = '';
public string $type = '';
public string $length = '';
public string $title = '';
public string $height = '';
public string $width = '';
}

24
classes/FeedItem.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
abstract class FeedItem {
abstract function get_id(): string;
/** @return int|false a timestamp on success, false otherwise */
abstract function get_date(): false|int;
abstract function get_link(): string;
abstract function get_title(): string;
abstract function get_description(): string;
abstract function get_content(): string;
abstract function get_comments_url(): string;
abstract function get_comments_count(): int;
/** @return array<int, string> */
abstract function get_categories(): array;
/** @return array<int, FeedEnclosure> */
abstract function get_enclosures(): array;
abstract function get_author(): string;
abstract function get_language(): string;
}

223
classes/FeedItem_Atom.php Normal file
View File

@@ -0,0 +1,223 @@
<?php
class FeedItem_Atom extends FeedItem_Common {
const NS_XML = "http://www.w3.org/XML/1998/namespace";
function get_id(): string {
$id = $this->elem->getElementsByTagName("id")->item(0);
if ($id) {
return $id->nodeValue;
} else {
return clean($this->get_link());
}
}
/**
* @return int|false a timestamp on success, false otherwise
*/
function get_date(): false|int {
$updated = $this->elem->getElementsByTagName("updated")->item(0);
if ($updated) {
return strtotime($updated->nodeValue ?? '');
}
$published = $this->elem->getElementsByTagName("published")->item(0);
if ($published) {
return strtotime($published->nodeValue ?? '');
}
$date = $this->xpath->query("dc:date", $this->elem)->item(0);
if ($date) {
return strtotime($date->nodeValue ?? '');
}
// consistent with strtotime failing to parse
return false;
}
function get_link(): string {
$links = $this->elem->getElementsByTagName("link");
foreach ($links as $link) {
if ($link->hasAttribute("href") &&
(!$link->hasAttribute("rel")
|| $link->getAttribute("rel") == "alternate"
|| $link->getAttribute("rel") == "standout")) {
$base = $this->xpath->evaluate("string(ancestor-or-self::*[@xml:base][1]/@xml:base)", $link);
if ($base)
return UrlHelper::rewrite_relative($base, clean(trim($link->getAttribute("href"))));
else
return clean(trim($link->getAttribute("href")));
}
}
return '';
}
function get_title(): string {
$title = $this->elem->getElementsByTagName("title")->item(0);
return $title ? clean(trim($title->nodeValue)) : '';
}
/**
* @param string|null $base optional (returns $content if $base is null)
* @param string $content an HTML string
*
* @return string the rewritten XML or original $content
*/
private function rewrite_content_to_base(?string $base = null, ?string $content = '') {
if (!empty($base) && !empty($content)) {
$tmpdoc = new DOMDocument();
if (@$tmpdoc->loadHTML('<?xml encoding="UTF-8">' . $content)) {
$tmpxpath = new DOMXPath($tmpdoc);
$elems = $tmpxpath->query("(//*[@href]|//*[@src])");
/** @var DOMElement $elem */
foreach ($elems as $elem) {
if ($elem->hasAttribute("href")) {
$elem->setAttribute("href",
UrlHelper::rewrite_relative($base, $elem->getAttribute("href")));
} else if ($elem->hasAttribute("src")) {
$elem->setAttribute("src",
UrlHelper::rewrite_relative($base, $elem->getAttribute("src")));
}
}
// Fall back to $content if saveXML somehow fails (i.e. returns false)
$modified_content = $tmpdoc->saveXML();
return $modified_content !== false ? $modified_content : $content;
}
}
return $content;
}
function get_content(): string {
/** @var DOMElement|null */
$content = $this->elem->getElementsByTagName("content")->item(0);
if ($content) {
$base = $this->xpath->evaluate("string(ancestor-or-self::*[@xml:base][1]/@xml:base)", $content);
if ($content->hasAttribute('type')) {
if ($content->getAttribute('type') == 'xhtml') {
for ($i = 0; $i < $content->childNodes->length; $i++) {
$child = $content->childNodes->item($i);
if ($child->hasChildNodes()) {
return $this->rewrite_content_to_base($base, $this->doc->saveHTML($child));
}
}
}
}
return $this->rewrite_content_to_base($base, $this->subtree_or_text($content));
}
return '';
}
// TODO: duplicate code should be merged with get_content()
function get_description(): string {
/** @var DOMElement|null */
$content = $this->elem->getElementsByTagName("summary")->item(0);
if ($content) {
$base = $this->xpath->evaluate("string(ancestor-or-self::*[@xml:base][1]/@xml:base)", $content);
if ($content->hasAttribute('type')) {
if ($content->getAttribute('type') == 'xhtml') {
for ($i = 0; $i < $content->childNodes->length; $i++) {
$child = $content->childNodes->item($i);
if ($child->hasChildNodes()) {
return $this->rewrite_content_to_base($base, $this->doc->saveHTML($child));
}
}
}
}
return $this->rewrite_content_to_base($base, $this->subtree_or_text($content));
}
return '';
}
/**
* @return array<int, string>
*/
function get_categories(): array {
$categories = $this->elem->getElementsByTagName("category");
$cats = [];
foreach ($categories as $cat) {
if ($cat->hasAttribute("term"))
array_push($cats, $cat->getAttribute("term"));
}
$categories = $this->xpath->query("dc:subject", $this->elem);
foreach ($categories as $cat) {
array_push($cats, $cat->nodeValue);
}
return $this->normalize_categories($cats);
}
/**
* @return array<int, FeedEnclosure>
*/
function get_enclosures(): array {
$links = $this->elem->getElementsByTagName("link");
$encs = [];
foreach ($links as $link) {
if ($link->hasAttribute("href") && $link->hasAttribute("rel")) {
$base = $this->xpath->evaluate("string(ancestor-or-self::*[@xml:base][1]/@xml:base)", $link);
if ($link->getAttribute("rel") == "enclosure") {
$enc = new FeedEnclosure();
$enc->type = clean($link->getAttribute('type'));
$enc->length = clean($link->getAttribute('length'));
$enc->link = clean($link->getAttribute('href'));
if (!empty($base)) {
$enc->link = UrlHelper::rewrite_relative($base, $enc->link);
}
array_push($encs, $enc);
}
}
}
array_push($encs, ...parent::get_enclosures());
return $encs;
}
function get_language(): string {
$lang = $this->elem->getAttributeNS(self::NS_XML, "lang");
if (!empty($lang)) {
return clean($lang);
} else {
// Fall back to the language declared on the feed, if any.
/** @var DOMElement|DOMNode $child */
foreach ($this->doc->childNodes as $child) {
if (method_exists($child, "getAttributeNS")) {
return clean($child->getAttributeNS(self::NS_XML, "lang"));
}
}
}
return '';
}
}

212
classes/FeedItem_Common.php Normal file
View File

@@ -0,0 +1,212 @@
<?php
abstract class FeedItem_Common extends FeedItem {
protected readonly DOMElement $elem;
protected readonly DOMDocument $doc;
protected readonly DOMXPath $xpath;
function __construct(DOMElement $elem, DOMDocument $doc, DOMXPath $xpath) {
$this->elem = $elem;
$this->doc = $doc;
$this->xpath = $xpath;
try {
$source = $elem->getElementsByTagName("source")->item(0);
// we don't need <source> element
if ($source)
$elem->removeChild($source);
} catch (DOMException $e) {
//
}
}
function get_element(): DOMElement {
return $this->elem;
}
function get_author(): string {
/** @var DOMElement|null */
$author = $this->elem->getElementsByTagName("author")->item(0);
if ($author) {
$name = $author->getElementsByTagName("name")->item(0);
if ($name) return clean($name->nodeValue);
$email = $author->getElementsByTagName("email")->item(0);
if ($email) return clean($email->nodeValue);
if ($author->nodeValue)
return clean($author->nodeValue);
}
$author_elems = $this->xpath->query("dc:creator", $this->elem);
$authors = [];
foreach ($author_elems as $author) {
array_push($authors, clean($author->nodeValue));
}
return implode(", ", $authors);
}
function get_comments_url(): string {
//RSS only. Use a query here to avoid namespace clashes (e.g. with slash).
//might give a wrong result if a default namespace was declared (possible with XPath 2.0)
$com_url = $this->xpath->query("comments", $this->elem)->item(0);
if ($com_url)
return clean($com_url->nodeValue);
//Atom Threading Extension (RFC 4685) stuff. Could be used in RSS feeds, so it's in common.
//'text/html' for type is too restrictive?
$com_url = $this->xpath->query("atom:link[@rel='replies' and contains(@type,'text/html')]/@href", $this->elem)->item(0);
if ($com_url)
return clean($com_url->nodeValue);
return '';
}
function get_comments_count(): int {
//also query for ATE stuff here
$query = "slash:comments|thread:total|atom:link[@rel='replies']/@thread:count";
$comments = $this->xpath->query($query, $this->elem)->item(0);
if ($comments && is_numeric($comments->nodeValue)) {
return (int) clean($comments->nodeValue);
}
return 0;
}
/**
* 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>
*/
function get_enclosures(): array {
$encs = [];
$enclosures = $this->xpath->query("media:content", $this->elem);
/** @var DOMElement $enclosure */
foreach ($enclosures as $enclosure) {
$enc = new FeedEnclosure();
$enc->type = clean($enclosure->getAttribute('type'));
$enc->link = clean($enclosure->getAttribute('url'));
$enc->length = clean($enclosure->getAttribute('length'));
$enc->height = clean($enclosure->getAttribute('height'));
$enc->width = clean($enclosure->getAttribute('width'));
$medium = clean($enclosure->getAttribute("medium"));
if (!$enc->type && $medium) {
$enc->type = strtolower("$medium/generic");
}
$desc = $this->xpath->query("media:description", $enclosure)->item(0);
if ($desc) $enc->title = clean($desc->nodeValue);
array_push($encs, $enc);
}
$enclosures = $this->xpath->query("media:group", $this->elem);
foreach ($enclosures as $enclosure) {
/** @var DOMElement|null */
$content = $this->xpath->query("media:content", $enclosure)->item(0);
if ($content) {
$enc = new FeedEnclosure();
$enc->type = clean($content->getAttribute('type'));
$enc->link = clean($content->getAttribute('url'));
$enc->length = clean($content->getAttribute('length'));
$enc->height = clean($content->getAttribute('height'));
$enc->width = clean($content->getAttribute('width'));
$medium = clean($content->getAttribute("medium"));
if (!$enc->type && $medium) {
$enc->type = strtolower("$medium/generic");
}
$desc = $this->xpath->query("media:description", $content)->item(0);
if ($desc) {
$enc->title = clean($desc->nodeValue);
} else {
$desc = $this->xpath->query("media:description", $enclosure)->item(0);
if ($desc) $enc->title = clean($desc->nodeValue);
}
array_push($encs, $enc);
}
}
$enclosures = $this->xpath->query("(.|media:content|media:group|media:group/media:content)/media:thumbnail", $this->elem);
/** @var DOMElement $enclosure */
foreach ($enclosures as $enclosure) {
$enc = new FeedEnclosure();
$enc->type = 'image/generic';
$enc->link = clean($enclosure->getAttribute('url'));
$enc->height = clean($enclosure->getAttribute('height'));
$enc->width = clean($enclosure->getAttribute('width'));
array_push($encs, $enc);
}
return $encs;
}
function count_children(DOMElement $node): int {
return $node->getElementsByTagName("*")->length;
}
/**
* @return false|string false on failure, otherwise string contents
*/
function subtree_or_text(DOMElement $node): false|string {
if ($this->count_children($node) == 0) {
return $node->nodeValue;
} else {
return $node->c14n();
}
}
/**
* @param array<int, string> $cats
*
* @return array<int, string>
*/
static function normalize_categories(array $cats): array {
$tmp = [];
foreach ($cats as $rawcat) {
array_push($tmp, ...explode(",", $rawcat));
}
$tmp = array_map(function($srccat) {
$cat = clean(trim(mb_strtolower($srccat)));
// we don't support numeric tags
if (is_numeric($cat))
$cat = 't:' . $cat;
$cat = preg_replace('/[,\'\"]/', "", $cat);
if (mb_strlen($cat) > 250)
$cat = mb_substr($cat, 0, 250);
return $cat;
}, $tmp);
// remove empty values
$tmp = array_filter($tmp, 'strlen');
asort($tmp);
return array_unique($tmp);
}
}

169
classes/FeedItem_RSS.php Normal file
View File

@@ -0,0 +1,169 @@
<?php
class FeedItem_RSS extends FeedItem_Common {
function get_id(): string {
$id = $this->elem->getElementsByTagName("guid")->item(0);
if ($id) {
return clean($id->nodeValue);
} else {
return clean($this->get_link());
}
}
/**
* @return int|false a timestamp on success, false otherwise
*/
function get_date(): false|int {
$pubDate = $this->elem->getElementsByTagName("pubDate")->item(0);
if ($pubDate) {
return strtotime($pubDate->nodeValue ?? '');
}
$date = $this->xpath->query("dc:date", $this->elem)->item(0);
if ($date) {
return strtotime($date->nodeValue ?? '');
}
// consistent with strtotime failing to parse
return false;
}
function get_link(): string {
$links = $this->xpath->query("atom:link", $this->elem);
/** @var DOMElement $link */
foreach ($links as $link) {
if ($link->hasAttribute("href") &&
(!$link->hasAttribute("rel")
|| $link->getAttribute("rel") == "alternate"
|| $link->getAttribute("rel") == "standout")) {
return clean(trim($link->getAttribute("href")));
}
}
/** @var DOMElement|null */
$link = $this->elem->getElementsByTagName("guid")->item(0);
if ($link && $link->hasAttributes() && $link->getAttribute("isPermaLink") == "true") {
return clean(trim($link->nodeValue));
}
$link = $this->elem->getElementsByTagName("link")->item(0);
if ($link) {
return clean(trim($link->nodeValue));
}
return '';
}
function get_title(): string {
$title = $this->xpath->query("title", $this->elem)->item(0);
if ($title) {
return clean(trim($title->nodeValue));
}
// if the document has a default namespace then querying for
// title would fail because of reasons so let's try the old way
$title = $this->elem->getElementsByTagName("title")->item(0);
if ($title) {
return clean(trim($title->nodeValue));
}
return '';
}
function get_content(): string {
/** @var DOMElement|null */
$contentA = $this->xpath->query("content:encoded", $this->elem)->item(0);
/** @var DOMElement|null */
$contentB = $this->elem->getElementsByTagName("description")->item(0);
if ($contentA && $contentB) {
$resultA = $this->subtree_or_text($contentA);
$resultB = $this->subtree_or_text($contentB);
return mb_strlen($resultA) > mb_strlen($resultB) ? $resultA : $resultB;
}
if ($contentA) {
return $this->subtree_or_text($contentA);
}
if ($contentB) {
return $this->subtree_or_text($contentB);
}
return '';
}
function get_description(): string {
$summary = $this->elem->getElementsByTagName("description")->item(0);
if ($summary) {
return $summary->nodeValue;
}
return '';
}
/**
* @return array<int, string>
*/
function get_categories(): array {
$categories = $this->elem->getElementsByTagName("category");
$cats = [];
foreach ($categories as $cat) {
array_push($cats, $cat->nodeValue);
}
$categories = $this->xpath->query("dc:subject", $this->elem);
foreach ($categories as $cat) {
array_push($cats, $cat->nodeValue);
}
return $this->normalize_categories($cats);
}
/**
* @return array<int, FeedEnclosure>
*/
function get_enclosures(): array {
$enclosures = $this->elem->getElementsByTagName("enclosure");
$encs = array();
foreach ($enclosures as $enclosure) {
$enc = new FeedEnclosure();
$enc->type = clean($enclosure->getAttribute('type'));
$enc->link = clean($enclosure->getAttribute('url'));
$enc->length = clean($enclosure->getAttribute('length'));
$enc->height = clean($enclosure->getAttribute('height'));
$enc->width = clean($enclosure->getAttribute('width'));
array_push($encs, $enc);
}
array_push($encs, ...parent::get_enclosures());
return $encs;
}
function get_language(): string {
$languages = $this->doc->getElementsByTagName('language');
if (count($languages) == 0) {
return "";
}
return clean($languages[0]->textContent);
}
}

249
classes/FeedParser.php Normal file
View File

@@ -0,0 +1,249 @@
<?php
class FeedParser {
private DOMDocument $doc;
private ?string $error = null;
/** @var array<string> */
private array $libxml_errors = [];
/** @var array<FeedItem> */
private array $items = [];
private ?string $link = null;
private ?string $title = null;
/** @var FeedParser::FEED_* */
private int $type;
private ?DOMXPath $xpath = null;
const FEED_UNKNOWN = -1;
const FEED_RDF = 0;
const FEED_RSS = 1;
const FEED_ATOM = 2;
function __construct(string $data) {
libxml_use_internal_errors(true);
libxml_clear_errors();
$this->type = $this::FEED_UNKNOWN;
$this->doc = new DOMDocument();
$this->doc->loadXML($data);
mb_substitute_character("none");
$error = libxml_get_last_error();
if ($error) {
foreach (libxml_get_errors() as $error) {
if ($error->level == LIBXML_ERR_FATAL) {
// currently only the first error is reported
$this->error ??= Errors::format_libxml_error($error);
$this->libxml_errors[] = Errors::format_libxml_error($error);
}
}
}
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');
}
/**
* @return bool false if initialization couldn't occur (e.g. parsing error or unrecognized feed type), otherwise true
*/
function init(): bool {
if ($this->error)
return false;
$type = $this->get_type();
if ($type === self::FEED_UNKNOWN)
return false;
$xpath = $this->xpath;
switch ($type) {
case $this::FEED_ATOM:
$title = $xpath->query('//atom:feed/atom:title')->item(0)
?? $xpath->query('//atom03:feed/atom03:title')->item(0);
if ($title) {
$this->title = $title->nodeValue;
}
/** @var DOMElement|null $link */
$link = $xpath->query('//atom:feed/atom:link[not(@rel)]')->item(0)
?? $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");
if (empty($articles) || $articles->length == 0)
$articles = $xpath->query("//atom03:entry");
foreach ($articles as $article) {
array_push($this->items, new FeedItem_Atom($article, $this->doc, $this->xpath));
}
break;
case $this::FEED_RSS:
$title = $xpath->query("//channel/title")->item(0);
if ($title) {
$this->title = $title->nodeValue;
}
/** @var DOMElement|null $link */
$link = $xpath->query("//channel/link")->item(0);
if ($link) {
if ($link->getAttribute("href"))
$this->link = $link->getAttribute("href");
else if ($link->nodeValue)
$this->link = $link->nodeValue;
}
$articles = $xpath->query("//channel/item");
foreach ($articles as $article) {
array_push($this->items, new FeedItem_RSS($article, $this->doc, $this->xpath));
}
break;
case $this::FEED_RDF:
$xpath->registerNamespace('rssfake', 'http://purl.org/rss/1.0/');
$title = $xpath->query("//rssfake:channel/rssfake:title")->item(0);
if ($title) {
$this->title = $title->nodeValue;
}
$link = $xpath->query("//rssfake:channel/rssfake:link")->item(0);
if ($link) {
$this->link = $link->nodeValue;
}
$articles = $xpath->query("//rssfake:item");
foreach ($articles as $article) {
array_push($this->items, new FeedItem_RSS($article, $this->doc, $this->xpath));
}
break;
}
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 */
function format_error(LibXMLError $error) : string {
return Errors::format_libxml_error($error);
}
// libxml may have invalid unicode data in error messages
function error() : string {
return UConverter::transcode($this->error ?? '', 'UTF-8', 'UTF-8');
}
/** @return array<string> - WARNING: may return invalid unicode data */
function errors() : array {
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 {
return clean($this->link ?? '');
}
function get_title() : string {
return clean($this->title ?? '');
}
/** @return array<FeedItem> */
function get_items() : array {
return $this->items;
}
/** @return array<string> */
function get_links(string $rel) : array {
$rv = array();
switch ($this->type) {
case $this::FEED_ATOM:
$links = $this->xpath->query("//atom:feed/atom:link");
/** @var DOMElement $link */
foreach ($links as $link) {
if (!$rel || $link->hasAttribute('rel') && $link->getAttribute('rel') == $rel) {
array_push($rv, clean(trim($link->getAttribute('href'))));
}
}
break;
case $this::FEED_RSS:
$links = $this->xpath->query("//atom:link");
/** @var DOMElement $link */
foreach ($links as $link) {
if (!$rel || $link->hasAttribute('rel') && $link->getAttribute('rel') == $rel) {
array_push($rv, clean(trim($link->getAttribute('href'))));
}
}
break;
}
return $rv;
}
}

2400
classes/Feeds.php Normal file

File diff suppressed because it is too large Load Diff

35
classes/Handler.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
class Handler implements IHandler {
protected PDO $pdo;
/** @var array<int|string, mixed> */
protected array $args;
/**
* @param array<int|string, mixed> $args
*/
function __construct(array $args) {
$this->pdo = Db::pdo();
$this->args = $args;
}
function csrf_ignore(string $method): bool {
return false;
}
function before(string $method): bool {
return true;
}
function after(): bool {
return true;
}
/**
* @param mixed $p
*/
public static function _param_to_bool($p): bool {
$p = clean($p);
return $p && ($p !== "f" && $p !== "false");
}
}

View File

@@ -0,0 +1,11 @@
<?php
class Handler_Administrative extends Handler_Protected {
function before(string $method): bool {
if (parent::before($method)) {
if (($_SESSION["access_level"] ?? 0) >= UserHelper::ACCESS_LEVEL_ADMIN) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,7 @@
<?php
class Handler_Protected extends Handler {
function before(string $method): bool {
return parent::before($method) && !empty($_SESSION['uid']);
}
}

861
classes/Handler_Public.php Normal file
View File

@@ -0,0 +1,861 @@
<?php
class Handler_Public extends Handler {
/**
* @param string $feed may be a feed ID or tag
*/
private function generate_syndicated_feed(int $owner_uid, string $feed, bool $is_cat,
int $limit, int $offset, string $search, string $view_mode = "",
string $format = 'atom', string $order = "", string $orig_guid = "", string $start_ts = ""): void {
// fail early if the requested format isn't recognized
if (!in_array($format, ['atom', 'json'])) {
header('Content-Type: text/plain; charset=utf-8');
print "Unknown format: $format.";
return;
}
$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);
if (!$override_order) {
$override_order = match (true) {
$feed == Feeds::FEED_PUBLISHED && !$is_cat => 'last_published DESC',
$feed == Feeds::FEED_STARRED && !$is_cat => 'last_marked DESC',
default => 'date_entered DESC, updated DESC',
};
}
$params = array(
"owner_uid" => $owner_uid,
"feed" => $feed,
"limit" => $limit,
"view_mode" => $view_mode,
"cat_view" => $is_cat,
"search" => $search,
"override_order" => $override_order,
"include_children" => true,
"ignore_vfeed_group" => true,
"offset" => $offset,
"start_ts" => $start_ts
);
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
// 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->load(Config::get(Config::PLUGINS), PluginHost::KIND_ALL);
$tmppluginhost->load((string)$user_plugins, PluginHost::KIND_USER, $owner_uid);
//$tmppluginhost->load_data();
$handler = $tmppluginhost->get_feed_handler(
PluginHost::feed_to_pfeed_id((int)$feed));
if ($handler) {
$qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id((int)$feed), $params);
} else {
user_error("Failed to find handler for plugin feed ID: $feed", E_USER_ERROR);
return;
}
} else {
$qfh_ret = Feeds::_get_headlines($params);
}
$result = $qfh_ret[0];
$feed_title = htmlspecialchars($qfh_ret[1]);
$feed_site_url = $qfh_ret[2];
/* $last_error = $qfh_ret[3]; */
$feed_self_url = Config::get_self_url() .
"/public.php?op=rss&id=$feed&key=" .
Feeds::_get_access_key($feed, false, $owner_uid);
if (!$feed_site_url)
$feed_site_url = Config::get_self_url();
if ($format == 'atom') {
$tpl = new Templator();
$tpl->readTemplateFromFile("generated_feed.txt");
$tpl->setVariable('FEED_TITLE', $feed_title, true);
$tpl->setVariable('FEED_UPDATED', date('c'), true);
$tpl->setVariable('VERSION', Config::get_version(), true);
$tpl->setVariable('FEED_URL', htmlspecialchars($feed_self_url), true);
$tpl->setVariable('SELF_URL', htmlspecialchars(Config::get_self_url()), true);
while ($line = $result->fetch()) {
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
$line["tags"] = Article::_get_tags($line["id"], $owner_uid);
$max_excerpt_length = 250;
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
function ($result) use (&$line) {
$line = $result;
},
$line, $max_excerpt_length);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_EXPORT_FEED,
function ($result) use (&$line) {
$line = $result;
},
$line, $feed, $is_cat, $owner_uid);
$tpl->setVariable('ARTICLE_ID',
htmlspecialchars($orig_guid ? $line['link'] :
$this->_make_article_tag_uri($line['id'], $line['date_entered'])), true);
$tpl->setVariable('ARTICLE_LINK', htmlspecialchars($line['link']), true);
$tpl->setVariable('ARTICLE_TITLE', htmlspecialchars($line['title']), true);
$tpl->setVariable('ARTICLE_EXCERPT', $line["content_preview"], true);
$content = Sanitizer::sanitize($line["content"], false, $owner_uid,
$feed_site_url, null, $line["id"]);
$content = DiskCache::rewrite_urls($content);
if ($line['note']) {
$content = "<div style=\"$note_style\">Article note: " . $line['note'] . "</div>" . $content;
$tpl->setVariable('ARTICLE_NOTE', htmlspecialchars($line['note']), true);
}
$tpl->setVariable('ARTICLE_CONTENT', $content, true);
$tpl->setVariable('ARTICLE_UPDATED_ATOM',
date('c', strtotime($line["updated"] ?? '')), true);
$tpl->setVariable('ARTICLE_UPDATED_RFC822',
date(DATE_RFC822, strtotime($line["updated"] ?? '')), true);
$tpl->setVariable('ARTICLE_AUTHOR', htmlspecialchars($line['author']), true);
$tpl->setVariable('ARTICLE_SOURCE_LINK', htmlspecialchars($line['site_url'] ? $line["site_url"] : Config::get_self_url()), true);
$tpl->setVariable('ARTICLE_SOURCE_TITLE', htmlspecialchars($line['feed_title'] ?? $feed_title), true);
foreach ($line["tags"] as $tag) {
$tpl->setVariable('ARTICLE_CATEGORY', htmlspecialchars($tag), true);
$tpl->addBlock('category');
}
$enclosures = Article::_get_enclosures($line["id"]);
if (count($enclosures) > 0) {
foreach ($enclosures as $e) {
$type = htmlspecialchars($e['content_type']);
$url = htmlspecialchars($e['content_url']);
$length = $e['duration'] ?: 1;
$tpl->setVariable('ARTICLE_ENCLOSURE_URL', $url, true);
$tpl->setVariable('ARTICLE_ENCLOSURE_TYPE', $type, true);
$tpl->setVariable('ARTICLE_ENCLOSURE_LENGTH', $length, true);
$tpl->addBlock('enclosure');
}
} else {
$tpl->setVariable('ARTICLE_ENCLOSURE_URL', "", true);
$tpl->setVariable('ARTICLE_ENCLOSURE_TYPE', "", true);
$tpl->setVariable('ARTICLE_ENCLOSURE_LENGTH', "", true);
}
list ($og_image, $og_stream) = Article::_get_image($enclosures, $line['content'], $feed_site_url, $line);
$tpl->setVariable('ARTICLE_OG_IMAGE', $og_image, true);
$tpl->addBlock('entry');
}
$tmp = "";
$tpl->addBlock('feed');
$tpl->generateOutputToString($tmp);
if (empty($_REQUEST["noxml"])) {
header("Content-Type: text/xml; charset=utf-8");
} else {
header("Content-Type: text/plain; charset=utf-8");
}
print $tmp;
} else { // $format == 'json'
$feed = [
'title' => $feed_title,
'feed_url' => $feed_self_url,
'self_url' => Config::get_self_url(),
'articles' => [],
];
while ($line = $result->fetch()) {
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
$line["tags"] = Article::_get_tags($line["id"], $owner_uid);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
function ($result) use (&$line) {
$line = $result;
},
$line);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_EXPORT_FEED,
function ($result) use (&$line) {
$line = $result;
},
$line, $feed, $is_cat, $owner_uid);
$article = [
'id' => $line['link'],
'link' => $line['link'],
'title' => $line['title'],
'content' => Sanitizer::sanitize($line['content'], false, $owner_uid, $feed_site_url, null, $line['id']),
'updated' => date('c', strtotime($line['updated'] ?? '')),
'source' => [
'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'];
if (count($line['tags']) > 0)
$article['tags'] = $line['tags'];
$enclosures = Article::_get_enclosures($line["id"]);
if (count($enclosures) > 0) {
$article['enclosures'] = array();
foreach ($enclosures as $e) {
$article['enclosures'][] = [
'url' => $e['content_url'],
'type' => $e['content_type'],
'length' => $e['duration'],
];
}
}
array_push($feed['articles'], $article);
}
header("Content-Type: application/json; charset=utf-8");
print json_encode($feed);
}
}
function getUnread(): void {
$login = clean($_REQUEST["login"]);
$fresh = clean($_REQUEST["fresh"] ?? "0") == "1";
$uid = UserHelper::find_user_by_login($login);
if ($uid) {
print Feeds::_get_global_unread($uid);
if ($fresh) {
print ";";
print Feeds::_get_counters(Feeds::FEED_FRESH, false, true, $uid);
}
} else {
print "-1;User not found";
}
}
function getProfiles(): void {
$login = clean($_REQUEST["login"]);
$rv = [];
if ($login) {
$profiles = ORM::for_table('ttrss_settings_profiles')
->table_alias('p')
->select_many('title' , 'p.id')
->join('ttrss_users', ['owner_uid', '=', 'u.id'], 'u')
->where_raw('LOWER(login) = LOWER(?)', [$login])
->order_by_asc('title')
->find_many();
$rv = [ [ "value" => 0, "label" => __("Default profile") ] ];
foreach ($profiles as $profile) {
array_push($rv, [ "label" => $profile->title, "value" => $profile->id ]);
}
}
print json_encode($rv);
}
function logout(): void {
if (validate_csrf($_POST["csrf_token"])) {
$login = $_SESSION["name"];
$user_id = $_SESSION["uid"];
UserHelper::logout();
$redirect_url = "";
PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_POST_LOGOUT,
function ($result) use (&$redirect_url) {
if (!empty($result[0]))
$redirect_url = UrlHelper::validate($result[0]);
},
$login, $user_id);
if (!$redirect_url)
$redirect_url = Config::get_self_url() . "/index.php";
header("Location: " . $redirect_url);
} else {
header("Content-Type: application/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
}
}
function rss(): void {
$feed = clean($_REQUEST["id"]);
$key = clean($_REQUEST["key"]);
$is_cat = self::_param_to_bool($_REQUEST["is_cat"] ?? false);
$limit = (int)clean($_REQUEST["limit"] ?? 0);
$offset = (int)clean($_REQUEST["offset"] ?? 0);
$search = clean($_REQUEST["q"] ?? "");
$view_mode = clean($_REQUEST["view-mode"] ?? "");
$order = clean($_REQUEST["order"] ?? "");
$start_ts = clean($_REQUEST["ts"] ?? "");
$format = clean($_REQUEST['format'] ?? "atom");
$orig_guid = clean($_REQUEST["orig_guid"] ?? "");
if (Config::get(Config::SINGLE_USER_MODE)) {
UserHelper::authenticate("admin", null);
}
if ($key) {
$access_key = ORM::for_table('ttrss_access_keys')
->select('owner_uid')
->where(['access_key' => $key, 'feed_id' => $feed])
->find_one();
if ($access_key) {
$this->generate_syndicated_feed($access_key->owner_uid, $feed, $is_cat, $limit,
$offset, $search, $view_mode, $format, $order, $orig_guid, $start_ts);
return;
}
}
header('HTTP/1.1 403 Forbidden');
}
function login(): void {
if (!Config::get(Config::SINGLE_USER_MODE)) {
$login = clean($_POST["login"]);
$password = clean($_POST["password"]);
$remember_me = clean($_POST["remember_me"] ?? false);
$safe_mode = checkbox_to_sql_bool($_POST["safe_mode"] ?? false);
if (session_status() != PHP_SESSION_ACTIVE) {
if ($remember_me) {
session_set_cookie_params(Config::get(Config::SESSION_COOKIE_LIFETIME));
} else {
session_set_cookie_params(0);
}
}
if (UserHelper::authenticate($login, $password)) {
$_POST["password"] = "";
$_SESSION["ref_schema_version"] = Config::get_schema_version();
$_SESSION["bw_limit"] = !!clean($_POST["bw_limit"] ?? false);
$_SESSION["safe_mode"] = $safe_mode;
if (!empty($_POST["profile"])) {
$profile = (int) clean($_POST["profile"]);
$profile_obj = ORM::for_table('ttrss_settings_profiles')
->where(['id' => $profile, 'owner_uid' => $_SESSION['uid']])
->find_one();
$_SESSION["profile"] = $profile_obj ? $profile : null;
}
} else {
// start an empty session to deliver login error message
if (session_status() != PHP_SESSION_ACTIVE)
session_start();
$_SESSION["login_error_msg"] ??= __("Incorrect username or password");
}
$return = clean($_REQUEST['return'] ?? '');
if ($return && mb_strpos($return, Config::get_self_url()) === 0) {
header("Location: $return");
} else {
header("Location: " . Config::get_self_url());
}
}
}
function index(): void {
header("Content-Type: text/plain");
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
}
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();
session_start();
$hash = clean($_REQUEST["hash"] ?? '');
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html>
<head>
<title>Tiny Tiny RSS</title>
<link rel="shortcut icon" type="image/png" href="images/favicon.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"/>
<?php
echo javascript_tag("lib/dojo/dojo.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() ?>
</head>
<body class='flat ttrss_utility css_loading'>
<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">
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){
ready(function() {
parser.parse();
});
});
</script>
<style type="text/css">
@media (prefers-color-scheme: dark) {
body {
background : #303030;
}
}
body.css_loading * {
display : none;
}
</style>
<?php
print "<h1>".__("Password recovery")."</h1>";
print "<div class='content'>";
$method = clean($_POST['method'] ?? '');
if ($hash) {
$login = clean($_REQUEST["login"]);
if ($login) {
$user = ORM::for_table('ttrss_users')
->select_many('id', 'resetpass_token')
->where_raw('LOWER(login) = LOWER(?)', [$login])
->find_one();
if ($user) {
list($timestamp, $resetpass_token) = explode(":", $user->resetpass_token);
if ($timestamp && $resetpass_token &&
$timestamp >= time() - 15*60*60 &&
$resetpass_token === $hash) {
$user->resetpass_token = null;
$user->save();
UserHelper::reset_password($user->id, true);
print "<p>"."Completed."."</p>";
} else {
print_error("Some of the information provided is missing or incorrect.");
}
} else {
print_error("Some of the information provided is missing or incorrect.");
}
} else {
print_error("Some of the information provided is missing or incorrect.");
}
print "<a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>";
} else if (!$method) {
print_notice(__("You will need to provide valid account name and email. Password reset link will be sent to your email address."));
print "<form method='POST' action='public.php'>
<input type='hidden' name='method' value='do'>
<input type='hidden' name='op' value='forgotpass'>
<fieldset>
<label>".__("Login:")."</label>
<input dojoType='dijit.form.TextBox' type='text' name='login' value='' required>
</fieldset>
<fieldset>
<label>".__("Email:")."</label>
<input dojoType='dijit.form.TextBox' type='email' name='email' value='' required>
</fieldset>";
$_SESSION["pwdreset:testvalue1"] = rand(1,10);
$_SESSION["pwdreset:testvalue2"] = rand(1,10);
print "<fieldset>
<label>".T_sprintf("How much is %d + %d:", $_SESSION["pwdreset:testvalue1"], $_SESSION["pwdreset:testvalue2"])."</label>
<input dojoType='dijit.form.TextBox' type='text' name='test' value='' required>
</fieldset>
<hr/>
<fieldset>
<button dojoType='dijit.form.Button' type='submit' class='alt-danger'>".__("Reset password")."</button>
<a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>
</fieldset>
</form>";
} else if ($method == 'do') {
$login = clean($_POST["login"]);
$email = clean($_POST["email"]);
$test = clean($_POST["test"]);
if ($test != ($_SESSION["pwdreset:testvalue1"] + $_SESSION["pwdreset:testvalue2"]) || !$email || !$login) {
print_error(__('Some of the required form parameters are missing or incorrect.'));
print "<form method='GET' action='public.php'>
<input type='hidden' name='op' value='forgotpass'>
<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>".__("Go back")."</button>
</form>";
} else {
// prevent submitting this form multiple times
$_SESSION["pwdreset:testvalue1"] = rand(1, 1000);
$_SESSION["pwdreset:testvalue2"] = rand(1, 1000);
$user = ORM::for_table('ttrss_users')
->select('id')
->where_raw('LOWER(login) = LOWER(?)', [$login])
->where('email', $email)
->find_one();
if ($user) {
print_notice("Password reset instructions are being sent to your email address.");
$resetpass_token = sha1(get_random_bytes(128));
$resetpass_link = Config::get_self_url() . "/public.php?op=forgotpass&hash=" . $resetpass_token .
"&login=" . urlencode($login);
$tpl = new Templator();
$tpl->readTemplateFromFile("resetpass_link_template.txt");
$tpl->setVariable('LOGIN', $login);
$tpl->setVariable('RESETPASS_LINK', $resetpass_link);
$tpl->setVariable('TTRSS_HOST', Config::get_self_url());
$tpl->addBlock('message');
$message = "";
$tpl->generateOutputToString($message);
$mailer = new Mailer();
$rc = $mailer->mail(["to_name" => $login,
"to_address" => $email,
"subject" => __("[tt-rss] Password reset request"),
"message" => $message]);
if (!$rc) print_error($mailer->error());
$user->resetpass_token = time() . ":" . $resetpass_token;
$user->save();
print "<a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>";
} else {
print_error(__("Sorry, login and email combination not found."));
print "<form method='GET' action='public.php'>
<input type='hidden' name='op' value='forgotpass'>
<button dojoType='dijit.form.Button' type='submit'>".__("Go back")."</button>
</form>";
}
}
}
print "</div>";
print "</div>";
print "</body>";
print "</html>";
}
function dbupdate(): void {
startup_gettext();
if (!Config::get(Config::SINGLE_USER_MODE) && ($_SESSION["access_level"] ?? 0) < 10) {
$_SESSION["login_error_msg"] = __("Your access level is insufficient to run this script.");
$this->_render_login_form();
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Tiny Tiny RSS: Database Updater</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="icon" type="image/png" sizes="72x72" href="images/favicon-72px.png">
<link rel="shortcut icon" type="image/png" href="images/favicon.png">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<?php
foreach (["lib/dojo/dojo.js",
"lib/dojo/tt-rss-layer.js",
"js/common.js",
"js/utility.js"] as $jsfile) {
echo javascript_tag($jsfile);
} ?>
<?= Config::get_override_links() ?>
<style type="text/css">
@media (prefers-color-scheme: dark) {
body {
background : #303030;
}
}
body.css_loading * {
display : none;
}
</style>
<script type="text/javascript">
require({cache:{}});
</script>
</head>
<body class="flat ttrss_utility css_loading">
<script type="text/javascript">
const UtilityApp = {
init: function() {
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){
ready(function() {
parser.parse();
});
});
}
}
function confirmDbUpdate() {
return confirm(__("Proceed with update?"));
}
</script>
<div class="container">
<h1><?= __("Database Updater") ?></h1>
<div class="content">
<?php
@$op = clean($_REQUEST["subop"] ?? "");
$migrations = Config::get_migrations();
if ($op == "performupdate") {
if ($migrations->is_migration_needed()) {
?>
<h2><?= T_sprintf("Performing updates to version %d", Config::SCHEMA_VERSION) ?></h2>
<code><pre class="small pre-wrap"><?php
Debug::set_enabled(true);
Debug::set_loglevel(Debug::LOG_VERBOSE);
$result = $migrations->migrate();
Debug::set_loglevel(Debug::LOG_NORMAL);
Debug::set_enabled(false);
?></pre></code>
<?php if (!$result) { ?>
<?= format_error("One of migrations failed. Either retry the process or perform updates manually.") ?>
<form method="post">
<?= \Controls\hidden_tag('subop', 'performupdate') ?>
<?= \Controls\submit_tag(__("Update"), ["onclick" => "return confirmDbUpdate()"]) ?>
</form>
<?php } else { ?>
<?= format_notice("Update successful.") ?>
<a href="index.php"><?= __("Return to Tiny Tiny RSS") ?></a>
<?php }
} else { ?>
<?= format_notice("Database is already up to date.") ?>
<a href="index.php"><?= __("Return to Tiny Tiny RSS") ?></a>
<?php
}
} else {
if ($migrations->is_migration_needed()) {
?>
<h2><?= T_sprintf("Database schema needs update to the latest version (%d to %d).",
Config::get_schema_version(), Config::SCHEMA_VERSION) ?></h2>
<?= format_warning("Please backup your database before proceeding.") ?>
<form method="post">
<?= \Controls\hidden_tag('subop', 'performupdate') ?>
<?= \Controls\submit_tag(__("Update"), ["onclick" => "return confirmDbUpdate()"]) ?>
</form>
<?php
} else { ?>
<?= format_notice("Database is already up to date.") ?>
<a href="index.php"><?= __("Return to Tiny Tiny RSS") ?></a>
<?php
}
}
?>
</div>
</div>
</body>
</html>
<?php
}
function cached(): void {
list ($cache_dir, $filename) = explode("/", $_GET["file"], 2);
// we do not allow files with extensions at the moment
$filename = str_replace(".", "", $filename);
$cache = DiskCache::instance($cache_dir);
if ($cache->exists($filename)) {
$size = $cache->get_size($filename);
if ($size && $size > 0)
header("Content-Length: $size");
$cache->send($filename);
} else {
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
echo "File not found.";
}
}
function feed_icon() : void {
$id = (int)$_REQUEST['id'];
$cache = DiskCache::instance('feed-icons');
if ($cache->exists((string)$id)) {
$cache->send((string)$id);
} else {
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
echo "File not found.";
}
}
private function _make_article_tag_uri(int $id, string $timestamp): string {
$timestamp = date("Y-m-d", strtotime($timestamp));
return "tag:" . parse_url(Config::get_self_url(), PHP_URL_HOST) . ",$timestamp:/$id";
}
// this should be used very carefully because this endpoint is exposed to unauthenticated users
// plugin data is not loaded because there's no user context and owner_uid/session may or may not be available
// in general, don't do anything user-related in here and do not modify $_SESSION
public function pluginhandler(): void {
$host = new PluginHost();
$plugin_name = basename(clean($_REQUEST["plugin"]));
$method = clean($_REQUEST["pmethod"]);
$host->load($plugin_name, PluginHost::KIND_ALL, 0);
//$host->load_data();
$plugin = $host->get_plugin($plugin_name);
if ($plugin) {
if (method_exists($plugin, $method)) {
if ($plugin->is_public_method($method)) {
$plugin->$method();
} else {
user_error("PluginHandler[PUBLIC]: Requested private method '$method' of plugin '$plugin_name'.", E_USER_WARNING);
header("Content-Type: application/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
}
} else {
user_error("PluginHandler[PUBLIC]: Requested unknown method '$method' of plugin '$plugin_name'.", E_USER_WARNING);
header("Content-Type: application/json");
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
}
} else {
user_error("PluginHandler[PUBLIC]: Requested method '$method' of unknown plugin '$plugin_name'.", E_USER_WARNING);
header("Content-Type: application/json");
print Errors::to_json(Errors::E_UNKNOWN_PLUGIN, ['plugin' => $plugin_name]);
}
}
static function _render_login_form(string $return_to = ""): void {
header('Cache-Control: public');
if ($return_to)
$_REQUEST['return'] = $return_to;
require_once "login_form.php";
exit;
}
// implicit Config::sanity_check() does the actual checking */
public function healthcheck() : void {
header("Content-Type: text/plain");
print "OK";
}
}
?>

18
classes/IAuthModule.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
interface IAuthModule {
/**
* @param string $login
* @param string $password
* @param string $service
* @return int|false user_id
*/
function authenticate($login, $password, $service = '');
/** this is a pluginhost compatibility wrapper that invokes $this->authenticate(...$args) (Auth_Base)
* @param string $login
* @param string $password
* @param string $service
* @return int|false user_id
*/
function hook_auth_user($login, $password, $service = '');
}

4
classes/IAuthModule2.php Normal file
View File

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

4
classes/ICatchall.php Normal file
View File

@@ -0,0 +1,4 @@
<?php
interface ICatchall {
function catchall(string $method): void;
}

6
classes/IHandler.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
interface IHandler {
function csrf_ignore(string $method): bool;
function before(string $method): bool;
function after(): bool;
}

11
classes/IVirtualFeed.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
interface IVirtualFeed {
function get_unread(int $feed_id) : int;
function get_total(int $feed_id) : int;
/**
* @param int $feed_id
* @param array<string,int|string|bool> $options
* @return array<int,int|string>
*/
function get_headlines(int $feed_id, array $options) : array;
}

233
classes/Labels.php Normal file
View File

@@ -0,0 +1,233 @@
<?php
class Labels
{
static function label_to_feed_id(int $label): int {
return LABEL_BASE_INDEX - 1 - abs($label);
}
static function feed_to_label_id(int $feed): int {
return LABEL_BASE_INDEX - 1 + abs($feed);
}
static function find_id(string $label, int $owner_uid): int {
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT id FROM ttrss_labels2 WHERE LOWER(caption) = LOWER(?)
AND owner_uid = ? LIMIT 1");
$sth->execute([$label, $owner_uid]);
if ($row = $sth->fetch()) {
return $row['id'];
} else {
return 0;
}
}
static function find_caption(int $label, int $owner_uid): string {
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT caption FROM ttrss_labels2 WHERE id = ?
AND owner_uid = ? LIMIT 1");
$sth->execute([$label, $owner_uid]);
if ($row = $sth->fetch()) {
return $row['caption'];
} else {
return "";
}
}
/**
* @return array<int, array<string, string>>
*/
static function get_as_hash(int $owner_uid): array {
$rv = [];
$labels = Labels::get_all($owner_uid);
foreach ($labels as $i => $label) {
$rv[(int)$label["id"]] = $labels[$i];
}
return $rv;
}
/**
* @return array<int, array<string, string>> An array of label detail arrays
*/
static function get_all(int $owner_uid) {
$rv = array();
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT id, fg_color, bg_color, caption FROM ttrss_labels2
WHERE owner_uid = ? ORDER BY caption");
$sth->execute([$owner_uid]);
while ($line = $sth->fetch(PDO::FETCH_ASSOC)) {
array_push($rv, $line);
}
return $rv;
}
/**
* @param array{'no-labels': 1}|array<int, array<int, array{0: int, 1: string, 2: string, 3: string}>> $labels
* [label_id, caption, fg_color, bg_color]
*
* @see Article::_get_labels()
*/
static function update_cache(int $owner_uid, int $id, array $labels, bool $force = false): void {
$pdo = Db::pdo();
if ($force)
self::clear_cache($id);
if (!$labels)
$labels = Article::_get_labels($id);
$labels = json_encode($labels);
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
label_cache = ? WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$labels, $id, $owner_uid]);
}
static function clear_cache(int $id): void {
$pdo = Db::pdo();
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
label_cache = '' WHERE ref_id = ?");
$sth->execute([$id]);
}
static function remove_article(int $id, string $label, int $owner_uid): void {
$label_id = self::find_id($label, $owner_uid);
if (!$label_id) return;
$pdo = Db::pdo();
$sth = $pdo->prepare("DELETE FROM ttrss_user_labels2
WHERE
label_id = ? AND
article_id = ?");
$sth->execute([$label_id, $id]);
self::clear_cache($id);
}
static function add_article(int $id, string $label, int $owner_uid): void {
$label_id = self::find_id($label, $owner_uid);
if (!$label_id) return;
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT
article_id FROM ttrss_labels2, ttrss_user_labels2
WHERE
label_id = id AND
label_id = ? AND
article_id = ? AND owner_uid = ?
LIMIT 1");
$sth->execute([$label_id, $id, $owner_uid]);
if (!$sth->fetch()) {
$sth = $pdo->prepare("INSERT INTO ttrss_user_labels2
(label_id, article_id) VALUES (?, ?)");
$sth->execute([$label_id, $id]);
}
self::clear_cache($id);
}
static function remove(int $id, int $owner_uid): void {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
$tr_in_progress = false;
try {
$pdo->beginTransaction();
} catch (Exception $e) {
$tr_in_progress = true;
}
$sth = $pdo->prepare("SELECT caption FROM ttrss_labels2
WHERE id = ?");
$sth->execute([$id]);
$row = $sth->fetch();
$caption = $row['caption'];
$sth = $pdo->prepare("DELETE FROM ttrss_labels2 WHERE id = ?
AND owner_uid = ?");
$sth->execute([$id, $owner_uid]);
if ($sth->rowCount() != 0 && $caption) {
/* Remove access key for the label */
$ext_id = LABEL_BASE_INDEX - 1 - $id;
$sth = $pdo->prepare("DELETE FROM ttrss_access_keys WHERE
feed_id = ? AND owner_uid = ?");
$sth->execute([$ext_id, $owner_uid]);
/* Remove cached data */
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET label_cache = ''
WHERE owner_uid = ?");
$sth->execute([$owner_uid]);
}
if (!$tr_in_progress) $pdo->commit();
}
/**
* @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): false|int {
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
$pdo = Db::pdo();
$tr_in_progress = false;
try {
$pdo->beginTransaction();
} catch (Exception $e) {
$tr_in_progress = true;
}
$sth = $pdo->prepare("SELECT id FROM ttrss_labels2
WHERE LOWER(caption) = LOWER(?) AND owner_uid = ?");
$sth->execute([$caption, $owner_uid]);
if (!$sth->fetch()) {
$sth = $pdo->prepare("INSERT INTO ttrss_labels2
(caption,owner_uid,fg_color,bg_color) VALUES (?, ?, ?, ?)");
$sth->execute([$caption, $owner_uid, $fg_color, $bg_color]);
$result = $sth->rowCount();
} else {
$result = false;
}
if (!$tr_in_progress) $pdo->commit();
return $result;
}
}

80
classes/Logger.php Normal file
View File

@@ -0,0 +1,80 @@
<?php
class Logger {
private static ?Logger $instance = null;
private ?Logger_Adapter $adapter = null;
const LOG_DEST_SQL = "sql";
const LOG_DEST_STDOUT = "stdout";
const LOG_DEST_SYSLOG = "syslog";
const ERROR_NAMES = [
1 => 'E_ERROR',
2 => 'E_WARNING',
4 => 'E_PARSE',
8 => 'E_NOTICE',
16 => 'E_CORE_ERROR',
32 => 'E_CORE_WARNING',
64 => 'E_COMPILE_ERROR',
128 => 'E_COMPILE_WARNING',
256 => 'E_USER_ERROR',
512 => 'E_USER_WARNING',
1024 => 'E_USER_NOTICE',
2048 => 'E_STRICT',
4096 => 'E_RECOVERABLE_ERROR',
8192 => 'E_DEPRECATED',
16384 => 'E_USER_DEPRECATED',
32767 => 'E_ALL'];
static function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
return self::get_instance()->_log_error($errno, $errstr, $file, $line, $context);
}
private function _log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
//if ($errno == E_NOTICE) return false;
if ($this->adapter)
return $this->adapter->log_error($errno, $errstr, $file, $line, $context);
else
return false;
}
static function log(int $errno, string $errstr, string $context = ""): bool {
return self::get_instance()->_log($errno, $errstr, $context);
}
private function _log(int $errno, string $errstr, string $context = ""): bool {
if ($this->adapter)
return $this->adapter->log_error($errno, $errstr, '', 0, $context);
else
return user_error($errstr, $errno);
}
private function __clone() {
//
}
function __construct() {
$this->adapter = match (Config::get(Config::LOG_DESTINATION)) {
self::LOG_DEST_SQL => new Logger_SQL(),
self::LOG_DEST_SYSLOG => new Logger_Syslog(),
self::LOG_DEST_STDOUT => new Logger_Stdout(),
default => null,
};
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);
}
private static function get_instance() : Logger {
if (self::$instance == null)
self::$instance = new self();
return self::$instance;
}
static function get() : Logger {
user_error("Please don't use Logger::get(), call Logger::log(...) instead.", E_USER_DEPRECATED);
return self::get_instance();
}
}

View File

@@ -0,0 +1,4 @@
<?php
interface Logger_Adapter {
function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool;
}

61
classes/Logger_SQL.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
class Logger_SQL implements Logger_Adapter {
function __construct() {
$conn = get_class($this);
ORM::configure(Db::get_dsn(), null, $conn);
ORM::configure('username', Config::get(Config::DB_USER), $conn);
ORM::configure('password', Config::get(Config::DB_PASS), $conn);
ORM::configure('return_result_sets', true, $conn);
}
function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
if (Config::get_schema_version() > 117) {
// limit context length, DOMDocument dumps entire XML in here sometimes, which may be huge
$context = mb_substr($context, 0, 8192);
$server_params = [
"Real IP" => "HTTP_X_REAL_IP",
"Forwarded For" => "HTTP_X_FORWARDED_FOR",
"Forwarded Protocol" => "HTTP_X_FORWARDED_PROTO",
"Remote IP" => "REMOTE_ADDR",
"Request URI" => "REQUEST_URI",
"User agent" => "HTTP_USER_AGENT",
];
foreach ($server_params as $n => $p) {
if (isset($_SERVER[$p]))
$context .= "\n$n: " . $_SERVER[$p];
}
// passed error message may contain invalid unicode characters, failing to insert an error here
// would break the execution entirely by generating an actual fatal error instead of a E_WARNING etc
$errstr = UConverter::transcode($errstr, 'UTF-8', 'UTF-8');
$context = UConverter::transcode($context, 'UTF-8', 'UTF-8');
// can't use $_SESSION["uid"] ?? null because what if its, for example, false? or zero?
// this would cause a PDOException on insert below
$owner_uid = !empty($_SESSION["uid"]) ? $_SESSION["uid"] : null;
$entry = ORM::for_table('ttrss_error_log', get_class($this))->create();
$entry->set([
'errno' => $errno,
'errstr' => $errstr,
'filename' => $file,
'lineno' => (int)$line,
'context' => $context,
'owner_uid' => $owner_uid,
'created_at' => Db::NOW(),
]);
return $entry->save();
}
return false;
}
}

31
classes/Logger_Stdout.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
class Logger_Stdout implements Logger_Adapter {
function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
switch ($errno) {
case E_ERROR:
case E_PARSE:
case E_CORE_ERROR:
case E_COMPILE_ERROR:
case E_USER_ERROR:
$priority = LOG_ERR;
break;
case E_WARNING:
case E_CORE_WARNING:
case E_COMPILE_WARNING:
case E_USER_WARNING:
$priority = LOG_WARNING;
break;
default:
$priority = LOG_INFO;
}
$errname = Logger::ERROR_NAMES[$errno] . " ($errno)";
print "[EEE] $priority $errname ($file:$line) $errstr\n";
return true;
}
}

31
classes/Logger_Syslog.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
class Logger_Syslog implements Logger_Adapter {
function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
switch ($errno) {
case E_ERROR:
case E_PARSE:
case E_CORE_ERROR:
case E_COMPILE_ERROR:
case E_USER_ERROR:
$priority = LOG_ERR;
break;
case E_WARNING:
case E_CORE_WARNING:
case E_COMPILE_WARNING:
case E_USER_WARNING:
$priority = LOG_WARNING;
break;
default:
$priority = LOG_INFO;
}
$errname = Logger::ERROR_NAMES[$errno] . " ($errno)";
syslog($priority, "[tt-rss] $errname ($file:$line) $errstr");
return true;
}
}

65
classes/Mailer.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
class Mailer {
private string $last_error = "";
/**
* @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()
*/
function mail(array $params): bool|int {
$to_name = $params["to_name"] ?? "";
$to_address = $params["to_address"];
$subject = $params["subject"];
$message = $params["message"];
// $message_html = $params["message_html"] ?? "";
$from_name = $params["from_name"] ?? Config::get(Config::SMTP_FROM_NAME);
$from_address = $params["from_address"] ?? Config::get(Config::SMTP_FROM_ADDRESS);
$additional_headers = $params["headers"] ?? [];
$from_combined = $from_name ? "$from_name <$from_address>" : $from_address;
$to_combined = $to_name ? "$to_name <$to_address>" : $to_address;
if (Config::get(Config::LOG_SENT_MAIL))
Logger::log(E_USER_NOTICE, "Sending mail from $from_combined to $to_combined [$subject]: $message");
// HOOK_SEND_MAIL plugin instructions:
// 1. return 1 or true if mail is handled
// 2. return -1 if there's been a fatal error and no further action is allowed
// 3. any other return value will allow cycling to the next handler and, eventually, to default mail() function
// 4. set error message if needed via passed Mailer instance function set_error()
$hooks_tried = 0;
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEND_MAIL) as $p) {
$rc = $p->hook_send_mail($this, $params);
if ($rc == 1)
return $rc;
if ($rc == -1)
return 0;
++$hooks_tried;
}
$headers = [ "From: $from_combined", "Content-Type: text/plain; charset=UTF-8" ];
$rc = mail($to_combined, $subject, $message, implode("\r\n", [...$headers, ...$additional_headers]));
if (!$rc) {
$this->set_error(error_get_last()['message'] ?? T_sprintf("Unknown error while sending mail. Hooks tried: %d.", $hooks_tried));
}
return $rc;
}
function set_error(string $message): void {
$this->last_error = $message;
user_error("Error sending mail: $message", E_USER_WARNING);
}
function error(): string {
return $this->last_error;
}
}

657
classes/OPML.php Normal file
View File

@@ -0,0 +1,657 @@
<?php
class OPML extends Handler_Protected {
function csrf_ignore(string $method): bool {
$csrf_ignored = array("export", "import");
return array_search($method, $csrf_ignored) !== false;
}
/**
* @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(): bool|int|null {
$output_name = sprintf("tt-rss_%s_%s.opml", $_SESSION["name"], date("Y-m-d"));
$include_settings = $_REQUEST["include_settings"] == "1";
$owner_uid = $_SESSION["uid"];
$rc = $this->opml_export($output_name, $owner_uid, false, $include_settings);
return $rc;
}
// Export
private function opml_export_category(int $owner_uid, int $cat_id, bool $hide_private_feeds = false, bool $include_settings = true): string {
if ($hide_private_feeds)
$hide_qpart = "(private IS false AND auth_login = '' AND auth_pass = '')";
else
$hide_qpart = "true";
$out = "";
$ttrss_specific_qpart = "";
if ($cat_id) {
$sth = $this->pdo->prepare("SELECT title,order_id
FROM ttrss_feed_categories WHERE id = ?
AND owner_uid = ?");
$sth->execute([$cat_id, $owner_uid]);
$row = $sth->fetch();
$cat_title = htmlspecialchars($row['title']);
if ($include_settings) {
$order_id = (int)$row["order_id"];
$ttrss_specific_qpart = "ttrssSortOrder=\"$order_id\"";
}
} else {
$cat_title = "";
}
if ($cat_title) $out .= "<outline text=\"$cat_title\" $ttrss_specific_qpart>\n";
$sth = $this->pdo->prepare("SELECT id,title
FROM ttrss_feed_categories WHERE
(parent_cat = :cat OR (:cat = 0 AND parent_cat IS NULL)) AND
owner_uid = :uid ORDER BY order_id, title");
$sth->execute([':cat' => $cat_id, ':uid' => $owner_uid]);
while ($line = $sth->fetch()) {
$out .= $this->opml_export_category($owner_uid, $line["id"], $hide_private_feeds, $include_settings);
}
$fsth = $this->pdo->prepare("select title, feed_url, site_url, update_interval, order_id, purge_interval
FROM ttrss_feeds WHERE
(cat_id = :cat OR (:cat = 0 AND cat_id IS NULL)) AND owner_uid = :uid AND $hide_qpart
ORDER BY order_id, title");
$fsth->execute([':cat' => $cat_id, ':uid' => $owner_uid]);
while ($fline = $fsth->fetch()) {
$title = htmlspecialchars($fline["title"]);
$url = htmlspecialchars($fline["feed_url"]);
$site_url = htmlspecialchars($fline["site_url"]);
if ($include_settings) {
$update_interval = (int)$fline["update_interval"];
$order_id = (int)$fline["order_id"];
$purge_interval = (int)$fline["purge_interval"];
$ttrss_specific_qpart = "ttrssSortOrder=\"$order_id\" ttrssPurgeInterval=\"$purge_interval\" ttrssUpdateInterval=\"$update_interval\"";
} else {
$ttrss_specific_qpart = "";
}
if ($site_url) {
$html_url_qpart = "htmlUrl=\"$site_url\"";
} else {
$html_url_qpart = "";
}
$out .= "<outline type=\"rss\" text=\"$title\" xmlUrl=\"$url\" $ttrss_specific_qpart $html_url_qpart/>\n";
}
if ($cat_title) $out .= "</outline>\n";
return $out;
}
/**
* @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): bool|int|null {
if (!$owner_uid) return null;
if (!$file_output)
if (!isset($_REQUEST["debug"])) {
header("Content-type: application/xml+opml");
header("Content-Disposition: attachment; filename=$filename");
} else {
header("Content-type: text/xml");
}
$out = "<?xml version=\"1.0\" encoding=\"utf-8\"?".">";
$out .= "<opml version=\"1.0\">";
$out .= "<head>
<dateCreated>" . date("r", time()) . "</dateCreated>
<title>Tiny Tiny RSS Feed Export</title>
</head>";
$out .= "<body>";
$out .= $this->opml_export_category($owner_uid, 0, $hide_private_feeds, $include_settings);
# export tt-rss settings
if ($include_settings) {
$out .= "<outline text=\"tt-rss-prefs\" schema-version=\"".Config::SCHEMA_VERSION."\">";
$sth = $this->pdo->prepare("SELECT pref_name, value FROM ttrss_user_prefs2 WHERE
profile IS NULL AND owner_uid = ? ORDER BY pref_name");
$sth->execute([$owner_uid]);
while ($line = $sth->fetch()) {
$name = $line["pref_name"];
$value = htmlspecialchars($line["value"]);
$out .= "<outline pref-name=\"$name\" value=\"$value\"/>";
}
$out .= "</outline>";
$out .= "<outline text=\"tt-rss-labels\" schema-version=\"".Config::SCHEMA_VERSION."\">";
$sth = $this->pdo->prepare("SELECT * FROM ttrss_labels2 WHERE
owner_uid = ?");
$sth->execute([$owner_uid]);
while ($line = $sth->fetch()) {
$name = htmlspecialchars($line['caption']);
$fg_color = htmlspecialchars($line['fg_color']);
$bg_color = htmlspecialchars($line['bg_color']);
$out .= "<outline label-name=\"$name\" label-fg-color=\"$fg_color\" label-bg-color=\"$bg_color\"/>";
}
$out .= "</outline>";
$out .= "<outline text=\"tt-rss-filters\" schema-version=\"".Config::SCHEMA_VERSION."\">";
$sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2
WHERE owner_uid = ? ORDER BY id");
$sth->execute([$owner_uid]);
while ($line = $sth->fetch(PDO::FETCH_ASSOC)) {
$line["rules"] = array();
$line["actions"] = array();
$tmph = $this->pdo->prepare("SELECT * FROM ttrss_filters2_rules
WHERE filter_id = ?");
$tmph->execute([$line['id']]);
while ($tmp_line = $tmph->fetch(PDO::FETCH_ASSOC)) {
unset($tmp_line["id"]);
unset($tmp_line["filter_id"]);
$cat_filter = $tmp_line["cat_filter"];
if (!$tmp_line["match_on"]) {
if ($cat_filter && $tmp_line["cat_id"] || $tmp_line["feed_id"]) {
$tmp_line["feed"] = Feeds::_get_title(
$cat_filter ? $tmp_line["cat_id"] : $tmp_line["feed_id"],
$owner_uid,
$cat_filter);
} else {
$tmp_line["feed"] = "";
}
} else {
$match = [];
foreach (json_decode($tmp_line["match_on"], true) as $feed_id) {
if (str_starts_with($feed_id, "CAT:")) {
$feed_id = (int)substr($feed_id, 4);
if ($feed_id) {
array_push($match, [Feeds::_get_cat_title($feed_id, $owner_uid), true, false]);
} else {
array_push($match, [0, true, true]);
}
} else {
if ($feed_id) {
array_push($match, [Feeds::_get_title((int)$feed_id, $owner_uid), false, false]);
} else {
array_push($match, [0, false, true]);
}
}
}
$tmp_line["match"] = $match;
unset($tmp_line["match_on"]);
}
unset($tmp_line["feed_id"]);
unset($tmp_line["cat_id"]);
array_push($line["rules"], $tmp_line);
}
$tmph = $this->pdo->prepare("SELECT * FROM ttrss_filters2_actions
WHERE filter_id = ?");
$tmph->execute([$line['id']]);
while ($tmp_line = $tmph->fetch(PDO::FETCH_ASSOC)) {
unset($tmp_line["id"]);
unset($tmp_line["filter_id"]);
array_push($line["actions"], $tmp_line);
}
unset($line["id"]);
unset($line["owner_uid"]);
$filter = json_encode($line);
$out .= "<outline filter-type=\"2\"><![CDATA[$filter]]></outline>";
}
$out .= "</outline>";
}
$out .= "</body></opml>";
// Format output.
$doc = new DOMDocument();
$doc->formatOutput = true;
$doc->preserveWhiteSpace = false;
$doc->loadXML($out);
$xpath = new DOMXPath($doc);
$outlines = $xpath->query("//outline[@title]");
// cleanup empty categories
foreach ($outlines as $node) {
if ($node->getElementsByTagName('outline')->length == 0)
$node->parentNode->removeChild($node);
}
$res = $doc->saveXML();
/* // saveXML uses a two-space indent. Change to tabs.
$res = preg_replace_callback('/^(?: )+/mu',
create_function(
'$matches',
'return str_repeat("\t", intval(strlen($matches[0])/2));'),
$res); */
if ($file_output)
return file_put_contents($filename, $res) > 0;
print $res;
return true;
}
// Import
private function opml_import_feed(DOMNode $node, int $cat_id, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$feed_title = mb_substr($attrs->getNamedItem('text')->nodeValue, 0, 250);
if (!$feed_title) $feed_title = mb_substr($attrs->getNamedItem('title')->nodeValue, 0, 250);
$feed_url = $attrs->getNamedItem('xmlUrl')->nodeValue;
if (!$feed_url) $feed_url = $attrs->getNamedItem('xmlURL')->nodeValue;
$site_url = mb_substr($attrs->getNamedItem('htmlUrl')->nodeValue, 0, 250);
if ($feed_url) {
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE
feed_url = ? AND owner_uid = ?");
$sth->execute([$feed_url, $owner_uid]);
if (!$feed_title) $feed_title = '[Unknown]';
if (!$sth->fetch()) {
#$this->opml_notice("[FEED] [$feed_title/$feed_url] dst_CAT=$cat_id");
$this->opml_notice(T_sprintf("Adding feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title), $nest);
if (!$cat_id) $cat_id = null;
$update_interval = (int) $attrs->getNamedItem('ttrssUpdateInterval')->nodeValue;
if (!$update_interval) $update_interval = 0;
$order_id = (int) $attrs->getNamedItem('ttrssSortOrder')->nodeValue;
if (!$order_id) $order_id = 0;
$purge_interval = (int) $attrs->getNamedItem('ttrssPurgeInterval')->nodeValue;
if (!$purge_interval) $purge_interval = 0;
$sth = $this->pdo->prepare("INSERT INTO ttrss_feeds
(title, feed_url, owner_uid, cat_id, site_url, order_id, update_interval, purge_interval) VALUES
(?, ?, ?, ?, ?, ?, ?, ?)");
$sth->execute([$feed_title, $feed_url, $owner_uid, $cat_id, $site_url, $order_id, $update_interval, $purge_interval]);
} else {
$this->opml_notice(T_sprintf("Duplicate feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title), $nest);
}
}
}
private function opml_import_label(DOMNode $node, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$label_name = $attrs->getNamedItem('label-name')->nodeValue;
if ($label_name) {
$fg_color = $attrs->getNamedItem('label-fg-color')->nodeValue;
$bg_color = $attrs->getNamedItem('label-bg-color')->nodeValue;
if (!Labels::find_id($label_name, $owner_uid)) {
$this->opml_notice(T_sprintf("Adding label %s", htmlspecialchars($label_name)), $nest);
Labels::create($label_name, $fg_color, $bg_color, $owner_uid);
} else {
$this->opml_notice(T_sprintf("Duplicate label: %s", htmlspecialchars($label_name)), $nest);
}
}
}
/**
* @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 {
$attrs = $node->attributes;
$pref_name = $attrs->getNamedItem('pref-name')->nodeValue;
if ($pref_name) {
$pref_value = $attrs->getNamedItem('value')->nodeValue;
$this->opml_notice(T_sprintf("Setting preference key %s to %s",
$pref_name, $pref_value), $nest);
Prefs::set($pref_name, $pref_value, $owner_uid, $_SESSION['profile'] ?? null);
}
}
private function opml_import_filter(DOMNode $node, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$filter_type = $attrs->getNamedItem('filter-type')->nodeValue;
if ($filter_type == '2') {
$filter = json_decode($node->nodeValue, true);
if ($filter) {
$match_any_rule = bool_to_sql_bool($filter["match_any_rule"]);
$enabled = bool_to_sql_bool($filter["enabled"]);
$inverse = bool_to_sql_bool($filter["inverse"]);
$title = $filter["title"];
//print "F: $title, $inverse, $enabled, $match_any_rule";
$sth = $this->pdo->prepare("INSERT INTO ttrss_filters2 (match_any_rule,enabled,inverse,title,owner_uid)
VALUES (?, ?, ?, ?, ?) RETURNING id");
$sth->execute([$match_any_rule, $enabled, $inverse, $title, $owner_uid]);
$row = $sth->fetch();
$filter_id = $row['id'];
if ($filter_id) {
$this->opml_notice(T_sprintf("Adding filter %s...", $title), $nest);
//$this->opml_notice(json_encode($filter));
foreach ($filter["rules"] as $rule) {
$feed_id = null;
$cat_id = null;
if ($rule["match"] ?? false) {
$match_on = [];
foreach ($rule["match"] as $match) {
list ($name, $is_cat, $is_id) = $match;
if ($is_id) {
array_push($match_on, ($is_cat ? "CAT:" : "") . $name);
} else {
$match_id = Feeds::_find_by_title($name, $is_cat, $owner_uid);
if ($match_id) {
if ($is_cat) {
array_push($match_on, "CAT:$match_id");
} else {
array_push($match_on, $match_id);
}
}
/*if (!$is_cat) {
$tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
WHERE title = ? AND owner_uid = ?");
$tsth->execute([$name, $_SESSION['uid']]);
if ($row = $tsth->fetch()) {
$match_id = $row['id'];
array_push($match_on, $match_id);
}
} else {
$tsth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories
WHERE title = ? AND owner_uid = ?");
$tsth->execute([$name, $_SESSION['uid']]);
if ($row = $tsth->fetch()) {
$match_id = $row['id'];
array_push($match_on, "CAT:$match_id");
}
} */
}
}
$reg_exp = $rule["reg_exp"];
$filter_type = (int)$rule["filter_type"];
$inverse = bool_to_sql_bool($rule["inverse"]);
$match_on = json_encode($match_on);
$usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules
(feed_id,cat_id,match_on,filter_id,filter_type,reg_exp,cat_filter,inverse)
VALUES
(NULL, NULL, ?, ?, ?, ?, false, ?)");
$usth->execute([$match_on, $filter_id, $filter_type, $reg_exp, $inverse]);
} else {
$match_id = Feeds::_find_by_title($rule['feed'] ?? "", $rule['cat_filter'], $owner_uid);
if ($match_id) {
if ($rule['cat_filter']) {
$cat_id = $match_id;
} else {
$feed_id = $match_id;
}
}
/*if (!$rule["cat_filter"]) {
$tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
WHERE title = ? AND owner_uid = ?");
$tsth->execute([$rule['feed'], $_SESSION['uid']]);
if ($row = $tsth->fetch()) {
$feed_id = $row['id'];
}
} else {
$tsth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories
WHERE title = ? AND owner_uid = ?");
$tsth->execute([$rule['feed'], $_SESSION['uid']]);
if ($row = $tsth->fetch()) {
$feed_id = $row['id'];
}
} */
$cat_filter = bool_to_sql_bool($rule["cat_filter"]);
$reg_exp = $rule["reg_exp"];
$filter_type = (int)$rule["filter_type"];
$inverse = bool_to_sql_bool($rule["inverse"]);
$usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules
(feed_id,cat_id,filter_id,filter_type,reg_exp,cat_filter,inverse)
VALUES
(?, ?, ?, ?, ?, ?, ?)");
$usth->execute([$feed_id, $cat_id, $filter_id, $filter_type, $reg_exp, $cat_filter, $inverse]);
}
}
foreach ($filter["actions"] as $action) {
$action_id = (int)$action["action_id"];
$action_param = $action["action_param"];
$usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_actions
(filter_id,action_id,action_param)
VALUES
(?, ?, ?)");
$usth->execute([$filter_id, $action_id, $action_param]);
}
}
}
}
}
private function opml_import_category(DOMDocument $doc, ?DOMNode $root_node, int $owner_uid, int $parent_id, int $nest): void {
$default_cat_id = (int) $this->get_feed_category('Imported feeds', $owner_uid, 0);
if ($root_node) {
$cat_title = mb_substr($root_node->attributes->getNamedItem('text')->nodeValue, 0, 250);
if (!$cat_title)
$cat_title = mb_substr($root_node->attributes->getNamedItem('title')->nodeValue, 0, 250);
if (!in_array($cat_title, array("tt-rss-filters", "tt-rss-labels", "tt-rss-prefs"))) {
$cat_id = $this->get_feed_category($cat_title, $owner_uid, $parent_id);
if ($cat_id === 0) {
$order_id = (int) $root_node->attributes->getNamedItem('ttrssSortOrder')->nodeValue;
Feeds::_add_cat($cat_title, $owner_uid, $parent_id ? $parent_id : null, (int)$order_id);
$cat_id = $this->get_feed_category($cat_title, $owner_uid, $parent_id);
}
} else {
$cat_id = 0;
}
$outlines = $root_node->childNodes;
} else {
$xpath = new DOMXPath($doc);
$outlines = $xpath->query("//opml/body/outline");
$cat_id = 0;
$cat_title = false;
}
//$this->opml_notice("[CAT] $cat_title id: $cat_id P_id: $parent_id");
$this->opml_notice(T_sprintf("Processing category: %s", $cat_title ? $cat_title : __("Uncategorized")), $nest);
foreach ($outlines as $node) {
if ($node->hasAttributes() && strtolower($node->tagName) == "outline") {
$attrs = $node->attributes;
$node_cat_title = $attrs->getNamedItem('text') ? $attrs->getNamedItem('text')->nodeValue : false;
if (!$node_cat_title)
$node_cat_title = $attrs->getNamedItem('title') ? $attrs->getNamedItem('title')->nodeValue : false;
$node_feed_url = $attrs->getNamedItem('xmlUrl') ? $attrs->getNamedItem('xmlUrl')->nodeValue : false;
if ($node_cat_title && !$node_feed_url) {
$this->opml_import_category($doc, $node, $owner_uid, $cat_id, $nest+1);
} else {
if (!$cat_id) {
$dst_cat_id = $default_cat_id;
} else {
$dst_cat_id = $cat_id;
}
match ($cat_title) {
'tt-rss-prefs' => $this->opml_import_preference($node, $owner_uid, $nest+1),
'tt-rss-labels' => $this->opml_import_label($node, $owner_uid, $nest+1),
'tt-rss-filters' => $this->opml_import_filter($node, $owner_uid, $nest+1),
default => $this->opml_import_feed($node, $dst_cat_id, $owner_uid, $nest+1),
};
}
}
}
}
/** $filename is optional; assumes HTTP upload with $_FILES otherwise */
/**
* @return bool|null false on failure, true if successful, null if $owner_uid is missing
*/
function opml_import(int $owner_uid, string $filename = ""): ?bool {
if (!$owner_uid) return null;
if (!$filename) {
if ($_FILES['opml_file']['error'] != 0) {
print_error(T_sprintf("Upload failed with error code %d",
$_FILES['opml_file']['error']));
return false;
}
if (is_uploaded_file($_FILES['opml_file']['tmp_name'])) {
$tmp_file = (string)tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'opml');
$result = move_uploaded_file($_FILES['opml_file']['tmp_name'],
$tmp_file);
if (!$result) {
print_error(__("Unable to move uploaded file."));
return false;
}
} else {
print_error(__('Error: please upload OPML file.'));
return false;
}
} else {
$tmp_file = $filename;
}
if (!is_readable($tmp_file)) {
$this->opml_notice(T_sprintf("Error: file is not readable: %s", $filename));
return false;
}
$doc = new DOMDocument();
$loaded = $doc->load($tmp_file);
// only remove temporary i.e. HTTP uploaded files
if (!$filename)
unlink($tmp_file);
if ($loaded) {
// we're using ORM while importing so we can't transaction-lock things anymore
//$this->pdo->beginTransaction();
$this->opml_import_category($doc, null, $owner_uid, 0, 0);
//$this->pdo->commit();
} else {
$this->opml_notice(__('Error while parsing document.'));
return false;
}
return true;
}
private function opml_notice(string $msg, int $prefix_length = 0): void {
if (php_sapi_name() == "cli") {
Debug::log(str_repeat(" ", $prefix_length) . $msg);
} else {
// TODO: use better separator i.e. CSS-defined span of certain width or something
print str_repeat("&nbsp;&nbsp;&nbsp;", $prefix_length) . $msg . "<br/>";
}
}
function get_feed_category(string $feed_cat, int $owner_uid, int $parent_cat_id) : int {
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories
WHERE title = :title
AND (parent_cat = :parent OR (:parent = 0 AND parent_cat IS NULL))
AND owner_uid = :uid");
$sth->execute([':title' => $feed_cat, ':parent' => $parent_cat_id, ':uid' => $owner_uid]);
if ($row = $sth->fetch()) {
return $row['id'];
} else {
return 0;
}
}
}

738
classes/Plugin.php Normal file
View File

@@ -0,0 +1,738 @@
<?php
abstract class Plugin {
const API_VERSION_COMPAT = 1;
/** @var PDO $pdo */
protected $pdo;
/**
* @param PluginHost $host
*
* @return void
* */
abstract function init($host);
/** @return array<null|float|string|bool> */
abstract function about();
// return array(1.0, "plugin", "No description", "No author", false);
function __construct() {
$this->pdo = Db::pdo();
}
/** @return array<string,bool> */
function flags() {
/* associative array, possible keys:
needs_curl = boolean
*/
return array();
}
/**
* @param string $method
*
* @return bool */
function is_public_method($method) {
return false;
}
/**
* @param string $method
*
* @return bool */
function csrf_ignore($method) {
return false;
}
/** @return string */
function get_js() {
return "";
}
/** @return string */
function get_login_js() {
return "";
}
/** @return string */
function get_css() {
return "";
}
/** @return string */
function get_prefs_js() {
return "";
}
/** @return int */
function api_version() {
return Plugin::API_VERSION_COMPAT;
}
/* gettext-related helpers */
/**
* @param string $msgid
*
* @return string */
function __($msgid) {
// this is a strictly template-related hack
return _dgettext(PluginHost::object_to_domain($this), $msgid);
}
/**
* @param string $singular
* @param string $plural
* @param int $number
*
* @return string */
function _ngettext($singular, $plural, $number) {
// this is a strictly template-related hack
return _dngettext(PluginHost::object_to_domain($this), $singular, $plural, $number);
}
/** @return string */
function T_sprintf() {
$args = func_get_args();
$msgid = array_shift($args);
return vsprintf($this->__($msgid), $args);
}
/* plugin hook methods */
/* GLOBAL hooks are invoked in global context, only available to system plugins (loaded via .env for all users) */
/** Adds buttons for article (on the right) - e.g. mail, share, add note. Generated markup must be valid XML.
* @param array<string,mixed> $line
* @return string
* @see PluginHost::HOOK_ARTICLE_BUTTON
* @see Plugin::hook_article_left_button()
*/
function hook_article_button($line) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows plugins to alter article data as gathered from feed XML, i.e. embed images, get full text content, etc.
* @param array<string,mixed> $article
* @return array<string,mixed>
* @see PluginHost::HOOK_ARTICLE_FILTER
*/
function hook_article_filter($article) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Allow adding new UI elements (e.g. accordion panes) to (top) tab contents in Preferences
* @param string $tab
* @return void
* @see PluginHost::HOOK_PREFS_TAB
*/
function hook_prefs_tab($tab) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Allow adding new content to various sections of preferences UI (i.e. OPML import/export pane)
* @param string $section
* @return void
* @see PluginHost::HOOK_PREFS_TAB_SECTION
*/
function hook_prefs_tab_section($section) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Allows adding new (top) tabs in preferences UI
* @return void
* @see PluginHost::HOOK_PREFS_TABS
*/
function hook_prefs_tabs() {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Invoked when feed XML is processed by FeedParser class
* @param FeedParser $parser
* @param int $feed_id
* @return void
* @see PluginHost::HOOK_FEED_PARSED
*/
function hook_feed_parsed($parser, $feed_id) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** GLOBAL: Invoked when a feed update task finishes
* @param array<string,string> $cli_options
* @return void
* @see PluginHost::HOOK_UPDATE_TASK
*/
function hook_update_task($cli_options) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** This is a pluginhost compatibility wrapper that invokes $this->authenticate(...$args) (Auth_Base)
* @param string $login
* @param string $password
* @param string $service
* @return int|false user_id
* @see PluginHost::HOOK_AUTH_USER
*/
function hook_auth_user($login, $password, $service = '') {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
/** IAuthModule only
* @param string $login
* @param string $password
* @param string $service
* @return int|false user_id
*/
function authenticate($login, $password, $service = '') {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
/** Allows plugins to modify global hotkey map (hotkey sequence -> action)
* @param array<string, string> $hotkeys
* @return array<string, string>
* @see PluginHost::HOOK_HOTKEY_MAP
* @see Plugin::hook_hotkey_info()
*/
function hook_hotkey_map($hotkeys) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Invoked when article is rendered by backend (before it gets passed to frontent JS code) - three panel mode
* @param array<string, mixed> $article
* @return array<string, mixed>
* @see PluginHost::HOOK_RENDER_ARTICLE
*/
function hook_render_article($article) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Invoked when article is rendered by backend (before it gets passed to frontent JS code) - combined mode
* @param array<string, mixed> $article
* @return array<string, mixed>
* @see PluginHost::HOOK_RENDER_ARTICLE_CDM
*/
function hook_render_article_cdm($article) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Invoked when raw feed XML data has been successfully downloaded (but not parsed yet)
* @param string $feed_data
* @param string $fetch_url
* @param int $owner_uid
* @param int $feed
* @return string
* @see PluginHost::HOOK_FEED_FETCHED
*/
function hook_feed_fetched($feed_data, $fetch_url, $owner_uid, $feed) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked on article content when it is sanitized (i.e. potentially harmful tags removed)
* @param DOMDocument $doc
* @param string $site_url
* @param array<string> $allowed_elements
* @param array<string> $disallowed_attributes
* @param int $article_id
* @return DOMDocument|array<int,DOMDocument|array<string>>
* @see PluginHost::HOOK_SANITIZE
*/
function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) {
user_error("Dummy method invoked.", E_USER_ERROR);
return $doc;
}
/** Invoked when article is rendered by backend (before it gets passed to frontent JS code) - exclusive to API clients
* @param array{'article': array<string,mixed>|null, 'headline': array<string,mixed>|null} $params
* @return array<string, string>
* @see PluginHost::HOOK_RENDER_ARTICLE_API
*/
function hook_render_article_api($params) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Allows adding new UI elements to tt-rss main toolbar (to the right, before Actions... dropdown)
* @return string
* @see PluginHost::HOOK_TOOLBAR_BUTTON
*/
function hook_toolbar_button() {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows adding new items to tt-rss main Actions... dropdown menu
* @return string
* @see PluginHost::HOOK_ACTION_ITEM
*/
function hook_action_item() {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows adding new UI elements to the toolbar area related to currently loaded feed headlines
* @param int $feed_id
* @param bool $is_cat
* @return string
* @see PluginHost::HOOK_HEADLINE_TOOLBAR_BUTTON
*/
function hook_headline_toolbar_button($feed_id, $is_cat) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows adding new hotkey action names and descriptions
* @param array<string, array<string, string>> $hotkeys
* @return array<string, array<string, string>>
* @see PluginHost::HOOK_HOTKEY_INFO
* @see Plugin::hook_hotkey_map()
*/
function hook_hotkey_info($hotkeys) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Adds per-article buttons on the left side. Generated markup must be valid XML.
* @param array<string,mixed> $row
* @return string
* @see PluginHost::HOOK_ARTICLE_LEFT_BUTTON
* @see Plugin::hook_article_button()
*/
function hook_article_left_button($row) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows adding new UI elements to the "Plugins" tab of the feed editor UI
* @param int $feed_id
* @return void
* @see PluginHost::HOOK_PREFS_EDIT_FEED
*/
function hook_prefs_edit_feed($feed_id) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Invoked when data is saved in the feed editor
* @param int $feed_id
* @return void
* @see PluginHost::HOOK_PREFS_SAVE_FEED
*/
function hook_prefs_save_feed($feed_id) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Allows overriding built-in fetching mechanism for feeds, substituting received data if necessary
* (i.e. origin site doesn't actually provide any RSS feeds), or XML is invalid
* @param string $feed_data
* @param string $fetch_url
* @param int $owner_uid
* @param int $feed
* @param int $last_article_timestamp
* @param string $auth_login
* @param string $auth_pass
* @return string (possibly mangled feed data)
* @see PluginHost::HOOK_FETCH_FEED
*/
function hook_fetch_feed($feed_data, $fetch_url, $owner_uid, $feed, $last_article_timestamp, $auth_login, $auth_pass) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked when headlines data ($row) has been retrieved from the database
* @param array<string,mixed> $row
* @param int $excerpt_length
* @return array<string,mixed>
* @see PluginHost::HOOK_QUERY_HEADLINES
*/
function hook_query_headlines($row, $excerpt_length) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** This is run periodically by the update daemon when idle (available both to user and system plugins)
* @return void
* @see PluginHost::HOOK_HOUSE_KEEPING */
function hook_house_keeping() {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Allows overriding built-in article search
* @param string $query
* @return array<int, string|array<string>> - list(SQL search query, highlight keywords)
* @see PluginHost::HOOK_SEARCH
*/
function hook_search($query) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Invoked when enclosures are rendered to HTML (when article itself is rendered)
* @param string $enclosures_formatted
* @param array<int, array<string, mixed>> $enclosures
* @param int $article_id
* @param bool $always_display_enclosures
* @param string $article_content
* @param bool $hide_images
* @return string|array<string,array<int, array<string, mixed>>> ($enclosures_formatted, $enclosures)
* @see PluginHost::HOOK_FORMAT_ENCLOSURES
*/
function hook_format_enclosures($enclosures_formatted, $enclosures, $article_id, $always_display_enclosures, $article_content, $hide_images) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked during feed subscription (after data has been fetched)
* @param string $contents
* @param string $url
* @param string $auth_login
* @param string $auth_pass
* @return string (possibly mangled feed data)
* @see PluginHost::HOOK_SUBSCRIBE_FEED
*/
function hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/**
* @param int $feed
* @param bool $is_cat
* @param array<string,mixed> $qfh_ret (headlines object)
* @return string
* @see PluginHost::HOOK_HEADLINES_BEFORE
*/
function hook_headlines_before($feed, $is_cat, $qfh_ret) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/**
* @param array<string,mixed> $entry
* @param int $article_id
* @param array<string,mixed> $rv
* @return string
* @see PluginHost::HOOK_RENDER_ENCLOSURE
*/
function hook_render_enclosure($entry, $article_id, $rv) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/**
* @param array<string,mixed> $article
* @param string $action
* @return array<string,mixed> ($article)
* @see PluginHost::HOOK_ARTICLE_FILTER_ACTION
*/
function hook_article_filter_action($article, $action) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/**
* @param array<string,mixed> $line
* @param int $feed
* @param bool $is_cat
* @param int $owner_uid
* @return array<string,mixed> ($line)
* @see PluginHost::HOOK_ARTICLE_EXPORT_FEED
*/
function hook_article_export_feed($line, $feed, $is_cat, $owner_uid) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Allows adding custom buttons to tt-rss main toolbar (left side)
* @return void
* @see PluginHost::HOOK_MAIN_TOOLBAR_BUTTON
*/
function hook_main_toolbar_button() {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Invoked for every enclosure entry as article is being rendered
* @param array<string,string> $entry
* @param int $id article id
* @param array{'formatted': string, 'entries': array<int, array<string, mixed>>} $rv
* @return array<string,string> ($entry)
* @see PluginHost::HOOK_ENCLOSURE_ENTRY
*/
function hook_enclosure_entry($entry, $id, $rv) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Share plugins run this when article is being rendered as HTML for sharing
* @param string $html
* @param array<string,mixed> $row
* @return string ($html)
* @see PluginHost::HOOK_FORMAT_ARTICLE
*/
function hook_format_article($html, $row) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked when basic feed information (title, site_url) is being collected, useful to override default if feed doesn't provide anything (or feed itself is synthesized)
* @param array{"title": string, "site_url": string} $basic_info
* @param string $fetch_url
* @param int $owner_uid
* @param int $feed_id
* @param string $auth_login
* @param string $auth_pass
* @return array{"title": string, "site_url": string}
* @see PluginHost::HOOK_FEED_BASIC_INFO
*/
function hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) {
user_error("Dummy method invoked.", E_USER_ERROR);
return $basic_info;
}
/** Invoked when file (e.g. cache entry, static data) is being sent to client, may override default mechanism
* using faster httpd-specific implementation (see nginx_xaccel)
* @param string $filename
* @return bool
* @see PluginHost::HOOK_SEND_LOCAL_FILE
*/
function hook_send_local_file($filename) {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
/** Invoked when user tries to unsubscribe from a feed, returning true would prevent any further default actions
* @param int $feed_id
* @param int $owner_uid
* @return bool
* @see PluginHost::HOOK_UNSUBSCRIBE_FEED
*/
function hook_unsubscribe_feed($feed_id, $owner_uid) {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
/** Invoked when mail is being sent (if no hooks are registered, tt-rss uses PHP mail() as a fallback)
* @param Mailer $mailer
* @param array<string,mixed> $params
* @return int
* @see PluginHost::HOOK_SEND_MAIL
*/
function hook_send_mail($mailer, $params) {
user_error("Dummy method invoked.", E_USER_ERROR);
return -1;
}
/**
* Invoked when filtering is triggered on an article. May be used to implement logging for filters, etc.
* @param int $feed_id
* @param int $owner_uid
* @param array<string,mixed> $article
* @param array<string,mixed> $matched_filters
* @param array<string,string|bool|int> $matched_rules
* @param array<int, array{'type': string, 'param': string}> $article_filter_actions An array of filter actions from matched filters
* @return void
* @see PluginHost::HOOK_FILTER_TRIGGERED
*/
function hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filter_actions) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Plugins may provide this to allow getting full article text (af_readbility implements this)
* @param string $url
* @return string|false
* @see PluginHost::HOOK_GET_FULL_TEXT
*/
function hook_get_full_text($url) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked when article flavor image is being determined, allows overriding default selection logic
* @param array<string,string> $enclosures
* @param string $content
* @param string $site_url
* @param array<string,mixed> $article
* @return string|array<int,string>
* @see PluginHost::HOOK_ARTICLE_IMAGE
*/
function hook_article_image($enclosures, $content, $site_url, $article) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows adding arbitrary elements before feed tree
* @return string HTML
* @see PluginHost::HOOK_FEED_TREE
* */
function hook_feed_tree() {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked for every iframe to determine if it is allowed to be displayed
* @param string $url
* @return bool
* @see PluginHost::HOOK_IFRAME_WHITELISTED
*/
function hook_iframe_whitelisted($url) {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
/**
* @param object $enclosure
* @param int $feed
* @return object ($enclosure)
* @see PluginHost::HOOK_ENCLOSURE_IMPORTED
*/
function hook_enclosure_imported($enclosure, $feed) {
user_error("Dummy method invoked.", E_USER_ERROR);
return $enclosure;
}
/** Allows adding custom elements to headline sort dropdown (name -> caption)
* @return array<string,string>
* @see PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP
*/
function hook_headlines_custom_sort_map() {
user_error("Dummy method invoked.", E_USER_ERROR);
return ["" => ""];
}
/** Allows overriding headline sorting (or provide custom sort methods)
* @param string $order
* @return array<int, string|bool> -- (query, skip_first_id)
* @see PluginHost::HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE
*/
function hook_headlines_custom_sort_override($order) {
user_error("Dummy method invoked.", E_USER_ERROR);
return ["", false];
}
/** Allows adding custom elements to headlines Select... dropdown
* @deprecated removed, see Plugin::hook_headline_toolbar_select_menu_item2()
* @param int $feed_id
* @param int $is_cat
* @return string
* @see PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM
*/
function hook_headline_toolbar_select_menu_item($feed_id, $is_cat) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows adding custom elements to headlines Select... select dropdown (<option> format)
* @param int $feed_id
* @param int $is_cat
* @return string
* @see PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2
*/
function hook_headline_toolbar_select_menu_item2($feed_id, $is_cat) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked when user tries to subscribe to feed, may override information (i.e. feed URL) used afterwards
* @param string $url
* @param string $auth_login
* @param string $auth_pass
* @return bool
* @see PluginHost::HOOK_PRE_SUBSCRIBE
*/
function hook_pre_subscribe(&$url, $auth_login, $auth_pass) {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
/** Invoked after user logout, may override built-in behavior (redirect back to login page)
* @param string $login
* @param int $user_id
* @return array<mixed> - [0] - if set, url to redirect to
*/
function hook_post_logout($login, $user_id) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [""];
}
/** Adds buttons to the right of default Login button
* @return void
*/
function hook_loginform_additional_buttons() {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Returns false if session is considered invalid, true cascades to next handler */
function hook_validate_session(): bool {
user_error("Dummy method invoked.", E_USER_ERROR);
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);
}
}

29
classes/PluginHandler.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
class PluginHandler extends Handler_Protected {
function csrf_ignore(string $method): bool {
return true;
}
function catchall(string $method): void {
$plugin_name = clean($_REQUEST["plugin"]);
$plugin = PluginHost::getInstance()->get_plugin($plugin_name);
$csrf_token = ($_POST["csrf_token"] ?? "");
if ($plugin) {
if (method_exists($plugin, $method)) {
if (validate_csrf($csrf_token) || $plugin->csrf_ignore($method)) {
$plugin->$method();
} else {
user_error("Rejected {$plugin_name}->{$method}(): invalid CSRF token.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNAUTHORIZED);
}
} else {
user_error("Rejected {$plugin_name}->{$method}(): unknown method.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
}
} else {
user_error("Rejected {$plugin_name}->{$method}(): unknown plugin.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNKNOWN_PLUGIN);
}
}
}

943
classes/PluginHost.php Normal file
View File

@@ -0,0 +1,943 @@
<?php
class PluginHost {
private ?PDO $pdo = null;
/**
* separate handle for plugin data so transaction while saving wouldn't clash with possible main
* tt-rss code transactions; only initialized when first needed
*/
private ?PDO $pdo_data = null;
/** @var array<string, array<int, array<int, Plugin>>> hook types -> priority levels -> Plugins */
private array $hooks = [];
/** @var array<string, Plugin> */
private array $plugins = [];
/** @var array<string, array<string, Plugin>> handler type -> method type -> Plugin */
private array $handlers = [];
/** @var array<string, array{'description': string, 'suffix': string, 'arghelp': string, 'class': Plugin}> command type -> details array */
private array $commands = [];
/** @var array<string, array<string, mixed>> plugin name -> (potential profile array) -> key -> value */
private array $storage = [];
/** @var array<int, array{'id': int, 'title': string, 'sender': Plugin, 'icon': string}> */
private array $special_feeds = [];
/** @var array<string, Plugin> API method name, Plugin sender */
private array $api_methods = [];
/** @var array<string, array<int, array{'action': string, 'description': string, 'sender': Plugin}>> */
private array $plugin_actions = [];
private ?int $owner_uid = null;
private bool $data_loaded = false;
private static ?PluginHost $instance = null;
private ?Scheduler $scheduler = null;
const API_VERSION = 2;
const PUBLIC_METHOD_DELIMITER = "--";
/** @see Plugin::hook_article_button() */
const HOOK_ARTICLE_BUTTON = "hook_article_button";
/** @see Plugin::hook_article_filter() */
const HOOK_ARTICLE_FILTER = "hook_article_filter";
/** @see Plugin::hook_prefs_tab() */
const HOOK_PREFS_TAB = "hook_prefs_tab";
/** @see Plugin::hook_prefs_tab_section() */
const HOOK_PREFS_TAB_SECTION = "hook_prefs_tab_section";
/** @see Plugin::hook_prefs_tabs() */
const HOOK_PREFS_TABS = "hook_prefs_tabs";
/** @see Plugin::hook_feed_parsed() */
const HOOK_FEED_PARSED = "hook_feed_parsed";
/** @see Plugin::hook_update_task() */
const HOOK_UPDATE_TASK = "hook_update_task"; //*1
/** @see Plugin::hook_auth_user() */
const HOOK_AUTH_USER = "hook_auth_user";
/** @see Plugin::hook_hotkey_map() */
const HOOK_HOTKEY_MAP = "hook_hotkey_map";
/** @see Plugin::hook_render_article() */
const HOOK_RENDER_ARTICLE = "hook_render_article";
/** @see Plugin::hook_render_article_cdm() */
const HOOK_RENDER_ARTICLE_CDM = "hook_render_article_cdm";
/** @see Plugin::hook_feed_fetched() */
const HOOK_FEED_FETCHED = "hook_feed_fetched";
/** @see Plugin::hook_sanitize() */
const HOOK_SANITIZE = "hook_sanitize";
/** @see Plugin::hook_render_article_api() */
const HOOK_RENDER_ARTICLE_API = "hook_render_article_api";
/** @see Plugin::hook_toolbar_button() */
const HOOK_TOOLBAR_BUTTON = "hook_toolbar_button";
/** @see Plugin::hook_action_item() */
const HOOK_ACTION_ITEM = "hook_action_item";
/** @see Plugin::hook_headline_toolbar_button() */
const HOOK_HEADLINE_TOOLBAR_BUTTON = "hook_headline_toolbar_button";
/** @see Plugin::hook_hotkey_info() */
const HOOK_HOTKEY_INFO = "hook_hotkey_info";
/** @see Plugin::hook_article_left_button() */
const HOOK_ARTICLE_LEFT_BUTTON = "hook_article_left_button";
/** @see Plugin::hook_prefs_edit_feed() */
const HOOK_PREFS_EDIT_FEED = "hook_prefs_edit_feed";
/** @see Plugin::hook_prefs_save_feed() */
const HOOK_PREFS_SAVE_FEED = "hook_prefs_save_feed";
/** @see Plugin::hook_fetch_feed() */
const HOOK_FETCH_FEED = "hook_fetch_feed";
/** @see Plugin::hook_query_headlines() */
const HOOK_QUERY_HEADLINES = "hook_query_headlines";
/** @see Plugin::hook_house_keeping() */
const HOOK_HOUSE_KEEPING = "hook_house_keeping"; //*1
/** @see Plugin::hook_search() */
const HOOK_SEARCH = "hook_search";
/** @see Plugin::hook_format_enclosures() */
const HOOK_FORMAT_ENCLOSURES = "hook_format_enclosures";
/** @see Plugin::hook_subscribe_feed() */
const HOOK_SUBSCRIBE_FEED = "hook_subscribe_feed";
/** @see Plugin::hook_headlines_before() */
const HOOK_HEADLINES_BEFORE = "hook_headlines_before";
/** @see Plugin::hook_render_enclosure() */
const HOOK_RENDER_ENCLOSURE = "hook_render_enclosure";
/** @see Plugin::hook_article_filter_action() */
const HOOK_ARTICLE_FILTER_ACTION = "hook_article_filter_action";
/** @see Plugin::hook_article_export_feed() */
const HOOK_ARTICLE_EXPORT_FEED = "hook_article_export_feed";
/** @see Plugin::hook_main_toolbar_button() */
const HOOK_MAIN_TOOLBAR_BUTTON = "hook_main_toolbar_button";
/** @see Plugin::hook_enclosure_entry() */
const HOOK_ENCLOSURE_ENTRY = "hook_enclosure_entry";
/** @see Plugin::hook_format_article() */
const HOOK_FORMAT_ARTICLE = "hook_format_article";
/** @see Plugin::hook_feed_basic_info() */
const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info";
/** @see Plugin::hook_send_local_file() */
const HOOK_SEND_LOCAL_FILE = "hook_send_local_file";
/** @see Plugin::hook_unsubscribe_feed() */
const HOOK_UNSUBSCRIBE_FEED = "hook_unsubscribe_feed";
/** @see Plugin::hook_send_mail() */
const HOOK_SEND_MAIL = "hook_send_mail";
/** @see Plugin::hook_filter_triggered() */
const HOOK_FILTER_TRIGGERED = "hook_filter_triggered";
/** @see Plugin::hook_get_full_text() */
const HOOK_GET_FULL_TEXT = "hook_get_full_text";
/** @see Plugin::hook_article_image() */
const HOOK_ARTICLE_IMAGE = "hook_article_image";
/** @see Plugin::hook_feed_tree() */
const HOOK_FEED_TREE = "hook_feed_tree";
/** @see Plugin::hook_iframe_whitelisted() */
const HOOK_IFRAME_WHITELISTED = "hook_iframe_whitelisted";
/** @see Plugin::hook_enclosure_imported() */
const HOOK_ENCLOSURE_IMPORTED = "hook_enclosure_imported";
/** @see Plugin::hook_headlines_custom_sort_map() */
const HOOK_HEADLINES_CUSTOM_SORT_MAP = "hook_headlines_custom_sort_map";
/** @see Plugin::hook_headlines_custom_sort_override() */
const HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE = "hook_headlines_custom_sort_override";
/** @see Plugin::hook_headline_toolbar_select_menu_item()
* @deprecated removed, see PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2
*/
const HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM = "hook_headline_toolbar_select_menu_item";
/** @see Plugin::hook_headline_toolbar_select_menu_item() */
const HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2 = "hook_headline_toolbar_select_menu_item2";
/** @see Plugin::hook_pre_subscribe() */
const HOOK_PRE_SUBSCRIBE = "hook_pre_subscribe";
/** @see Plugin::hook_post_logout() */
const HOOK_POST_LOGOUT = "hook_post_logout";
/** @see Plugin::hook_loginform_additional_buttons() */
const HOOK_LOGINFORM_ADDITIONAL_BUTTONS = "hook_loginform_additional_buttons";
/** @see Plugin::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_SYSTEM = 2;
const KIND_USER = 3;
static function object_to_domain(Plugin $plugin): string {
return strtolower(get_class($plugin));
}
function __construct() {
$this->pdo = Db::pdo();
$this->scheduler = new Scheduler('PluginHost Scheduler');
$this->storage = [];
}
private function __clone() {
//
}
public static function getInstance(): PluginHost {
if (self::$instance == null)
self::$instance = new self();
return self::$instance;
}
private function register_plugin(string $name, Plugin $plugin): void {
//array_push($this->plugins, $plugin);
$this->plugins[$name] = $plugin;
}
/** needed for compatibility with API 1 */
function get_link(): bool {
return false;
}
/** needed for compatibility with API 2 (?) */
function get_dbh(): bool {
return false;
}
function get_pdo(): PDO {
return $this->pdo;
}
/**
* @return array<int, string>
*/
function get_plugin_names(): array {
$names = [];
foreach ($this->plugins as $p) {
array_push($names, get_class($p));
}
return $names;
}
/**
* @return array<Plugin>
*/
function get_plugins(): array {
return $this->plugins;
}
function get_plugin(string $name): ?Plugin {
return $this->plugins[strtolower($name)] ?? null;
}
/**
* @param PluginHost::HOOK_* $hook
* @param mixed $args
*/
function run_hooks(string $hook, ...$args): void {
$method = strtolower((string)$hook);
foreach ($this->get_hooks((string)$hook) as $plugin) {
//Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
try {
$plugin->$method(...$args);
} catch (Exception $ex) {
user_error($ex, E_USER_WARNING);
} catch (Error $err) {
user_error($err, E_USER_WARNING);
}
}
}
/**
* @param PluginHost::HOOK_* $hook
* @param mixed $args
* @param mixed $check
*/
function run_hooks_until(string $hook, $check, ...$args): bool {
$method = strtolower((string)$hook);
foreach ($this->get_hooks((string)$hook) as $plugin) {
try {
$result = $plugin->$method(...$args);
if ($result == $check)
return true;
} catch (Exception $ex) {
user_error($ex, E_USER_WARNING);
} catch (Error $err) {
user_error($err, E_USER_WARNING);
}
}
return false;
}
/**
* @param PluginHost::HOOK_* $hook
* @param mixed $args
*/
function run_hooks_callback(string $hook, Closure $callback, ...$args): void {
$method = strtolower((string)$hook);
foreach ($this->get_hooks((string)$hook) as $plugin) {
//Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
try {
if ($callback($plugin->$method(...$args), $plugin))
break;
} catch (Exception $ex) {
user_error($ex, E_USER_WARNING);
} catch (Error $err) {
user_error($err, E_USER_WARNING);
}
}
}
/**
* @param PluginHost::HOOK_* $hook
* @param mixed $args
*/
function chain_hooks_callback(string $hook, Closure $callback, &...$args): void {
$method = strtolower((string)$hook);
foreach ($this->get_hooks((string)$hook) as $plugin) {
try {
if ($callback($plugin->$method(...$args), $plugin))
break;
} catch (Exception $ex) {
user_error($ex, E_USER_WARNING);
} catch (Error $err) {
user_error($err, E_USER_WARNING);
}
}
}
/**
* @param PluginHost::HOOK_* $type
*/
function add_hook(string $type, Plugin $sender, int $priority = 50): void {
$priority = (int) $priority;
if (!method_exists($sender, strtolower((string)$type))) {
user_error(
sprintf("Plugin %s tried to register a hook without implementation: %s",
get_class($sender), $type),
E_USER_WARNING
);
return;
}
if (empty($this->hooks[$type])) {
$this->hooks[$type] = [];
}
if (empty($this->hooks[$type][$priority])) {
$this->hooks[$type][$priority] = [];
}
array_push($this->hooks[$type][$priority], $sender);
ksort($this->hooks[$type]);
}
/**
* @param PluginHost::HOOK_* $type
*/
function del_hook(string $type, Plugin $sender): void {
if (array_key_exists($type, $this->hooks)) {
foreach (array_keys($this->hooks[$type]) as $prio) {
$key = array_search($sender, $this->hooks[$type][$prio]);
if ($key !== false) {
unset($this->hooks[$type][$prio][$key]);
}
}
}
}
/**
* @param PluginHost::HOOK_* $type
* @return array<int, Plugin>
*/
function get_hooks(string $type) {
if (isset($this->hooks[$type])) {
$tmp = [];
foreach (array_keys($this->hooks[$type]) as $prio) {
array_push($tmp, ...$this->hooks[$type][$prio]);
}
return $tmp;
}
return [];
}
/**
* @param PluginHost::KIND_* $kind
*/
function load_all(int $kind, ?int $owner_uid = null, bool $skip_init = false): void {
$plugins = [...(glob("plugins/*") ?: []), ...(glob("plugins.local/*") ?: [])];
$plugins = array_filter($plugins, "is_dir");
$plugins = array_map("basename", $plugins);
asort($plugins);
$this->load(join(",", $plugins), (int)$kind, $owner_uid, $skip_init);
}
/**
* @param PluginHost::KIND_* $kind
*/
function load(string $classlist, int $kind, ?int $owner_uid = null, bool $skip_init = false): void {
$plugins = explode(",", $classlist);
$this->owner_uid = (int) $owner_uid;
if ($this->owner_uid) {
$this->set_scheduler_name("PluginHost Scheduler for UID $owner_uid");
}
foreach ($plugins as $class) {
$class = trim($class);
$class_file = strtolower(basename(clean($class)));
// try system plugin directory first
$file = Config::get_self_dir() . "/plugins/$class_file/init.php";
if (!file_exists($file)) {
$file = Config::get_self_dir() . "/plugins.local/$class_file/init.php";
if (!file_exists($file)) {
continue;
}
}
if (!isset($this->plugins[$class])) {
// WIP hack
// we can't catch incompatible method signatures via Throwable
// this also enables global tt-rss safe mode in case there are more plugins like this
if (!getenv('TTRSS_XDEBUG_ENABLED') && ($_SESSION["plugin_blacklist"][$class] ?? 0)) {
// only report once per-plugin per-session
if ($_SESSION["plugin_blacklist"][$class] < 2) {
user_error("Plugin $class has caused a PHP fatal error so it won't be loaded again in this session.", E_USER_WARNING);
$_SESSION["plugin_blacklist"][$class] = 2;
}
$_SESSION["safe_mode"] = 1;
continue;
}
try {
$_SESSION["plugin_blacklist"][$class] = 1;
require_once $file;
unset($_SESSION["plugin_blacklist"][$class]);
} catch (Error $err) {
user_error($err, E_USER_WARNING);
continue;
}
if (class_exists($class) && is_subclass_of($class, "Plugin")) {
$plugin = new $class($this);
$plugin_api = $plugin->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);
continue;
}
if (file_exists(dirname($file) . "/locale")) {
_bindtextdomain($class, dirname($file) . "/locale");
_bind_textdomain_codeset($class, "UTF-8");
}
try {
switch ($kind) {
case $this::KIND_SYSTEM:
if ($this->is_system($plugin)) {
if (!$skip_init) $plugin->init($this);
$this->register_plugin($class, $plugin);
}
break;
case $this::KIND_USER:
if (!$this->is_system($plugin)) {
if (!$skip_init) $plugin->init($this);
$this->register_plugin($class, $plugin);
}
break;
case $this::KIND_ALL:
if (!$skip_init) $plugin->init($this);
$this->register_plugin($class, $plugin);
break;
}
} catch (Exception $ex) {
user_error($ex, E_USER_WARNING);
} catch (Error $err) {
user_error($err, E_USER_WARNING);
}
}
}
}
$this->load_data();
}
function is_system(Plugin $plugin): bool {
$about = $plugin->about();
return ($about[3] ?? false) === true;
}
// only system plugins are allowed to modify routing
function add_handler(string $handler, string $method, Plugin $sender): void {
$handler = str_replace("-", "_", strtolower($handler));
$method = strtolower($method);
if ($this->is_system($sender)) {
$this->handlers[$handler] ??= [];
$this->handlers[$handler][$method] = $sender;
}
}
function del_handler(string $handler, string $method, Plugin $sender): void {
$handler = str_replace("-", "_", strtolower($handler));
$method = strtolower($method);
if ($this->is_system($sender)) {
unset($this->handlers[$handler][$method]);
}
}
/**
* @return false|Plugin false if the handler couldn't be found, otherwise the Plugin/handler
*/
function lookup_handler(string $handler, string $method): false|Plugin {
$handler = str_replace("-", "_", strtolower($handler));
$method = strtolower($method);
if (isset($this->handlers[$handler])) {
return $this->handlers[$handler]["*"] ?? $this->handlers[$handler][$method];
}
return false;
}
function add_command(string $command, string $description, Plugin $sender, string $suffix = "", string $arghelp = ""): void {
$command = str_replace("-", "_", strtolower($command));
$this->commands[$command] = array("description" => $description,
"suffix" => $suffix,
"arghelp" => $arghelp,
"class" => $sender);
}
function del_command(string $command): void {
$command = "-" . strtolower($command);
unset($this->commands[$command]);
}
/**
* @return false|Plugin false if the command couldn't be found, otherwise the registered Plugin
*/
function lookup_command(string $command): false|Plugin {
$command = "-" . strtolower($command);
if (array_key_exists($command, $this->commands)) {
return $this->commands[$command]["class"];
} else {
return false;
}
}
/** @return array<string, array{'description': string, 'suffix': string, 'arghelp': string, 'class': Plugin}> command type -> details array */
function get_commands() {
return $this->commands;
}
/**
* @param array<string, mixed> $args
*/
function run_commands(array $args): void {
foreach ($this->get_commands() as $command => $data) {
if (isset($args[$command])) {
$command = str_replace("-", "", $command);
$data["class"]->$command($args);
}
}
}
private function load_data(): void {
if ($this->owner_uid && !$this->data_loaded && Config::get_schema_version() > 100) {
$sth = $this->pdo->prepare("SELECT name, content FROM ttrss_plugin_storage
WHERE owner_uid = ?");
$sth->execute([$this->owner_uid]);
while ($line = $sth->fetch()) {
$this->storage[$line["name"]] = unserialize($line["content"]);
}
$this->data_loaded = true;
}
}
private function save_data(string $plugin): void {
if ($this->owner_uid) {
if (!$this->pdo_data)
$this->pdo_data = Db::instance()->pdo_connect();
$this->pdo_data->beginTransaction();
$sth = $this->pdo_data->prepare("SELECT id FROM ttrss_plugin_storage WHERE
owner_uid= ? AND name = ?");
$sth->execute([$this->owner_uid, $plugin]);
$this->storage[$plugin] ??= [];
$content = serialize($this->storage[$plugin]);
if ($sth->fetch()) {
$sth = $this->pdo_data->prepare("UPDATE ttrss_plugin_storage SET content = ?
WHERE owner_uid= ? AND name = ?");
$sth->execute([$content, $this->owner_uid, $plugin]);
} else {
$sth = $this->pdo_data->prepare("INSERT INTO ttrss_plugin_storage
(name,owner_uid,content) VALUES
(?, ?, ?)");
$sth->execute([$plugin, $this->owner_uid, $content]);
}
$this->pdo_data->commit();
}
}
/**
* same as set(), but sets data to current preference profile
*
* @param mixed $value
*/
function profile_set(Plugin $sender, string $name, $value): void {
$profile_id = $_SESSION["profile"] ?? null;
if ($profile_id) {
$idx = get_class($sender);
$this->storage[$idx] ??= [];
$this->storage[$idx][$profile_id] ??= [];
$this->storage[$idx][$profile_id][$name] = $value;
$this->save_data(get_class($sender));
} else {
$this->set($sender, $name, $value);
}
}
/**
* @param mixed $value
*/
function set(Plugin $sender, string $name, $value): void {
$idx = get_class($sender);
$this->storage[$idx] ??= [];
$this->storage[$idx][$name] = $value;
$this->save_data(get_class($sender));
}
/**
* @param array<int|string, mixed> $params
*/
function set_array(Plugin $sender, array $params): void {
$idx = get_class($sender);
$this->storage[$idx] ??= [];
foreach ($params as $name => $value)
$this->storage[$idx][$name] = $value;
$this->save_data(get_class($sender));
}
/**
* same as get(), but sets data to current preference profile
*
* @param mixed $default_value
* @return mixed
*/
function profile_get(Plugin $sender, string $name, $default_value = false) {
$profile_id = $_SESSION["profile"] ?? null;
if ($profile_id) {
$idx = get_class($sender);
$this->load_data();
return $this->storage[$idx][$profile_id][$name] ?? $default_value;
} else {
return $this->get($sender, $name, $default_value);
}
}
/**
* @param mixed $default_value
* @return mixed
*/
function get(Plugin $sender, string $name, $default_value = false) {
$idx = get_class($sender);
$this->load_data();
return $this->storage[$idx][$name] ?? $default_value;
}
/**
* @param array<int|string, mixed> $default_value
* @return array<int|string, mixed>
*/
function get_array(Plugin $sender, string $name, array $default_value = []): array {
$tmp = $this->get($sender, $name);
if (!is_array($tmp)) $tmp = $default_value;
return $tmp;
}
/**
* @return array<string, mixed>
*/
function get_all(Plugin $sender) {
$idx = get_class($sender);
return $this->storage[$idx] ?? [];
}
function clear_data(Plugin $sender): void {
if ($this->owner_uid) {
$idx = get_class($sender);
unset($this->storage[$idx]);
$sth = $this->pdo->prepare("DELETE FROM ttrss_plugin_storage WHERE name = ?
AND owner_uid = ?");
$sth->execute([$idx, $this->owner_uid]);
}
}
/**
* 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;
$id = count($this->special_feeds);
$this->special_feeds[] = [
'id' => $id,
'title' => $title,
'sender' => $sender,
'icon' => $icon,
];
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}>
*/
function get_feeds(int $cat_id) {
return $cat_id === Feeds::CATEGORY_SPECIAL ? $this->special_feeds : [];
}
/**
* 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 {
foreach ($this->special_feeds as $feed) {
if ($feed['id'] == $pfeed_id) {
/** @var Plugin&IVirtualFeed $feed['sender'] */
return implements_interface($feed['sender'], 'IVirtualFeed') ? $feed['sender'] : null;
}
}
return null;
}
static function pfeed_to_feed_id(int $pfeed): int {
return PLUGIN_FEED_BASE_INDEX - 1 - abs($pfeed);
}
static function feed_to_pfeed_id(int $feed): int {
return PLUGIN_FEED_BASE_INDEX - 1 + abs($feed);
}
function add_api_method(string $name, Plugin $sender): void {
if ($this->is_system($sender)) {
$this->api_methods[strtolower($name)] = $sender;
}
}
function get_api_method(string $name): ?Plugin {
return $this->api_methods[$name] ?? null;
}
function add_filter_action(Plugin $sender, string $action_name, string $action_desc): void {
$sender_class = get_class($sender);
$this->plugin_actions[$sender_class] ??= [];
$this->plugin_actions[$sender_class][] = [
"action" => $action_name,
"description" => $action_desc,
"sender" => $sender,
];
}
/**
* @return array<string, array<int, array{'action': string, 'description': string, 'sender': Plugin}>>
*/
function get_filter_actions() {
return $this->plugin_actions;
}
function get_owner_uid(): ?int {
return $this->owner_uid;
}
/**
* handled by classes/PluginHandler.php, requires valid session
*
* @param array<int|string, mixed> $params
*/
function get_method_url(Plugin $sender, string $method, array $params = []): string {
return Config::get_self_url() . "/backend.php?" .
http_build_query([
'op' => 'pluginhandler',
'plugin' => strtolower(get_class($sender)),
'method' => $method,
...$params,
]);
}
// shortcut syntax (disabled for now)
/* function get_method_url(Plugin $sender, string $method, $params) {
return Config::get_self_url() . "/backend.php?" .
http_build_query(
array_merge(
[
"op" => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method),
],
$params));
} */
/**
* WARNING: endpoint in public.php, exposed to unauthenticated users
*
* @param array<int|string, mixed> $params
*/
function get_public_method_url(Plugin $sender, string $method, array $params = []): ?string {
if ($sender->is_public_method($method)) {
return Config::get_self_url() . "/public.php?" .
http_build_query([
'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.");
return null;
}
function get_plugin_dir(Plugin $plugin): string {
$ref = new ReflectionClass(get_class($plugin));
return dirname($ref->getFileName());
}
// TODO: use get_plugin_dir()
function is_local(Plugin $plugin): bool {
$ref = new ReflectionClass(get_class($plugin));
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);
}
}

1309
classes/Pref_Feeds.php Normal file

File diff suppressed because it is too large Load Diff

1074
classes/Pref_Filters.php Normal file

File diff suppressed because it is too large Load Diff

225
classes/Pref_Labels.php Normal file
View File

@@ -0,0 +1,225 @@
<?php
class Pref_Labels extends Handler_Protected {
function csrf_ignore(string $method): bool {
$csrf_ignored = array("index", "getlabeltree");
return array_search($method, $csrf_ignored) !== false;
}
function edit(): void {
$label = ORM::for_table('ttrss_labels2')
->where('owner_uid', $_SESSION['uid'])
->find_one($_REQUEST['id']);
if ($label) {
print json_encode($label->as_array());
}
}
function getlabeltree(): void {
$root = array();
$root['id'] = 'root';
$root['name'] = __('Labels');
$root['items'] = array();
$sth = $this->pdo->prepare("SELECT *
FROM ttrss_labels2
WHERE owner_uid = ?
ORDER BY caption");
$sth->execute([$_SESSION['uid']]);
while ($line = $sth->fetch()) {
$label = array();
$label['id'] = 'LABEL:' . $line['id'];
$label['bare_id'] = $line['id'];
$label['name'] = $line['caption'];
$label['fg_color'] = $line['fg_color'];
$label['bg_color'] = $line['bg_color'];
$label['type'] = 'label';
$label['checkbox'] = false;
array_push($root['items'], $label);
}
$fl = array();
$fl['identifier'] = 'id';
$fl['label'] = 'name';
$fl['items'] = array($root);
print json_encode($fl);
}
function colorset(): void {
$kind = clean($_REQUEST["kind"]);
$ids = explode(',', clean($_REQUEST["ids"]));
$color = clean($_REQUEST["color"]);
$fg = clean($_REQUEST["fg"]);
$bg = clean($_REQUEST["bg"]);
foreach ($ids as $id) {
if ($kind == "fg" || $kind == "bg") {
$sth = $this->pdo->prepare("UPDATE ttrss_labels2 SET
{$kind}_color = ? WHERE id = ?
AND owner_uid = ?");
$sth->execute([$color, $id, $_SESSION['uid']]);
} else {
$sth = $this->pdo->prepare("UPDATE ttrss_labels2 SET
fg_color = ?, bg_color = ? WHERE id = ?
AND owner_uid = ?");
$sth->execute([$fg, $bg, $id, $_SESSION['uid']]);
}
/* Remove cached data */
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET label_cache = ''
WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
}
}
function colorreset(): void {
$ids = explode(',', clean($_REQUEST["ids"]));
foreach ($ids as $id) {
$sth = $this->pdo->prepare("UPDATE ttrss_labels2 SET
fg_color = '', bg_color = '' WHERE id = ?
AND owner_uid = ?");
$sth->execute([$id, $_SESSION['uid']]);
/* Remove cached data */
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET label_cache = ''
WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
}
}
function save(): void {
$id = clean($_REQUEST["id"]);
$caption = clean($_REQUEST["caption"]);
$this->pdo->beginTransaction();
$sth = $this->pdo->prepare("SELECT caption FROM ttrss_labels2
WHERE id = ? AND owner_uid = ?");
$sth->execute([$id, $_SESSION['uid']]);
if ($row = $sth->fetch()) {
$old_caption = $row["caption"];
$sth = $this->pdo->prepare("SELECT id FROM ttrss_labels2
WHERE caption = ? AND owner_uid = ?");
$sth->execute([$caption, $_SESSION['uid']]);
if (!$sth->fetch()) {
if ($caption) {
$sth = $this->pdo->prepare("UPDATE ttrss_labels2 SET
caption = ? WHERE id = ? AND
owner_uid = ?");
$sth->execute([$caption, $id, $_SESSION['uid']]);
/* Update filters that reference label being renamed */
$sth = $this->pdo->prepare("UPDATE ttrss_filters2_actions SET
action_param = ? WHERE action_param = ?
AND action_id = 7
AND filter_id IN (SELECT id FROM ttrss_filters2 WHERE owner_uid = ?)");
$sth->execute([$caption, $old_caption, $_SESSION['uid']]);
print clean($_REQUEST["caption"]);
} else {
print $old_caption;
}
} else {
print $old_caption;
}
}
$this->pdo->commit();
}
function remove(): void {
/** @var array<int, int> */
$ids = array_map("intval", explode(",", clean($_REQUEST["ids"])));
foreach ($ids as $id) {
Labels::remove($id, $_SESSION["uid"]);
}
}
function add(): void {
$caption = clean($_REQUEST["caption"]);
$output = clean($_REQUEST["output"] ?? false);
if ($caption) {
if (Labels::create($caption)) {
if (!$output) {
print T_sprintf("Created label <b>%s</b>", htmlspecialchars($caption));
}
}
}
}
function index(): void {
?>
<div dojoType='dijit.layout.BorderContainer' gutters='false'>
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>
<div dojoType='fox.Toolbar'>
<div dojoType='fox.form.DropDownButton'>
<span><?= __('Select') ?></span>
<div dojoType='dijit.Menu' style='display: none'>
<div onclick="dijit.byId('labelTree').model.setAllChecked(true)"
dojoType='dijit.MenuItem'><?=('All') ?></div>
<div onclick="dijit.byId('labelTree').model.setAllChecked(false)"
dojoType='dijit.MenuItem'><?=('None') ?></div>
</div>
</div>
<button dojoType='dijit.form.Button' onclick='CommonDialogs.addLabel()'>
<?=('Create label') ?></button dojoType='dijit.form.Button'>
<button dojoType='dijit.form.Button' onclick="dijit.byId('labelTree').removeSelected()">
<?=('Remove') ?></button dojoType='dijit.form.Button'>
<button dojoType='dijit.form.Button' onclick="dijit.byId('labelTree').resetColors()">
<?=('Clear colors') ?></button dojoType='dijit.form.Button'>
</div>
</div>
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
<div dojoType='dojo.data.ItemFileWriteStore' jsId='labelStore'
url='backend.php?op=Pref_Labels&method=getlabeltree'>
</div>
<div dojoType='lib.CheckBoxStoreModel' jsId='labelModel' store='labelStore'
query="{id:'root'}" rootId='root'
childrenAttrs='items' checkboxStrict='false' checkboxAll='false'>
</div>
<div dojoType='fox.PrefLabelTree' id='labelTree' model='labelModel' openOnClick='true'>
<script type='dojo/method' event='onClick' args='item'>
var id = String(item.id);
var bare_id = id.substr(id.indexOf(':')+1);
if (id.match('LABEL:')) {
dijit.byId('labelTree').editLabel(bare_id);
}
</script>
</div>
</div>
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefLabels") ?>
</div>
<?php
}
}

1606
classes/Pref_Prefs.php Normal file

File diff suppressed because it is too large Load Diff

270
classes/Pref_System.php Normal file
View File

@@ -0,0 +1,270 @@
<?php
class Pref_System extends Handler_Administrative {
private const LOG_PAGE_LIMIT = 15;
function csrf_ignore(string $method): bool {
$csrf_ignored = array("index");
return array_search($method, $csrf_ignored) !== false;
}
function clearLog(): void {
$this->pdo->query("DELETE FROM ttrss_error_log");
}
function sendTestEmail(): void {
$mail_address = clean($_REQUEST["mail_address"]);
$mailer = new Mailer();
$rc = $mailer->mail(["to_name" => "",
"to_address" => $mail_address,
"subject" => __("Test message from tt-rss"),
"message" => ("This message confirms that tt-rss can send outgoing mail.")
]);
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 {
ob_start();
phpinfo();
$info = ob_get_contents();
ob_end_clean();
print preg_replace( '%^.*<body>(.*)</body>.*$%ms','$1', (string)$info);
}
private function _log_viewer(int $page, int $severity): void {
$errno_values = match ($severity) {
E_USER_ERROR => [E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR],
E_USER_WARNING => [E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR, E_WARNING, E_USER_WARNING, E_DEPRECATED, E_USER_DEPRECATED],
default => [],
};
if (count($errno_values) > 0) {
$errno_qmarks = arr_qmarks($errno_values);
$errno_filter_qpart = "errno IN ($errno_qmarks)";
} else {
$errno_filter_qpart = "true";
}
$offset = self::LOG_PAGE_LIMIT * $page;
$sth = $this->pdo->prepare("SELECT
COUNT(id) AS total_pages
FROM
ttrss_error_log
WHERE
$errno_filter_qpart");
$sth->execute($errno_values);
if ($res = $sth->fetch()) {
$total_pages = (int)($res["total_pages"] / self::LOG_PAGE_LIMIT);
} else {
$total_pages = 0;
}
?>
<div dojoType='dijit.layout.BorderContainer' gutters='false'>
<div region='top' dojoType='fox.Toolbar'>
<button dojoType='dijit.form.Button' onclick='Helpers.EventLog.refresh()'>
<?= __('Refresh') ?>
</button>
<button dojoType='dijit.form.Button' <?= ($page <= 0 ? "disabled" : "") ?>
onclick='Helpers.EventLog.prevPage()'>
<?= __('&lt;&lt;') ?>
</button>
<button dojoType='dijit.form.Button' disabled>
<?= T_sprintf('Page %d of %d', $page+1, $total_pages+1) ?>
</button>
<button dojoType='dijit.form.Button' <?= ($page >= $total_pages ? "disabled" : "") ?>
onclick='Helpers.EventLog.nextPage()'>
<?= __('&gt;&gt;') ?>
</button>
<button dojoType='dijit.form.Button'
onclick='Helpers.EventLog.clear()'>
<?= __('Clear') ?>
</button>
<div class='pull-right'>
<label><?= __("Severity:") ?></label>
<?= \Controls\select_hash("severity", $severity,
[
E_USER_ERROR => __("Errors"),
E_USER_WARNING => __("Warnings"),
E_USER_NOTICE => __("Everything")
], ["onchange"=> "Helpers.EventLog.refresh()"], "severity") ?>
</div>
</div>
<div style="padding : 0px" dojoType="dijit.layout.ContentPane" region="center">
<table width='100%' class='event-log'>
<tr>
<th width='5%'><?= __("Error") ?></th>
<th><?= __("Filename") ?></th>
<th><?= __("Message") ?></th>
<th width='5%'><?= __("User") ?></th>
<th width='5%'><?= __("Date") ?></th>
</tr>
<?php
$sth = $this->pdo->prepare("SELECT
errno, errstr, filename, lineno, created_at, login, context
FROM
ttrss_error_log LEFT JOIN ttrss_users ON (owner_uid = ttrss_users.id)
WHERE
$errno_filter_qpart
ORDER BY
ttrss_error_log.id DESC
LIMIT ". self::LOG_PAGE_LIMIT ." OFFSET $offset");
$sth->execute($errno_values);
while ($line = $sth->fetch()) {
foreach ($line as $k => $v) { $line[$k] = htmlspecialchars($v ?? ''); }
?>
<tr>
<td class='errno'>
<?= Logger::ERROR_NAMES[$line["errno"]] . " (" . $line["errno"] . ")" ?>
</td>
<td class='filename'><?= $line["filename"] . ":" . $line["lineno"] ?></td>
<td class='errstr'><?= $line["errstr"] . "\n" . $line["context"] ?></td>
<td class='login'><?= $line["login"] ?></td>
<td class='timestamp'>
<?= TimeHelper::make_local_datetime($line['created_at']) ?>
</td>
</tr>
<?php } ?>
</table>
</div>
</div>
<?php
}
function index(): void {
$severity = (int) ($_REQUEST["severity"] ?? E_USER_WARNING);
$page = (int) ($_REQUEST["page"] ?? 0);
?>
<div dojoType='dijit.layout.AccordionContainer' region='center'>
<?php if (Config::get(Config::LOG_DESTINATION) == Logger::LOG_DEST_SQL) { ?>
<div dojoType='dijit.layout.AccordionPane' style='padding : 0' title='<i class="material-icons">report</i> <?= __('Event log') ?>'>
<?php
$this->_log_viewer($page, $severity);
?>
</div>
<?php } ?>
<div dojoType='dijit.layout.AccordionPane' style='padding : 0' title='<i class="material-icons">mail</i> <?= __('Mail configuration') ?>'>
<div dojoType="dijit.layout.ContentPane">
<form dojoType="dijit.form.Form">
<script type="dojo/method" event="onSubmit" args="evt">
evt.preventDefault();
if (this.validate()) {
xhr.json("backend.php", this.getValues(), (reply) => {
const msg = App.byId("mail-test-result");
if (reply.rc) {
msg.innerHTML = __("Mail sent.");
msg.className = 'alert alert-success';
} else {
msg.innerHTML = reply.error;
msg.className = 'alert alert-danger';
}
msg.show();
})
}
</script>
<?= \Controls\hidden_tag("op", "Pref_System") ?>
<?= \Controls\hidden_tag("method", "sendTestEmail") ?>
<?php
$user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]);
?>
<fieldset>
<label><?= __("To:") ?></label>
<?= \Controls\input_tag("mail_address",$user->email, "text", ['required' => 1]) ?>
<?= \Controls\submit_tag(__("Send test email")) ?>
<span style="display: none; margin-left : 10px" class="alert alert-error" id="mail-test-result">...</span>
</fieldset>
</form>
</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') ?>'>
<script type='dojo/method' event='onSelected' args='evt'>
if (this.domNode.querySelector('.loading'))
window.setTimeout(() => {
xhr.post("backend.php", {op: 'Pref_System', method: 'getphpinfo'}, (reply) => {
this.attr('content', `<div class='phpinfo'>${reply}</div>`);
});
}, 200);
</script>
<span class='loading'><?= __("Loading, please wait...") ?></span>
</div>
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefSystem") ?>
</div>
<?php
}
}

278
classes/Pref_Users.php Normal file
View File

@@ -0,0 +1,278 @@
<?php
class Pref_Users extends Handler_Administrative {
function csrf_ignore(string $method): bool {
return $method == "index";
}
function edit(): void {
$user = ORM::for_table('ttrss_users')
->select_many('id', 'login', 'access_level', 'email', 'full_name', 'otp_enabled')
->find_one((int)$_REQUEST["id"])
->as_array();
global $access_level_names;
if ($user) {
print json_encode([
"user" => $user,
"access_level_names" => $access_level_names
]);
}
}
function userdetails(): void {
$id = (int) clean($_REQUEST["id"]);
$user = ORM::for_table('ttrss_users')
->table_alias('u')
->select_many('u.login', 'u.access_level')
->select_many_expr([
'created' => 'SUBSTRING_FOR_DATE(u.created,1,16)',
'last_login' => 'SUBSTRING_FOR_DATE(u.last_login,1,16)',
'stored_articles' => '(SELECT COUNT(ue.int_id) FROM ttrss_user_entries ue WHERE ue.owner_uid = u.id)',
])
->find_one($id);
if ($user) {
$created = TimeHelper::make_local_datetime($user->created);
$last_login = TimeHelper::make_local_datetime($user->last_login);
$user_owned_feeds = ORM::for_table('ttrss_feeds')
->select_many('id', 'title', 'site_url')
->where('owner_uid', $id)
->order_by_expr('LOWER(title)')
->find_many();
?>
<fieldset>
<label><?= __('Registered') ?>:</label>
<?= $created ?>
</fieldset>
<fieldset>
<label><?= __('Last logged in') ?>:</label>
<?= $last_login ?>
</fieldset>
<fieldset>
<label><?= __('Subscribed feeds') ?>:</label>
<?= count($user_owned_feeds) ?>
</fieldset>
<fieldset>
<label><?= __('Stored articles') ?>:</label>
<?= $user->stored_articles ?>
</fieldset>
<ul class="panel panel-scrollable list list-unstyled">
<?php foreach ($user_owned_feeds as $feed) { ?>
<li>
<?php
$icon_url = Feeds::_get_icon_url($feed->id, 'images/blank_icon.gif');
?>
<img class="icon" src="<?= htmlspecialchars($icon_url) ?>">
<a target="_blank" href="<?= htmlspecialchars($feed->site_url) ?>">
<?= htmlspecialchars($feed->title) ?>
</a>
</li>
<?php } ?>
</ul>
<?php
} else {
print_error(__('User not found'));
}
}
function editSave(): void {
$id = (int)$_REQUEST['id'];
$password = clean($_REQUEST["password"]);
$user = ORM::for_table('ttrss_users')->find_one($id);
if ($user) {
$login = clean($_REQUEST["login"]);
if ($id == 1) $login = "admin";
if (!$login) return;
$user->login = mb_strtolower($login);
$user->access_level = (int) clean($_REQUEST["access_level"]);
$user->email = clean($_REQUEST["email"]);
$user->otp_enabled = checkbox_to_sql_bool($_REQUEST["otp_enabled"] ?? "");
// force new OTP secret when next enabled
if (Config::get_schema_version() >= 143 && !$user->otp_enabled) {
$user->otp_secret = null;
}
$user->save();
}
if ($password) {
UserHelper::reset_password($id, false, $password);
}
}
function remove(): void {
$ids = explode(",", clean($_REQUEST["ids"]));
foreach ($ids as $id) {
if ($id != $_SESSION["uid"] && $id != 1) {
ORM::for_table('ttrss_tags')->where('owner_uid', $id)->delete_many();
ORM::for_table('ttrss_feeds')->where('owner_uid', $id)->delete_many();
ORM::for_table('ttrss_users')->where('id', $id)->delete_many();
}
}
}
function add(): void {
$login = clean($_REQUEST["login"]);
if (!$login) return; // no blank usernames
if (!UserHelper::find_user_by_login($login)) {
$new_password = make_password();
$user = ORM::for_table('ttrss_users')->create();
$user->salt = UserHelper::get_salt();
$user->login = mb_strtolower($login);
$user->pwd_hash = UserHelper::hash_password($new_password, $user->salt);
$user->access_level = 0;
$user->created = Db::NOW();
$user->save();
if (!is_null(UserHelper::find_user_by_login($login))) {
print T_sprintf("Added user %s with password %s",
$login, $new_password);
} else {
print T_sprintf("Could not create user %s", $login);
}
} else {
print T_sprintf("User %s already exists.", $login);
}
}
function resetPass(): void {
UserHelper::reset_password(clean($_REQUEST["id"]));
}
function index(): void {
global $access_level_names;
$user_search = clean($_REQUEST["search"] ?? "");
if (array_key_exists("search", $_REQUEST)) {
$_SESSION["prefs_user_search"] = $user_search;
} else {
$user_search = ($_SESSION["prefs_user_search"] ?? "");
}
$sort = clean($_REQUEST["sort"] ?? "");
if (!$sort || $sort == "undefined") {
$sort = "login";
}
if (!in_array($sort, ["login", "access_level", "created", "num_feeds", "created", "last_login"]))
$sort = "login";
if ($sort != "login") $sort = "$sort DESC";
?>
<div dojoType='dijit.layout.BorderContainer' gutters='false'>
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>
<div dojoType='fox.Toolbar'>
<div style='float : right'>
<form dojoType="dijit.form.Form" onsubmit="Users.reload(); return false;">
<input dojoType='dijit.form.TextBox' id='user_search' size='20' type='search'
value="<?= htmlspecialchars($user_search) ?>">
<button dojoType='dijit.form.Button' type='submit'>
<?= __('Search') ?>
</button>
</form>
</div>
<div dojoType='fox.form.DropDownButton'>
<span><?= __('Select') ?></span>
<div dojoType='dijit.Menu' style='display: none'>
<div onclick="Tables.select('users-list', true)"
dojoType='dijit.MenuItem'><?= __('All') ?></div>
<div onclick="Tables.select('users-list', false)"
dojoType='dijit.MenuItem'><?= __('None') ?></div>
</div>
</div>
<button dojoType='dijit.form.Button' onclick='Users.add()'>
<?= __('Create user') ?>
</button>
<button dojoType='dijit.form.Button' onclick='Users.removeSelected()'>
<?= __('Remove') ?>
</button>
<button dojoType='dijit.form.Button' onclick='Users.resetSelected()'>
<?= __('Reset password') ?>
</button>
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefUsersToolbar") ?>
</div>
</div>
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>
<table width='100%' class='users-list' id='users-list'>
<tr>
<th></th>
<th><a href='#' onclick="Users.reload('login')"><?= __('Login') ?></a></th>
<th><a href='#' onclick="Users.reload('access_level')"><?= __('Access level') ?></a></th>
<th><a href='#' onclick="Users.reload('num_feeds')"><?= __('Subscribed feeds') ?></a></th>
<th><a href='#' onclick="Users.reload('created')"><?= __('Registered') ?></a></th>
<th><a href='#' onclick="Users.reload('last_login')"><?= __('Last login') ?></a></th>
</tr>
<?php
$users = ORM::for_table('ttrss_users')
->table_alias('u')
->left_outer_join("ttrss_feeds", ["owner_uid", "=", "u.id"], 'f')
->select_expr('u.*,COUNT(f.id) AS num_feeds')
->where_like("login", $user_search ? "%$user_search%" : "%")
->order_by_expr($sort)
->group_by_expr('u.id')
->find_many();
foreach ($users as $user) { ?>
<tr data-row-id='<?= $user["id"] ?>' onclick='Users.edit(<?= $user["id"] ?>)' title="<?= __('Click to edit') ?>">
<td class='checkbox'>
<input onclick='Tables.onRowChecked(this); event.stopPropagation();'
dojoType='dijit.form.CheckBox' type='checkbox'>
</td>
<td width='30%'>
<i class='material-icons'>person</i>
<strong><?= htmlspecialchars($user["login"]) ?></strong>
</td>
<td><?= $access_level_names[$user["access_level"]] ?></td>
<td><?= $user["num_feeds"] ?></td>
<td class='text-muted'><?= TimeHelper::make_local_datetime($user['created']) ?></td>
<td class='text-muted'><?= TimeHelper::make_local_datetime($user['last_login']) ?></td>
</tr>
<?php } ?>
</table>
</div>
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefUsers") ?>
</div>
<?php
}
}

405
classes/Prefs.php Normal file
View File

@@ -0,0 +1,405 @@
<?php
class Prefs {
// (this is the database-backed version of Config.php)
const PURGE_OLD_DAYS = "PURGE_OLD_DAYS";
const DEFAULT_UPDATE_INTERVAL = "DEFAULT_UPDATE_INTERVAL";
//const DEFAULT_ARTICLE_LIMIT = "DEFAULT_ARTICLE_LIMIT";
//const ALLOW_DUPLICATE_POSTS = "ALLOW_DUPLICATE_POSTS";
const ENABLE_FEED_CATS = "ENABLE_FEED_CATS";
const SHOW_CONTENT_PREVIEW = "SHOW_CONTENT_PREVIEW";
const SHORT_DATE_FORMAT = "SHORT_DATE_FORMAT";
const LONG_DATE_FORMAT = "LONG_DATE_FORMAT";
const COMBINED_DISPLAY_MODE = "COMBINED_DISPLAY_MODE";
const HIDE_READ_FEEDS = "HIDE_READ_FEEDS";
const ON_CATCHUP_SHOW_NEXT_FEED = "ON_CATCHUP_SHOW_NEXT_FEED";
const FEEDS_SORT_BY_UNREAD = "FEEDS_SORT_BY_UNREAD";
const REVERSE_HEADLINES = "REVERSE_HEADLINES";
const DIGEST_ENABLE = "DIGEST_ENABLE";
const CONFIRM_FEED_CATCHUP = "CONFIRM_FEED_CATCHUP";
const CDM_AUTO_CATCHUP = "CDM_AUTO_CATCHUP";
const _DEFAULT_VIEW_MODE = "_DEFAULT_VIEW_MODE";
const _DEFAULT_VIEW_LIMIT = "_DEFAULT_VIEW_LIMIT";
//const _PREFS_ACTIVE_TAB = "_PREFS_ACTIVE_TAB";
//const STRIP_UNSAFE_TAGS = "STRIP_UNSAFE_TAGS";
const BLACKLISTED_TAGS = "BLACKLISTED_TAGS";
const FRESH_ARTICLE_MAX_AGE = "FRESH_ARTICLE_MAX_AGE";
const DIGEST_CATCHUP = "DIGEST_CATCHUP";
const CDM_EXPANDED = "CDM_EXPANDED";
const PURGE_UNREAD_ARTICLES = "PURGE_UNREAD_ARTICLES";
const HIDE_READ_SHOWS_SPECIAL = "HIDE_READ_SHOWS_SPECIAL";
const VFEED_GROUP_BY_FEED = "VFEED_GROUP_BY_FEED";
const STRIP_IMAGES = "STRIP_IMAGES";
const _DEFAULT_VIEW_ORDER_BY = "_DEFAULT_VIEW_ORDER_BY";
const ENABLE_API_ACCESS = "ENABLE_API_ACCESS";
//const _COLLAPSED_SPECIAL = "_COLLAPSED_SPECIAL";
//const _COLLAPSED_LABELS = "_COLLAPSED_LABELS";
//const _COLLAPSED_UNCAT = "_COLLAPSED_UNCAT";
//const _COLLAPSED_FEEDLIST = "_COLLAPSED_FEEDLIST";
//const _MOBILE_ENABLE_CATS = "_MOBILE_ENABLE_CATS";
//const _MOBILE_SHOW_IMAGES = "_MOBILE_SHOW_IMAGES";
//const _MOBILE_HIDE_READ = "_MOBILE_HIDE_READ";
//const _MOBILE_SORT_FEEDS_UNREAD = "_MOBILE_SORT_FEEDS_UNREAD";
//const _MOBILE_BROWSE_CATS = "_MOBILE_BROWSE_CATS";
//const _THEME_ID = "_THEME_ID";
const USER_TIMEZONE = "USER_TIMEZONE";
const USER_STYLESHEET = "USER_STYLESHEET";
//const SORT_HEADLINES_BY_FEED_DATE = "SORT_HEADLINES_BY_FEED_DATE";
const SSL_CERT_SERIAL = "SSL_CERT_SERIAL";
const DIGEST_PREFERRED_TIME = "DIGEST_PREFERRED_TIME";
//const _PREFS_SHOW_EMPTY_CATS = "_PREFS_SHOW_EMPTY_CATS";
const _DEFAULT_INCLUDE_CHILDREN = "_DEFAULT_INCLUDE_CHILDREN";
//const AUTO_ASSIGN_LABELS = "AUTO_ASSIGN_LABELS";
const _ENABLED_PLUGINS = "_ENABLED_PLUGINS";
//const _MOBILE_REVERSE_HEADLINES = "_MOBILE_REVERSE_HEADLINES";
const USER_CSS_THEME = "USER_CSS_THEME";
const USER_LANGUAGE = "USER_LANGUAGE";
const DEFAULT_SEARCH_LANGUAGE = "DEFAULT_SEARCH_LANGUAGE";
const _PREFS_MIGRATED = "_PREFS_MIGRATED";
const HEADLINES_NO_DISTINCT = "HEADLINES_NO_DISTINCT";
const DEBUG_HEADLINE_IDS = "DEBUG_HEADLINE_IDS";
const DISABLE_CONDITIONAL_COUNTERS = "DISABLE_CONDITIONAL_COUNTERS";
const WIDESCREEN_MODE = "WIDESCREEN_MODE";
const CDM_ENABLE_GRID = "CDM_ENABLE_GRID";
const DIGEST_MIN_SCORE = "DIGEST_MIN_SCORE";
private const _DEFAULTS = [
Prefs::PURGE_OLD_DAYS => [ 60, Config::T_INT ],
Prefs::DEFAULT_UPDATE_INTERVAL => [ 30, Config::T_INT ],
//Prefs::DEFAULT_ARTICLE_LIMIT => [ 30, Config::T_INT ],
//Prefs::ALLOW_DUPLICATE_POSTS => [ false, Config::T_BOOL ],
Prefs::ENABLE_FEED_CATS => [ true, Config::T_BOOL ],
Prefs::SHOW_CONTENT_PREVIEW => [ true, Config::T_BOOL ],
Prefs::SHORT_DATE_FORMAT => [ "M d, G:i", Config::T_STRING ],
Prefs::LONG_DATE_FORMAT => [ "D, M d Y - G:i", Config::T_STRING ],
Prefs::COMBINED_DISPLAY_MODE => [ true, Config::T_BOOL ],
Prefs::HIDE_READ_FEEDS => [ false, Config::T_BOOL ],
Prefs::ON_CATCHUP_SHOW_NEXT_FEED => [ false, Config::T_BOOL ],
Prefs::FEEDS_SORT_BY_UNREAD => [ false, Config::T_BOOL ],
Prefs::REVERSE_HEADLINES => [ false, Config::T_BOOL ],
Prefs::DIGEST_ENABLE => [ false, Config::T_BOOL ],
Prefs::CONFIRM_FEED_CATCHUP => [ true, Config::T_BOOL ],
Prefs::CDM_AUTO_CATCHUP => [ false, Config::T_BOOL ],
Prefs::_DEFAULT_VIEW_MODE => [ "adaptive", Config::T_STRING ],
Prefs::_DEFAULT_VIEW_LIMIT => [ 30, Config::T_INT ],
//Prefs::_PREFS_ACTIVE_TAB => [ "", Config::T_STRING ],
//Prefs::STRIP_UNSAFE_TAGS => [ true, Config::T_BOOL ],
Prefs::BLACKLISTED_TAGS => [ 'main, generic, misc, uncategorized, blog, blogroll, general, news', Config::T_STRING ],
Prefs::FRESH_ARTICLE_MAX_AGE => [ 24, Config::T_INT ],
Prefs::DIGEST_CATCHUP => [ false, Config::T_BOOL ],
Prefs::CDM_EXPANDED => [ true, Config::T_BOOL ],
Prefs::PURGE_UNREAD_ARTICLES => [ true, Config::T_BOOL ],
Prefs::HIDE_READ_SHOWS_SPECIAL => [ true, Config::T_BOOL ],
Prefs::VFEED_GROUP_BY_FEED => [ false, Config::T_BOOL ],
Prefs::STRIP_IMAGES => [ false, Config::T_BOOL ],
Prefs::_DEFAULT_VIEW_ORDER_BY => [ "default", Config::T_STRING ],
Prefs::ENABLE_API_ACCESS => [ false, Config::T_BOOL ],
//Prefs::_COLLAPSED_SPECIAL => [ false, Config::T_BOOL ],
//Prefs::_COLLAPSED_LABELS => [ false, Config::T_BOOL ],
//Prefs::_COLLAPSED_UNCAT => [ false, Config::T_BOOL ],
//Prefs::_COLLAPSED_FEEDLIST => [ false, Config::T_BOOL ],
//Prefs::_MOBILE_ENABLE_CATS => [ false, Config::T_BOOL ],
//Prefs::_MOBILE_SHOW_IMAGES => [ false, Config::T_BOOL ],
//Prefs::_MOBILE_HIDE_READ => [ false, Config::T_BOOL ],
//Prefs::_MOBILE_SORT_FEEDS_UNREAD => [ false, Config::T_BOOL ],
//Prefs::_MOBILE_BROWSE_CATS => [ true, Config::T_BOOL ],
//Prefs::_THEME_ID => [ 0, Config::T_BOOL ],
Prefs::USER_TIMEZONE => [ "Automatic", Config::T_STRING ],
Prefs::USER_STYLESHEET => [ "", Config::T_STRING ],
//Prefs::SORT_HEADLINES_BY_FEED_DATE => [ false, Config::T_BOOL ],
Prefs::SSL_CERT_SERIAL => [ "", Config::T_STRING ],
Prefs::DIGEST_PREFERRED_TIME => [ "00:00", Config::T_STRING ],
//Prefs::_PREFS_SHOW_EMPTY_CATS => [ false, Config::T_BOOL ],
Prefs::_DEFAULT_INCLUDE_CHILDREN => [ false, Config::T_BOOL ],
//Prefs::AUTO_ASSIGN_LABELS => [ false, Config::T_BOOL ],
Prefs::_ENABLED_PLUGINS => [ "", Config::T_STRING ],
//Prefs::_MOBILE_REVERSE_HEADLINES => [ false, Config::T_BOOL ],
Prefs::USER_CSS_THEME => [ "" , Config::T_STRING ],
Prefs::USER_LANGUAGE => [ "" , Config::T_STRING ],
Prefs::DEFAULT_SEARCH_LANGUAGE => [ "" , Config::T_STRING ],
Prefs::_PREFS_MIGRATED => [ false, Config::T_BOOL ],
Prefs::HEADLINES_NO_DISTINCT => [ false, Config::T_BOOL ],
Prefs::DEBUG_HEADLINE_IDS => [ false, Config::T_BOOL ],
Prefs::DISABLE_CONDITIONAL_COUNTERS => [ false, Config::T_BOOL ],
Prefs::WIDESCREEN_MODE => [ false, Config::T_BOOL ],
Prefs::CDM_ENABLE_GRID => [ false, Config::T_BOOL ],
Prefs::DIGEST_MIN_SCORE => [ 0, Config::T_INT ],
];
const _PROFILE_BLACKLIST = [
//Prefs::ALLOW_DUPLICATE_POSTS,
Prefs::PURGE_OLD_DAYS,
Prefs::PURGE_UNREAD_ARTICLES,
Prefs::DIGEST_ENABLE,
Prefs::DIGEST_CATCHUP,
Prefs::BLACKLISTED_TAGS,
Prefs::ENABLE_API_ACCESS,
//Prefs::UPDATE_POST_ON_CHECKSUM_CHANGE,
Prefs::DEFAULT_UPDATE_INTERVAL,
Prefs::USER_TIMEZONE,
//Prefs::SORT_HEADLINES_BY_FEED_DATE,
Prefs::SSL_CERT_SERIAL,
Prefs::DIGEST_PREFERRED_TIME,
Prefs::DIGEST_MIN_SCORE,
Prefs::_PREFS_MIGRATED
];
private static ?Prefs $instance = null;
/** @var array<string, bool|int|string> */
private array $cache = [];
private ?PDO $pdo = null;
public static function get_instance() : Prefs {
if (self::$instance == null)
self::$instance = new self();
return self::$instance;
}
static function is_valid(string $pref_name): bool {
return isset(self::_DEFAULTS[$pref_name]);
}
static function get_default(string $pref_name): bool|int|null|string {
if (self::is_valid($pref_name))
return self::_DEFAULTS[$pref_name][0];
else
return null;
}
function __construct() {
$this->pdo = Db::pdo();
if (!empty($_SESSION["uid"])) {
$owner_uid = (int) $_SESSION["uid"];
$profile_id = $_SESSION["profile"] ?? null;
$this->cache_all($owner_uid, $profile_id);
$this->migrate($owner_uid, $profile_id);
};
}
private function __clone() {
//
}
/**
* @return array<int, array<string, bool|int|null|string>>
*/
static function get_all(int $owner_uid, ?int $profile_id = null): array {
return self::get_instance()->_get_all($owner_uid, $profile_id);
}
/**
* @return array<int, array<string, bool|int|null|string>>
*/
private function _get_all(int $owner_uid, ?int $profile_id = null): array {
$rv = [];
$ref = new ReflectionClass(get_class($this));
foreach ($ref->getConstants() as $const => $cvalue) {
if (isset($this::_DEFAULTS[$const])) {
list ($def_val, $type_hint) = $this::_DEFAULTS[$const];
array_push($rv, [
"pref_name" => $const,
"value" => $this->_get($const, $owner_uid, $profile_id),
"type_hint" => $type_hint,
]);
}
}
return $rv;
}
private function cache_all(int $owner_uid, ?int $profile_id): void {
if (!$profile_id) $profile_id = null;
// fill cache with defaults
$ref = new ReflectionClass(get_class($this));
foreach ($ref->getConstants() as $const => $cvalue) {
if (isset($this::_DEFAULTS[$const])) {
list ($def_val, $type_hint) = $this::_DEFAULTS[$const];
$this->_set_cache($const, $def_val, $owner_uid, $profile_id);
}
}
if (Config::get_schema_version() >= 141) {
// fill in any overrides from the database
$sth = $this->pdo->prepare("SELECT pref_name, value FROM ttrss_user_prefs2
WHERE owner_uid = :uid AND
(profile = :profile OR (:profile IS NULL AND profile IS NULL))");
$sth->execute(["uid" => $owner_uid, "profile" => $profile_id]);
while ($row = $sth->fetch()) {
$this->_set_cache($row["pref_name"], $row["value"], $owner_uid, $profile_id);
}
}
}
static function get(string $pref_name, int $owner_uid, ?int $profile_id = null): bool|int|null|string {
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 {
if (isset(self::_DEFAULTS[$pref_name])) {
if (!$profile_id || in_array($pref_name, self::_PROFILE_BLACKLIST)) $profile_id = null;
list ($def_val, $type_hint) = self::_DEFAULTS[$pref_name];
if ($this->_is_cached($pref_name, $owner_uid, $profile_id)) {
$cached_value = $this->_get_cache($pref_name, $owner_uid, $profile_id);
return Config::cast_to($cached_value, $type_hint);
} else if (Config::get_schema_version() >= 141) {
$sth = $this->pdo->prepare("SELECT value FROM ttrss_user_prefs2
WHERE pref_name = :name AND owner_uid = :uid AND
(profile = :profile OR (:profile IS NULL AND profile IS NULL))");
$sth->execute(["uid" => $owner_uid, "profile" => $profile_id, "name" => $pref_name ]);
if ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
$this->_set_cache($pref_name, $row["value"], $owner_uid, $profile_id);
return Config::cast_to($row["value"], $type_hint);
} else {
$this->_set_cache($pref_name, $def_val, $owner_uid, $profile_id);
return $def_val;
}
} else {
return Config::cast_to($def_val, $type_hint);
}
} else {
user_error("Attempt to get invalid preference key: $pref_name (UID: $owner_uid, profile: $profile_id)", E_USER_WARNING);
}
return null;
}
private function _is_cached(string $pref_name, int $owner_uid, ?int $profile_id): bool {
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
return isset($this->cache[$cache_key]);
}
private function _get_cache(string $pref_name, int $owner_uid, ?int $profile_id): bool|int|null|string {
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
return $this->cache[$cache_key] ?? null;
}
private function _set_cache(string $pref_name, bool|int|string $value, int $owner_uid, ?int $profile_id): void {
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
$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 {
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 {
if (!$profile_id) $profile_id = null;
if ($profile_id && in_array($pref_name, self::_PROFILE_BLACKLIST))
return false;
if (isset(self::_DEFAULTS[$pref_name])) {
list ($def_val, $type_hint) = self::_DEFAULTS[$pref_name];
if ($strip_tags)
$value = trim(strip_tags($value));
$value = Config::cast_to($value, $type_hint);
if ($value == $this->_get($pref_name, $owner_uid, $profile_id))
return true; // no need to actually set this to the same value, let's just say we did
$this->_set_cache($pref_name, $value, $owner_uid, $profile_id);
$sth = $this->pdo->prepare("SELECT COUNT(pref_name) AS count FROM ttrss_user_prefs2
WHERE pref_name = :name AND owner_uid = :uid AND
(profile = :profile OR (:profile IS NULL AND profile IS NULL))");
$sth->execute(["uid" => $owner_uid, "profile" => $profile_id, "name" => $pref_name ]);
if ($row = $sth->fetch()) {
if ($row["count"] == 0) {
$sth = $this->pdo->prepare("INSERT INTO ttrss_user_prefs2
(pref_name, value, owner_uid, profile)
VALUES
(:name, :value, :uid, :profile)");
return $sth->execute(["uid" => $owner_uid, "profile" => $profile_id, "name" => $pref_name, "value" => $value ]);
} else {
$sth = $this->pdo->prepare("UPDATE ttrss_user_prefs2
SET value = :value
WHERE pref_name = :name AND owner_uid = :uid AND
(profile = :profile OR (:profile IS NULL AND profile IS NULL))");
return $sth->execute(["uid" => $owner_uid, "profile" => $profile_id, "name" => $pref_name, "value" => $value ]);
}
}
} else {
user_error("Attempt to set invalid preference key: $pref_name (UID: $owner_uid, profile: $profile_id)", E_USER_WARNING);
}
return false;
}
function migrate(int $owner_uid, ?int $profile_id): void {
if (Config::get_schema_version() < 141)
return;
if (!$profile_id) $profile_id = null;
if (!$this->_get(Prefs::_PREFS_MIGRATED, $owner_uid, $profile_id)) {
$in_nested_tr = false;
try {
$this->pdo->beginTransaction();
} catch (PDOException $e) {
$in_nested_tr = true;
}
$sth = $this->pdo->prepare("SELECT pref_name, value FROM ttrss_user_prefs
WHERE owner_uid = :uid AND
(profile = :profile OR (:profile IS NULL AND profile IS NULL))");
$sth->execute(["uid" => $owner_uid, "profile" => $profile_id]);
while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
if (isset(self::_DEFAULTS[$row["pref_name"]])) {
list ($def_val, $type_hint) = self::_DEFAULTS[$row["pref_name"]];
$user_val = Config::cast_to($row["value"], $type_hint);
if ($user_val != $def_val) {
$this->_set($row["pref_name"], $user_val, $owner_uid, $profile_id);
}
}
}
$this->_set(Prefs::_PREFS_MIGRATED, "1", $owner_uid, $profile_id);
if (!$in_nested_tr)
$this->pdo->commit();
Logger::log(E_USER_NOTICE, sprintf("Migrated preferences of user %d (profile %d)", $owner_uid, $profile_id));
}
}
static function reset(int $owner_uid, ?int $profile_id): void {
if (!$profile_id) $profile_id = null;
$sth = Db::pdo()->prepare("DELETE FROM ttrss_user_prefs2
WHERE owner_uid = :uid AND pref_name != :mig_key AND
(profile = :profile OR (:profile IS NULL AND profile IS NULL))");
$sth->execute(["uid" => $owner_uid, "mig_key" => self::_PREFS_MIGRATED, "profile" => $profile_id]);
}
}

725
classes/RPC.php Normal file
View File

@@ -0,0 +1,725 @@
<?php
class RPC extends Handler_Protected {
/*function csrf_ignore(string $method): bool {
$csrf_ignored = array("completelabels");
return array_search($method, $csrf_ignored) !== false;
}*/
/**
* @return array<string, string>
*/
private function _translations_as_array(): array {
global $text_domains;
$rv = [];
foreach (array_keys($text_domains) as $domain) {
/** @var gettext_reader $l10n */
$l10n = _get_reader($domain);
for ($i = 0; $i < $l10n->total; $i++) {
if (isset($l10n->table_originals[$i * 2 + 2]) && $orig = $l10n->get_original_string($i)) {
if(str_contains($orig, "\000")) { // Plural forms
$key = explode(chr(0), $orig);
$rv[$key[0]] = _ngettext($key[0], $key[1], 1); // Singular
$rv[$key[1]] = _ngettext($key[0], $key[1], 2); // Plural
} else {
$translation = _dgettext($domain,$orig);
$rv[$orig] = $translation;
}
}
}
}
return $rv;
}
function togglepref(): void {
$key = clean($_REQUEST["key"]);
$profile = $_SESSION['profile'] ?? null;
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));
}
function setpref(): void {
// set_pref escapes input, so no need to double escape it here
$key = clean($_REQUEST['key']);
$value = $_REQUEST['value'];
Prefs::set($key, $value, $_SESSION['uid'], $_SESSION['profile'] ?? null, $key != 'USER_STYLESHEET');
print json_encode(array("param" =>$key, "value" => $value));
}
function mark(): void {
$mark = clean($_REQUEST["mark"]);
$id = clean($_REQUEST["id"]);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET marked = ?,
last_marked = NOW()
WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$mark, $id, $_SESSION['uid']]);
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_MARK_TOGGLED, [$id]);
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function delete(): void {
$ids = explode(",", clean($_REQUEST["ids"]));
$ids_qmarks = arr_qmarks($ids);
$sth = $this->pdo->prepare("DELETE FROM ttrss_user_entries
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
$sth->execute([...$ids, $_SESSION['uid']]);
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function publ(): void {
$pub = clean($_REQUEST["pub"]);
$id = clean($_REQUEST["id"]);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = ?, last_published = NOW()
WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$pub, $id, $_SESSION['uid']]);
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_PUBLISH_TOGGLED, [$id]);
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function getRuntimeInfo(): void {
$reply = [
'runtime-info' => $this->_make_runtime_info()
];
print json_encode($reply);
}
function getAllCounters(): void {
@$seq = (int) $_REQUEST['seq'];
$feed_id_count = (int) ($_REQUEST["feed_id_count"] ?? -1);
$label_id_count = (int) ($_REQUEST["label_id_count"] ?? -1);
// it seems impossible to distinguish empty array [] from a null - both become unset in $_REQUEST
// so, count is >= 0 means we had an array, -1 means null
// we need null because it means "return all counters"; [] would return nothing
if ($feed_id_count == -1)
$feed_ids = null;
else
$feed_ids = array_map("intval", clean($_REQUEST["feed_ids"] ?? []));
if ($label_id_count == -1)
$label_ids = null;
else
$label_ids = array_map("intval", clean($_REQUEST["label_ids"] ?? []));
$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();
$reply = [
'counters' => $counters,
'seq' => $seq
];
print json_encode($reply);
}
/* GET["cmode"] = 0 - mark as read, 1 - as unread, 2 - toggle */
function catchupSelected(): void {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
if (count($ids) > 0)
Article::_catchup_by_id($ids, $cmode);
print json_encode(["message" => "UPDATE_COUNTERS",
"labels" => Article::_labels_of($ids),
"feeds" => Article::_feeds_of($ids)]);
}
function markSelected(): void {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
if (count($ids) > 0)
$this->markArticlesById($ids, $cmode);
print json_encode(["message" => "UPDATE_COUNTERS",
"labels" => Article::_labels_of($ids),
"feeds" => Article::_feeds_of($ids)]);
}
function publishSelected(): void {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
if (count($ids) > 0)
$this->publishArticlesById($ids, $cmode);
print json_encode(["message" => "UPDATE_COUNTERS",
"labels" => Article::_labels_of($ids),
"feeds" => Article::_feeds_of($ids)]);
}
function sanityCheck(): void {
$_SESSION["hasSandbox"] = self::_param_to_bool($_REQUEST["hasSandbox"] ?? false);
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
$client_location = $_REQUEST["clientLocation"];
$error = Errors::E_SUCCESS;
$error_params = [];
$client_scheme = parse_url($client_location, PHP_URL_SCHEME);
$server_scheme = parse_url(Config::get_self_url(), PHP_URL_SCHEME);
if (Config::is_migration_needed()) {
$error = Errors::E_SCHEMA_MISMATCH;
} else if ($client_scheme != $server_scheme) {
$error = Errors::E_URL_SCHEME_MISMATCH;
$error_params["client_scheme"] = $client_scheme;
$error_params["server_scheme"] = $server_scheme;
$error_params["self_url_path"] = Config::get_self_url();
}
if ($error == Errors::E_SUCCESS) {
$reply = [];
$reply['init-params'] = $this->_make_init_params();
$reply['runtime-info'] = $this->_make_runtime_info();
$reply['translations'] = $this->_translations_as_array();
print json_encode($reply);
} else {
print Errors::to_json($error, $error_params);
}
}
/*function completeLabels() {
$search = clean($_REQUEST["search"]);
$sth = $this->pdo->prepare("SELECT DISTINCT caption FROM
ttrss_labels2
WHERE owner_uid = ? AND
LOWER(caption) LIKE LOWER(?) ORDER BY caption
LIMIT 5");
$sth->execute([$_SESSION['uid'], "%$search%"]);
print "<ul>";
while ($line = $sth->fetch()) {
print "<li>" . $line["caption"] . "</li>";
}
print "</ul>";
}*/
function catchupFeed(): void {
$feed_id = clean($_REQUEST['feed_id']);
$is_cat = self::_param_to_bool($_REQUEST['is_cat'] ?? false);
$mode = clean($_REQUEST['mode'] ?? '');
$search_query = clean($_REQUEST['search_query']);
$search_lang = clean($_REQUEST['search_lang']);
Feeds::_catchup($feed_id, $is_cat, null, $mode, [$search_query, $search_lang]);
// return counters here synchronously so that frontend can figure out next unread feed properly
print json_encode(['counters' => Counters::get_all()]);
//print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function setWidescreen(): void {
$wide = (int) clean($_REQUEST["wide"]);
Prefs::set(Prefs::WIDESCREEN_MODE, $wide, $_SESSION['uid'], $_SESSION['profile'] ?? null);
print json_encode(["wide" => $wide]);
}
/**
* @param array<int, int> $ids
*/
private function markArticlesById(array $ids, int $cmode): void {
$ids_qmarks = arr_qmarks($ids);
if ($cmode == Article::CATCHUP_MODE_MARK_AS_READ) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
marked = false, last_marked = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else if ($cmode == Article::CATCHUP_MODE_MARK_AS_UNREAD) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
marked = true, last_marked = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
marked = NOT marked,last_marked = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
}
$sth->execute([...$ids, $_SESSION['uid']]);
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_MARK_TOGGLED, $ids);
}
/**
* @param array<int, int> $ids
*/
private function publishArticlesById(array $ids, int $cmode): void {
$ids_qmarks = arr_qmarks($ids);
if ($cmode == Article::CATCHUP_MODE_MARK_AS_READ) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = false, last_published = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else if ($cmode == Article::CATCHUP_MODE_MARK_AS_UNREAD) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = true, last_published = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = NOT published,last_published = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
}
$sth->execute([...$ids, $_SESSION['uid']]);
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_ARTICLES_PUBLISH_TOGGLED, $ids);
}
function log(): void {
$msg = clean($_REQUEST['msg'] ?? "");
$file = basename(clean($_REQUEST['file'] ?? ""));
$line = (int) clean($_REQUEST['line'] ?? 0);
$context = clean($_REQUEST['context'] ?? "");
if ($msg) {
Logger::log_error(E_USER_WARNING,
$msg, 'client-js:' . $file, $line, $context);
echo json_encode(array("message" => "HOST_ERROR_LOGGED"));
}
}
function checkforupdates(): void {
$rv = ["changeset" => [], "plugins" => []];
$version = Config::get_version(false);
$git_timestamp = $version["timestamp"] ?? 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) {
// $content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]);
$content = false;
/** @phpstan-ignore if.alwaysFalse (intentionally disabling for now) */
if ($content) {
$content = json_decode($content, true);
if ($content && isset($content["changeset"])) {
if ($git_timestamp < (int)$content["changeset"]["timestamp"] &&
$git_commit != $content["changeset"]["id"]) {
$rv["changeset"] = $content["changeset"];
}
}
}
$rv["plugins"] = Pref_Prefs::_get_updated_plugins();
}
print json_encode($rv);
}
/**
* @return array<string, mixed>
*/
private function _make_init_params(): array {
$profile = $_SESSION['profile'] ?? null;
$params = array();
foreach ([Prefs::ON_CATCHUP_SHOW_NEXT_FEED, Prefs::HIDE_READ_FEEDS,
Prefs::ENABLE_FEED_CATS, Prefs::FEEDS_SORT_BY_UNREAD,
Prefs::CONFIRM_FEED_CATCHUP, Prefs::CDM_AUTO_CATCHUP,
Prefs::FRESH_ARTICLE_MAX_AGE, Prefs::HIDE_READ_SHOWS_SPECIAL,
Prefs::COMBINED_DISPLAY_MODE, Prefs::DEBUG_HEADLINE_IDS, Prefs::CDM_ENABLE_GRID] as $param) {
$params[strtolower($param)] = (int) Prefs::get($param, $_SESSION['uid'], $profile);
}
$params["safe_mode"] = !empty($_SESSION["safe_mode"]);
$params["check_for_updates"] = Config::get(Config::CHECK_FOR_UPDATES);
$params["icons_url"] = Config::get_self_url() . '/public.php';
$params["cookie_lifetime"] = Config::get(Config::SESSION_COOKIE_LIFETIME);
$params["default_view_mode"] = Prefs::get(Prefs::_DEFAULT_VIEW_MODE, $_SESSION['uid'], $profile);
$params["default_view_limit"] = (int) Prefs::get(Prefs::_DEFAULT_VIEW_LIMIT, $_SESSION['uid'], $profile);
$params["default_view_order_by"] = Prefs::get(Prefs::_DEFAULT_VIEW_ORDER_BY, $_SESSION['uid'], $profile);
$params["bw_limit"] = (int) ($_SESSION["bw_limit"] ?? false);
$params["is_default_pw"] = UserHelper::is_default_password();
$params["label_base_index"] = LABEL_BASE_INDEX;
$theme = Prefs::get(Prefs::USER_CSS_THEME, $_SESSION['uid'], $profile);
$params["theme"] = theme_exists($theme) ? $theme : "";
$params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
$params["php_platform"] = PHP_OS;
$params["php_version"] = PHP_VERSION;
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
ttrss_feeds WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
$max_feed_id = $row["mid"];
$num_feeds = $row["nf"];
$params["self_url_prefix"] = Config::get_self_url();
$params["max_feed_id"] = (int) $max_feed_id;
$params["num_feeds"] = (int) $num_feeds;
$params["hotkeys"] = $this->get_hotkeys_map();
$params["widescreen"] = (int) Prefs::get(Prefs::WIDESCREEN_MODE, $_SESSION['uid'], $profile);
$params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif");
$params["icon_oval"] = $this->image_to_base64("images/oval.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["labels"] = Labels::get_all($_SESSION["uid"]);
return $params;
}
private function image_to_base64(string $filename): string {
if (file_exists($filename)) {
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if ($ext == "svg") $ext = "svg+xml";
return "data:image/$ext;base64," . base64_encode((string)file_get_contents($filename));
} else {
return "";
}
}
/**
* @return array<string, mixed>
*/
static function _make_runtime_info(): array {
$data = array();
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
ttrss_feeds WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
$max_feed_id = $row['mid'];
$num_feeds = $row['nf'];
$data["max_feed_id"] = (int) $max_feed_id;
$data["num_feeds"] = (int) $num_feeds;
$data['cdm_expanded'] = Prefs::get(Prefs::CDM_EXPANDED, $_SESSION['uid'], $_SESSION['profile'] ?? null);
$data["labels"] = Labels::get_all($_SESSION["uid"]);
if (Config::get(Config::LOG_DESTINATION) == 'sql' && $_SESSION['access_level'] >= UserHelper::ACCESS_LEVEL_ADMIN) {
$sth = $pdo->prepare("SELECT COUNT(id) AS cid
FROM ttrss_error_log
WHERE
errno NOT IN (".E_USER_NOTICE.", ".E_USER_DEPRECATED.") AND
created_at > NOW() - INTERVAL '1 hour' AND
errstr NOT LIKE '%Returning bool from comparison function is deprecated%' AND
errstr NOT LIKE '%imagecreatefromstring(): Data is not in a recognized format%'");
$sth->execute();
if ($row = $sth->fetch()) {
$data['recent_log_events'] = $row['cid'];
}
}
if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.lock")) {
$data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
if (time() - ($_SESSION["daemon_stamp_check"] ?? 0) > 30) {
$stamp = (int) @file_get_contents(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.stamp");
if ($stamp) {
$stamp_delta = time() - $stamp;
if ($stamp_delta > 1800) {
$stamp_check = 0;
} else {
$stamp_check = 1;
$_SESSION["daemon_stamp_check"] = time();
}
$data['daemon_stamp_ok'] = $stamp_check;
$stamp_fmt = date("Y.m.d, G:i", $stamp);
$data['daemon_stamp'] = $stamp_fmt;
}
}
}
return $data;
}
/**
* @return array<string, array<string, string>>
*/
static function get_hotkeys_info(): array {
$hotkeys = array(
__("Navigation") => array(
"next_feed" => __("Open next feed"),
"next_unread_feed" => __("Open next unread feed"),
"prev_feed" => __("Open previous feed"),
"prev_unread_feed" => __("Open previous unread feed"),
"next_article_or_scroll" => __("Open next article (in combined mode, scroll down)"),
"prev_article_or_scroll" => __("Open previous article (in combined mode, scroll up)"),
"next_headlines_page" => __("Scroll headlines by one page down"),
"prev_headlines_page" => __("Scroll headlines by one page up"),
"next_article_noscroll" => __("Open next article"),
"prev_article_noscroll" => __("Open previous article"),
"next_article_noexpand" => __("Move to next article (don't expand)"),
"prev_article_noexpand" => __("Move to previous article (don't expand)"),
"search_dialog" => __("Show search dialog"),
"cancel_search" => __("Cancel active search")),
__("Article") => array(
"toggle_mark" => __("Toggle starred"),
"toggle_publ" => __("Toggle published"),
"toggle_unread" => __("Toggle unread"),
"edit_tags" => __("Edit tags"),
"open_in_new_window" => __("Open in new window"),
"catchup_below" => __("Mark below as read"),
"catchup_above" => __("Mark above as read"),
"article_scroll_down" => __("Scroll down"),
"article_scroll_up" => __("Scroll up"),
"article_page_down" => __("Scroll down page"),
"article_page_up" => __("Scroll up page"),
"select_article_cursor" => __("Select article under cursor"),
"email_article" => __("Email article"),
"close_article" => __("Close/collapse article"),
"toggle_expand" => __("Toggle article expansion (combined mode)"),
"toggle_widescreen" => __("Toggle widescreen mode"),
"toggle_full_text" => __("Toggle full article text via Readability")),
__("Article selection") => array(
"select_all" => __("Select all articles"),
"select_unread" => __("Select unread"),
"select_marked" => __("Select starred"),
"select_published" => __("Select published"),
"select_invert" => __("Invert selection"),
"select_none" => __("Deselect everything")),
__("Feed") => array(
"feed_refresh" => __("Refresh current feed"),
"feed_unhide_read" => __("Un/hide read feeds"),
"feed_subscribe" => __("Subscribe to feed"),
"feed_edit" => __("Edit feed"),
"feed_catchup" => __("Mark as read"),
"feed_reverse" => __("Reverse headlines"),
"feed_toggle_vgroup" => __("Toggle headline grouping"),
"feed_toggle_grid" => __("Toggle grid view"),
"feed_debug_update" => __("Debug feed update"),
"feed_debug_viewfeed" => __("Debug viewfeed()"),
"catchup_all" => __("Mark all feeds as read"),
"cat_toggle_collapse" => __("Un/collapse current category"),
"toggle_cdm_expanded" => __("Toggle auto expand in combined mode"),
"toggle_combined_mode" => __("Toggle combined mode")),
__("Go to") => array(
"goto_all" => __("All articles"),
"goto_fresh" => __("Fresh"),
"goto_marked" => __("Starred"),
"goto_published" => __("Published"),
"goto_read" => __("Recently read"),
"goto_prefs" => __("Preferences")),
__("Other") => array(
"create_label" => __("Create label"),
"create_filter" => __("Create filter"),
"collapse_sidebar" => __("Un/collapse sidebar"),
"help_dialog" => __("Show help dialog"))
);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HOTKEY_INFO,
function ($result) use (&$hotkeys) {
$hotkeys = $result;
},
$hotkeys);
return $hotkeys;
}
/**
* {3} - 3 panel mode only
* {C} - combined mode only
*
* @return array{0: array<int, string>, 1: array<string, string>} $prefixes, $hotkeys
*/
static function get_hotkeys_map() {
$hotkeys = array(
"k" => "next_feed",
"K" => "next_unread_feed",
"j" => "prev_feed",
"J" => "prev_unread_feed",
"n" => "next_article_noscroll",
"p" => "prev_article_noscroll",
"N" => "article_page_down",
"P" => "article_page_up",
"*(33)|Shift+PgUp" => "article_page_up",
"*(34)|Shift+PgDn" => "article_page_down",
"{3}(38)|Up" => "prev_article_or_scroll",
"{3}(40)|Down" => "next_article_or_scroll",
"*(38)|Shift+Up" => "article_scroll_up",
"*(40)|Shift+Down" => "article_scroll_down",
"^(38)|Ctrl+Up" => "prev_article_noscroll",
"^(40)|Ctrl+Down" => "next_article_noscroll",
"/" => "search_dialog",
"\\" => "cancel_search",
"s" => "toggle_mark",
"S" => "toggle_publ",
"u" => "toggle_unread",
"T" => "edit_tags",
"o" => "open_in_new_window",
"c p" => "catchup_below",
"c n" => "catchup_above",
"a W" => "toggle_widescreen",
"a e" => "toggle_full_text",
"e" => "email_article",
"a q" => "close_article",
"a s" => "article_span_grid",
"a a" => "select_all",
"a u" => "select_unread",
"a U" => "select_marked",
"a p" => "select_published",
"a i" => "select_invert",
"a n" => "select_none",
"f r" => "feed_refresh",
"f a" => "feed_unhide_read",
"f s" => "feed_subscribe",
"f e" => "feed_edit",
"f q" => "feed_catchup",
"f x" => "feed_reverse",
"f g" => "feed_toggle_vgroup",
"f G" => "feed_toggle_grid",
"f D" => "feed_debug_update",
"f %" => "feed_debug_viewfeed",
"f C" => "toggle_combined_mode",
"f c" => "toggle_cdm_expanded",
"Q" => "catchup_all",
"x" => "cat_toggle_collapse",
"g a" => "goto_all",
"g f" => "goto_fresh",
"g s" => "goto_marked",
"g p" => "goto_published",
"g r" => "goto_read",
"g P" => "goto_prefs",
"r" => "select_article_cursor",
"c l" => "create_label",
"c f" => "create_filter",
"c s" => "collapse_sidebar",
"?" => "help_dialog",
);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HOTKEY_MAP,
function ($result) use (&$hotkeys) {
$hotkeys = $result;
},
$hotkeys);
$prefixes = array();
foreach (array_keys($hotkeys) as $hotkey) {
$pair = explode(" ", (string)$hotkey, 2);
if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
array_push($prefixes, $pair[0]);
}
}
return array($prefixes, $hotkeys);
}
function hotkeyHelp(): void {
$info = self::get_hotkeys_info();
$imap = self::get_hotkeys_map();
$omap = [];
foreach ($imap[1] as $sequence => $action) {
$omap[$action] ??= [];
$omap[$action][] = $sequence;
}
?>
<ul class='panel panel-scrollable hotkeys-help' style='height : 300px'>
<?php
foreach ($info as $section => $hotkeys) {
?>
<li><h3><?= $section ?></h3></li>
<?php
foreach ($hotkeys as $action => $description) {
if (!empty($omap[$action])) {
foreach ($omap[$action] as $sequence) {
if (str_contains($sequence, "|")) {
$sequence = substr($sequence,
strpos($sequence, "|")+1,
strlen($sequence));
} else {
$keys = explode(" ", $sequence);
for ($i = 0; $i < count($keys); $i++) {
if (strlen($keys[$i]) > 1) {
$tmp = '';
foreach (str_split($keys[$i]) as $c) {
$tmp .= match ($c) {
'*' => __('Shift') . '+',
'^' => __('Ctrl') . '+',
default => $c,
};
}
$keys[$i] = $tmp;
}
}
$sequence = join(" ", $keys);
}
?>
<li>
<div class='hk'><code><?= $sequence ?></code></div>
<div class='desc'><?= $description ?></div>
</li>
<?php
}
}
}
}
?>
</ul>
<footer class='text-center'>
<?= \Controls\submit_tag(__('Close this window')) ?>
</footer>
<?php
}
}

2009
classes/RSSUtils.php Normal file

File diff suppressed because it is too large Load Diff

274
classes/Sanitizer.php Normal file
View File

@@ -0,0 +1,274 @@
<?php
class Sanitizer {
/**
* @param array<int, string> $allowed_elements
* @param array<int, string> $disallowed_attributes
*/
private static function strip_harmful_tags(DOMDocument $doc, array $allowed_elements, $disallowed_attributes): DOMDocument {
$xpath = new DOMXPath($doc);
$entries = $xpath->query('//*');
foreach ($entries as $entry) {
/** @var DOMElement $entry */
if (!in_array($entry->nodeName, $allowed_elements)) {
$entry->parentNode->removeChild($entry);
}
if ($entry->hasAttributes()) {
$attrs_to_remove = array();
foreach ($entry->attributes as $attr) {
if (str_starts_with($attr->nodeName, 'on')) {
array_push($attrs_to_remove, $attr);
}
if (str_starts_with($attr->nodeName, 'data-')) {
array_push($attrs_to_remove, $attr);
}
if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
array_push($attrs_to_remove, $attr);
}
if (in_array($attr->nodeName, $disallowed_attributes)) {
array_push($attrs_to_remove, $attr);
}
}
foreach ($attrs_to_remove as $attr) {
$entry->removeAttributeNode($attr);
}
}
}
return $doc;
}
public static function iframe_whitelisted(DOMElement $entry): bool {
$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
if (!empty($src))
return PluginHost::getInstance()->run_hooks_until(PluginHost::HOOK_IFRAME_WHITELISTED, true, $src);
return false;
}
private static function is_prefix_https(): bool {
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.
*
* @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): false|string {
if (!$owner && isset($_SESSION["uid"]))
$owner = $_SESSION["uid"];
$profile = isset($_SESSION['uid']) && $owner == $_SESSION['uid'] && isset($_SESSION['profile']) ? $_SESSION['profile'] : null;
$res = trim($str); if (!$res) return '';
$doc = new DOMDocument();
$doc->loadHTML('<?xml encoding="UTF-8">' . $res);
$xpath = new DOMXPath($doc);
// is it a good idea to possibly rewrite urls to our own prefix?
// $rewrite_base_url = $site_url ? $site_url : Config::get_self_url();
$rewrite_base_url = $site_url ? $site_url : "http://domain.invalid/";
$entries = $xpath->query('(//a[@href]|//img[@src]|//source[@srcset|@src]|//video[@poster])');
/** @var DOMElement $entry */
foreach ($entries as $entry) {
if ($entry->hasAttribute('href')) {
$entry->setAttribute('href',
UrlHelper::rewrite_relative($rewrite_base_url, $entry->getAttribute('href'), $entry->tagName, "href"));
$entry->setAttribute('rel', 'noopener noreferrer');
$entry->setAttribute("target", "_blank");
}
if ($entry->hasAttribute('src')) {
$entry->setAttribute('src',
UrlHelper::rewrite_relative($rewrite_base_url, $entry->getAttribute('src'), $entry->tagName, "src"));
}
if ($entry->nodeName == 'img') {
$entry->setAttribute('referrerpolicy', 'no-referrer');
$entry->setAttribute('loading', 'lazy');
}
if ($entry->hasAttribute('srcset')) {
$matches = RSSUtils::decode_srcset($entry->getAttribute('srcset'));
for ($i = 0; $i < count($matches); $i++) {
$matches[$i]["url"] = UrlHelper::rewrite_relative($rewrite_base_url, $matches[$i]["url"]);
}
$entry->setAttribute("srcset", RSSUtils::encode_srcset($matches));
}
if ($entry->hasAttribute('poster')) {
$entry->setAttribute('poster',
UrlHelper::rewrite_relative($rewrite_base_url, $entry->getAttribute('poster'), $entry->tagName, "poster"));
}
if ($entry->hasAttribute('src') &&
($owner && Prefs::get(Prefs::STRIP_IMAGES, $owner, $profile)) || $force_remove_images || ($_SESSION['bw_limit'] ?? false)) {
$p = $doc->createElement('p');
$a = $doc->createElement('a');
$a->setAttribute('href', $entry->getAttribute('src'));
$a->appendChild(new DOMText($entry->getAttribute('src')));
$a->setAttribute('target', '_blank');
$a->setAttribute('rel', 'noopener noreferrer');
$p->appendChild($a);
if ($entry->nodeName == 'source') {
if ($entry->parentNode && $entry->parentNode->parentNode)
$entry->parentNode->parentNode->replaceChild($p, $entry->parentNode);
} else if ($entry->nodeName == 'img') {
if ($entry->parentNode)
$entry->parentNode->replaceChild($p, $entry);
}
}
}
$entries = $xpath->query('//iframe');
/** @var DOMElement $entry */
foreach ($entries as $entry) {
if (!self::iframe_whitelisted($entry)) {
$entry->setAttribute('sandbox', 'allow-scripts');
} else {
if (self::is_prefix_https()) {
$entry->setAttribute("src",
str_replace("http://", "https://",
$entry->getAttribute("src")));
}
}
}
$allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
'caption', 'cite', 'center', 'code', 'col', 'colgroup',
'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
'dt', 'em', 'footer', 'figure', 'figcaption',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
'ol', 'p', 'picture', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
if ($_SESSION['hasSandbox'] ?? false) $allowed_elements[] = 'iframe';
$disallowed_attributes = array('id', 'style', 'class', 'width', 'height', 'allow');
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_SANITIZE,
function ($result) use (&$doc, &$allowed_elements, &$disallowed_attributes) {
if (is_array($result)) {
$doc = $result[0];
$allowed_elements = $result[1];
$disallowed_attributes = $result[2];
} else {
$doc = $result;
}
},
$doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
$doc->removeChild($doc->firstChild); //remove doctype
$doc = self::strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
$entries = $xpath->query('//iframe');
foreach ($entries as $entry) {
$div = $doc->createElement('div');
$div->setAttribute('class', 'embed-responsive');
$entry->parentNode->replaceChild($div, $entry);
$div->appendChild($entry);
}
if (is_array($highlight_words))
self::highlight_words($doc, $xpath, $highlight_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;
}
}
}

171
classes/Scheduler.php Normal file
View 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;
}
}

156
classes/Sessions.php Normal file
View File

@@ -0,0 +1,156 @@
<?php
require_once 'lib/gettext/gettext.inc.php';
/**
* @todo look into making this behave closer to what SessionHandlerInterface intends
*/
class Sessions implements \SessionHandlerInterface {
private int $session_expire;
private string $session_name;
public function __construct() {
$this->session_expire = min(2147483647 - time() - 1, Config::get(Config::SESSION_COOKIE_LIFETIME));
$this->session_name = Config::get(Config::SESSION_NAME);
}
/**
* Adjusts session-related PHP configuration options
*/
public function configure(): void {
if (Config::is_server_https()) {
ini_set('session.cookie_secure', 'true');
}
ini_set('session.gc_probability', '75');
ini_set('session.name', $this->session_name);
ini_set('session.use_only_cookies', 'true');
ini_set('session.gc_maxlifetime', $this->session_expire);
ini_set('session.cookie_lifetime', '0');
}
/**
* 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
*/
public function extend_session(): bool {
if (isset($_COOKIE[$this->session_name]) && $this->session_expire > 0) {
return setcookie($this->session_name,
$_COOKIE[$this->session_name],
time() + $this->session_expire,
ini_get('session.cookie_path'),
ini_get('session.cookie_domain'),
ini_get('session.cookie_secure'),
ini_get('session.cookie_httponly'));
}
return false;
}
public function open(string $path, string $name): bool {
return true;
}
public function close(): bool {
return true;
}
public function read(string $id): false|string {
$sth = Db::pdo()->prepare('SELECT data FROM ttrss_sessions WHERE id=?');
$sth->execute([$id]);
if ($row = $sth->fetch()) {
$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;
$sth = Db::pdo()->prepare("INSERT INTO ttrss_sessions (id, data, expire)
VALUES (?, '', ?)");
return $sth->execute([$id, $expire]) ? '' : false;
}
public function write(string $id, string $data): bool {
if (Config::get(Config::ENCRYPTION_KEY))
$data = serialize(Crypt::encrypt_string($data));
$data = base64_encode($data);
$expire = time() + $this->session_expire;
$sth = Db::pdo()->prepare('SELECT id FROM ttrss_sessions WHERE id=?');
$sth->execute([$id]);
if ($sth->fetch()) {
$sth = Db::pdo()->prepare('UPDATE ttrss_sessions SET data=?, expire=? WHERE id=?');
return $sth->execute([$data, $expire, $id]);
}
$sth = Db::pdo()->prepare('INSERT INTO ttrss_sessions (id, data, expire) VALUES (?, ?, ?)');
return $sth->execute([$id, $data, $expire]);
}
public function destroy(string $id): bool {
$sth = Db::pdo()->prepare('DELETE FROM ttrss_sessions WHERE id = ?');
return $sth->execute([$id]);
}
/**
* @return int|false the number of deleted sessions on success, or false on failure
*/
public function gc(int $max_lifetime): false|int {
$result = Db::pdo()->query('DELETE FROM ttrss_sessions WHERE expire < ' . time());
return $result === false ? false : $result->rowCount();
}
public static function validate_session(): bool {
if (Config::get(Config::SINGLE_USER_MODE)) return true;
$pdo = Db::pdo();
if (!empty($_SESSION['uid'])) {
$user = ORM::for_table('ttrss_users')->find_one($_SESSION['uid']);
if ($user) {
if ($user->pwd_hash != $_SESSION['pwd_hash']) {
$_SESSION['login_error_msg'] = __('Session failed to validate (password changed)');
return false;
}
if ($user->access_level == UserHelper::ACCESS_LEVEL_DISABLED) {
$_SESSION['login_error_msg'] = __('Session failed to validate (account is disabled)');
return false;
}
// default to true because there might not be any hooks and this is our last check
$hook_result = true;
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_VALIDATE_SESSION,
function ($result) use (&$hook_result) {
$hook_result = $result;
if (!$result) {
return true;
}
});
return $hook_result;
} else {
$_SESSION['login_error_msg'] = __('Session failed to validate (user not found)');
return false;
}
}
return true;
}
}

21
classes/Templator.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
require_once "lib/MiniTemplator.class.php";
class Templator extends MiniTemplator {
/* this reads tt-rss template from templates.local/ or templates/ if only base filename is given */
function readTemplateFromFile ($fileName) {
if (!str_contains($fileName, "/")) {
$fileName = basename($fileName);
if (file_exists("templates.local/$fileName"))
return parent::readTemplateFromFile("templates.local/$fileName");
else
return parent::readTemplateFromFile("templates/$fileName");
} else {
return parent::readTemplateFromFile($fileName);
}
}
}

99
classes/TimeHelper.php Normal file
View File

@@ -0,0 +1,99 @@
<?php
class TimeHelper {
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'];
$profile = isset($_SESSION['uid']) && $owner_uid == $_SESSION['uid'] && isset($_SESSION['profile']) ? $_SESSION['profile'] : null;
if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
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)) {
$format = Prefs::get(Prefs::SHORT_DATE_FORMAT, $owner_uid, $profile);
if (!str_contains((strtolower($format)), "a"))
return date("G:i", $timestamp);
else
return date("g:i a", $timestamp);
} else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
$format = Prefs::get(Prefs::SHORT_DATE_FORMAT, $owner_uid, $profile);
return date($format, $timestamp);
} else {
$format = Prefs::get(Prefs::LONG_DATE_FORMAT, $owner_uid, $profile);
return date($format, $timestamp);
}
}
/**
* @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 {
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';
global $utc_tz;
global $user_tz;
if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
$timestamp = substr($timestamp, 0, 19);
# We store date in UTC internally
$dt = new DateTime($timestamp, $utc_tz);
$user_tz_string = Prefs::get(Prefs::USER_TIMEZONE, $owner_uid);
if ($user_tz_string != 'Automatic') {
try {
if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
} catch (Exception $e) {
$user_tz = $utc_tz;
}
$tz_offset = $user_tz->getOffset($dt);
} else {
$tz_offset = (int) -($_SESSION["clientTzOffset"] ?? 0);
}
$user_timestamp = $dt->format('U') + $tz_offset;
if (!$no_smart_dt) {
return self::smart_date_time($user_timestamp,
$tz_offset, $owner_uid, $eta_min);
} else {
if ($long)
$format = Prefs::get(Prefs::LONG_DATE_FORMAT, $owner_uid, $profile);
else
$format = Prefs::get(Prefs::SHORT_DATE_FORMAT, $owner_uid, $profile);
return date($format, $user_timestamp);
}
}
static function convert_timestamp(int $timestamp, string $source_tz, string $dest_tz): int {
try {
$source_tz = new DateTimeZone($source_tz);
} catch (Exception $e) {
$source_tz = new DateTimeZone('UTC');
}
try {
$dest_tz = new DateTimeZone($dest_tz);
} catch (Exception $e) {
$dest_tz = new DateTimeZone('UTC');
}
$dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
return (int)$dt->format('U') + $dest_tz->getOffset($dt);
}
}

485
classes/UrlHelper.php Normal file
View File

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

500
classes/UserHelper.php Normal file
View File

@@ -0,0 +1,500 @@
<?php
use OTPHP\TOTP;
class UserHelper {
const HASH_ALGO_SSHA512 = 'SSHA-512';
const HASH_ALGO_SSHA256 = 'SSHA-256';
const HASH_ALGO_MODE2 = 'MODE2';
const HASH_ALGO_SHA1X = 'SHA1X';
const HASH_ALGO_SHA1 = 'SHA1';
const HASH_ALGOS = [
self::HASH_ALGO_SSHA512,
self::HASH_ALGO_SSHA256,
self::HASH_ALGO_MODE2,
self::HASH_ALGO_SHA1X,
self::HASH_ALGO_SHA1
];
const ACCESS_LEVELS = [
self::ACCESS_LEVEL_DISABLED,
self::ACCESS_LEVEL_READONLY,
self::ACCESS_LEVEL_USER,
self::ACCESS_LEVEL_POWERUSER,
self::ACCESS_LEVEL_ADMIN,
self::ACCESS_LEVEL_KEEP_CURRENT
];
/** forbidden to login */
const ACCESS_LEVEL_DISABLED = -2;
/** can't subscribe to new feeds, feeds are not updated */
const ACCESS_LEVEL_READONLY = -1;
/** no restrictions, regular user */
const ACCESS_LEVEL_USER = 0;
/** not used, same as regular user */
const ACCESS_LEVEL_POWERUSER = 5;
/** has administrator permissions */
const ACCESS_LEVEL_ADMIN = 10;
/** used by self::user_modify() to keep current access level */
const ACCESS_LEVEL_KEEP_CURRENT = -1024;
/**
* @param int $level integer loglevel value
* @return UserHelper::ACCESS_LEVEL_* if valid, warn and return ACCESS_LEVEL_KEEP_CURRENT otherwise
*/
public static function map_access_level(int $level) : int {
if (in_array($level, self::ACCESS_LEVELS)) {
/** @phpstan-ignore return.type (yes it is a UserHelper::ACCESS_LEVEL_* value) */
return $level;
} else {
user_error("Passed invalid user access level: $level", E_USER_WARNING);
return self::ACCESS_LEVEL_KEEP_CURRENT;
}
}
static function authenticate(?string $login = null, ?string $password = null, bool $check_only = false, ?string $service = null): bool {
if (!Config::get(Config::SINGLE_USER_MODE)) {
$user_id = false;
$auth_module = false;
$login = mb_strtolower($login);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_AUTH_USER,
function ($result, $plugin) use (&$user_id, &$auth_module) {
if ($result) {
$user_id = (int)$result;
$auth_module = strtolower(get_class($plugin));
return true;
}
},
$login, $password, $service);
if ($user_id && !$check_only) {
if (session_status() != PHP_SESSION_ACTIVE)
session_start();
session_regenerate_id(true);
$user = ORM::for_table('ttrss_users')->find_one($user_id);
if ($user && $user->access_level != self::ACCESS_LEVEL_DISABLED) {
self::set_session_for_user($user_id);
$_SESSION["auth_module"] = $auth_module;
$_SESSION["name"] = $user->login;
$_SESSION["access_level"] = $user->access_level;
$_SESSION["pwd_hash"] = $user->pwd_hash;
$user->last_login = Db::NOW();
$user->save();
return true;
}
return false;
}
if ($login && $password && !$user_id && !$check_only)
Logger::log(E_USER_WARNING, "Failed login attempt for $login (service: $service) from " . UserHelper::get_user_ip());
return false;
} else {
self::set_session_for_user(1);
$_SESSION["name"] = "admin";
$_SESSION["access_level"] = self::ACCESS_LEVEL_ADMIN;
$_SESSION["hide_hello"] = true;
$_SESSION["hide_logout"] = true;
$_SESSION["auth_module"] = false;
return true;
}
}
static function set_session_for_user(int $owner_uid): void {
$_SESSION["uid"] = $owner_uid;
$_SESSION["last_login_update"] = time();
$_SESSION["ip_address"] = UserHelper::get_user_ip();
if (empty($_SESSION["csrf_token"]))
$_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
}
static function load_user_plugins(int $owner_uid, ?PluginHost $pluginhost = null): void {
if (!$pluginhost) $pluginhost = PluginHost::getInstance();
if ($owner_uid && Config::get_schema_version() >= 100 && empty($_SESSION["safe_mode"])) {
$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);
/*if (get_schema_version() > 100) {
$pluginhost->load_data();
}*/
}
}
static function login_sequence(): void {
$pdo = Db::pdo();
if (Config::get(Config::SINGLE_USER_MODE)) {
if (session_status() != PHP_SESSION_ACTIVE)
session_start();
self::authenticate("admin", null);
startup_gettext();
self::load_user_plugins($_SESSION["uid"]);
} else {
if (!Sessions::validate_session())
$_SESSION["uid"] = null;
if (empty($_SESSION["uid"])) {
if (Config::get(Config::AUTH_AUTO_LOGIN) && self::authenticate(null, null)) {
$_SESSION["ref_schema_version"] = Config::get_schema_version();
} else {
self::authenticate(null, null, true);
}
if (empty($_SESSION["uid"])) {
UserHelper::logout();
Handler_Public::_render_login_form();
exit;
}
} else {
/* bump login timestamp */
$user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]);
$user->last_login = Db::NOW();
$user->save();
$_SESSION["last_login_update"] = time();
}
startup_gettext();
self::load_user_plugins($_SESSION["uid"]);
}
}
static function print_user_stylesheet(): void {
$value = Prefs::get(Prefs::USER_STYLESHEET, $_SESSION['uid'], $_SESSION['profile'] ?? null);
if ($value) {
print "<style type='text/css' id='user_css_style'>";
print str_replace("<br/>", "\n", $value);
print "</style>";
}
}
static function get_user_ip(): ?string {
foreach (["HTTP_X_REAL_IP", "REMOTE_ADDR"] as $hdr) {
if (isset($_SERVER[$hdr]))
return $_SERVER[$hdr];
}
return null;
}
static function get_login_by_id(int $id): ?string {
$user = ORM::for_table('ttrss_users')
->find_one($id);
if ($user)
return $user->login;
else
return null;
}
static function find_user_by_login(string $login): ?int {
$user = ORM::for_table('ttrss_users')
->where_raw('LOWER(login) = LOWER(?)', [$login])
->find_one();
if ($user)
return $user->id;
else
return null;
}
static function logout(): void {
if (session_status() === PHP_SESSION_ACTIVE)
session_destroy();
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-42000, '/');
}
session_commit();
}
static function get_salt(): string {
return substr(bin2hex(get_random_bytes(125)), 0, 250);
}
/** TODO: this should invoke UserHelper::user_modify() */
static function reset_password(int $uid, bool $format_output = false, string $new_password = ""): void {
$user = ORM::for_table('ttrss_users')->find_one($uid);
if ($user) {
$login = $user->login;
$new_salt = self::get_salt();
$tmp_user_pwd = $new_password ? $new_password : make_password();
$pwd_hash = self::hash_password($tmp_user_pwd, $new_salt, self::HASH_ALGOS[0]);
$user->pwd_hash = $pwd_hash;
$user->salt = $new_salt;
$user->save();
$message = T_sprintf("Changed password of user %s to %s", "<strong>$login</strong>", "<strong>$tmp_user_pwd</strong>");
} else {
$message = __("User not found");
}
if ($format_output)
print_notice($message);
else
print $message;
}
static function check_otp(int $owner_uid, int $otp_check) : bool {
$otp = TOTP::create(self::get_otp_secret($owner_uid, true));
return $otp->now() == $otp_check;
}
static function disable_otp(int $owner_uid) : bool {
$user = ORM::for_table('ttrss_users')->find_one($owner_uid);
if ($user) {
$user->otp_enabled = false;
// force new OTP secret when next enabled
if (Config::get_schema_version() >= 143) {
$user->otp_secret = null;
}
$user->save();
return true;
} else {
return false;
}
}
static function enable_otp(int $owner_uid, int $otp_check) : bool {
$secret = self::get_otp_secret($owner_uid);
if ($secret) {
$otp = TOTP::create($secret);
$user = ORM::for_table('ttrss_users')->find_one($owner_uid);
if ($otp->now() == $otp_check && $user) {
$user->otp_enabled = true;
$user->save();
return true;
}
}
return false;
}
static function is_otp_enabled(int $owner_uid) : bool {
$user = ORM::for_table('ttrss_users')->find_one($owner_uid);
if ($user) {
return $user->otp_enabled;
} else {
return false;
}
}
static function get_otp_secret(int $owner_uid, bool $show_if_enabled = false): ?string {
$user = ORM::for_table('ttrss_users')->find_one($owner_uid);
if ($user) {
$salt_based_secret = mb_substr(sha1($user->salt), 0, 12);
if (Config::get_schema_version() >= 143) {
$secret = $user->otp_secret;
if (empty($secret)) {
/* migrate secret if OTP is already enabled, otherwise make a new one */
if ($user->otp_enabled) {
$user->otp_secret = $salt_based_secret;
} else {
$user->otp_secret = bin2hex(get_random_bytes(10));
}
$user->save();
$secret = $user->otp_secret;
}
} else {
$secret = $salt_based_secret;
}
if (!$user->otp_enabled || $show_if_enabled) {
return \ParagonIE\ConstantTime\Base32::encodeUpperUnpadded($secret);
}
}
return null;
}
/**
* @param null|int $owner_uid if null, checks current user via session-specific auth module, if set works on internal database only
* @throws PDOException
* @throws Exception
*/
static function is_default_password(?int $owner_uid = null): bool {
return self::user_has_password($owner_uid, 'password');
}
/**
* @param string $algo should be one of UserHelper::HASH_ALGO_*
*
* @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]): false|string {
$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,
};
if ($pass_hash === null)
user_error("hash_password: unknown hash algo: $algo", E_USER_ERROR);
return $pass_hash ? "$algo:$pass_hash" : false;
}
/**
* @param string $login Login for new user (case-insensitive)
* @param string $password Password for new user (may not be blank)
* @param UserHelper::ACCESS_LEVEL_* $access_level Access level for new user
* @return bool true if user has been created
*/
static function user_add(string $login, string $password, int $access_level) : bool {
$login = clean($login);
if ($login &&
$password &&
!self::find_user_by_login($login) &&
self::map_access_level((int)$access_level) != self::ACCESS_LEVEL_KEEP_CURRENT) {
$user = ORM::for_table('ttrss_users')->create();
$user->salt = self::get_salt();
$user->login = mb_strtolower($login);
$user->pwd_hash = self::hash_password($password, $user->salt);
$user->access_level = $access_level;
$user->created = Db::NOW();
return $user->save();
}
return false;
}
/**
* @param int $uid User ID to modify
* @param string $new_password set password to this value if its not blank
* @param UserHelper::ACCESS_LEVEL_* $access_level set user access level to this value if it is set (default ACCESS_LEVEL_KEEP_CURRENT)
* @return bool true if user record has been saved
*
* NOTE: $access_level is of mixed type because of intellephense
*/
static function user_modify(int $uid, string $new_password = '', $access_level = self::ACCESS_LEVEL_KEEP_CURRENT) : bool {
$user = ORM::for_table('ttrss_users')->find_one($uid);
if ($user) {
if ($new_password != '') {
$new_salt = self::get_salt();
$pwd_hash = self::hash_password($new_password, $new_salt, self::HASH_ALGOS[0]);
$user->pwd_hash = $pwd_hash;
$user->salt = $new_salt;
}
if ($access_level != self::ACCESS_LEVEL_KEEP_CURRENT) {
$user->access_level = (int)$access_level;
}
return $user->save();
}
return false;
}
/**
* @param int $uid user ID to delete (this won't delete built-in admin user with UID 1)
* @return bool true if user has been deleted
*/
static function user_delete(int $uid) : bool {
if ($uid != 1) {
$user = ORM::for_table('ttrss_users')->find_one($uid);
if ($user) {
// TODO: is it still necessary to split those queries?
ORM::for_table('ttrss_tags')
->where('owner_uid', $uid)
->delete_many();
ORM::for_table('ttrss_feeds')
->where('owner_uid', $uid)
->delete_many();
return $user->delete();
}
}
return false;
}
/**
* @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
*/
static function user_has_password(?int $owner_uid, string $password) : bool {
if ($owner_uid) {
$authenticator = new Auth_Internal();
return $authenticator->check_password($owner_uid, $password);
} else {
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
if ($authenticator &&
method_exists($authenticator, "check_password") &&
$authenticator->check_password($_SESSION["uid"], $password)) {
return true;
}
}
return false;
}
}

View File

@@ -1,888 +0,0 @@
<?php
class API extends Handler {
const API_LEVEL = 14;
const STATUS_OK = 0;
const STATUS_ERR = 1;
private $seq;
static function param_to_bool($p) {
return $p && ($p !== "f" && $p !== "false");
}
function before($method) {
if (parent::before($method)) {
header("Content-Type: text/json");
if (!$_SESSION["uid"] && $method != "login" && $method != "isloggedin") {
$this->wrap(self::STATUS_ERR, array("error" => 'NOT_LOGGED_IN'));
return false;
}
if ($_SESSION["uid"] && $method != "logout" && !get_pref('ENABLE_API_ACCESS')) {
$this->wrap(self::STATUS_ERR, array("error" => 'API_DISABLED'));
return false;
}
$this->seq = (int) clean($_REQUEST['seq']);
return true;
}
return false;
}
function wrap($status, $reply) {
print json_encode(array("seq" => $this->seq,
"status" => $status,
"content" => $reply));
}
function getVersion() {
$rv = array("version" => VERSION);
$this->wrap(self::STATUS_OK, $rv);
}
function getApiLevel() {
$rv = array("level" => self::API_LEVEL);
$this->wrap(self::STATUS_OK, $rv);
}
function login() {
@session_destroy();
@session_start();
$login = clean($_REQUEST["user"]);
$password = clean($_REQUEST["password"]);
$password_base64 = base64_decode(clean($_REQUEST["password"]));
if (SINGLE_USER_MODE) $login = "admin";
$sth = $this->pdo->prepare("SELECT id FROM ttrss_users WHERE login = ?");
$sth->execute([$login]);
if ($row = $sth->fetch()) {
$uid = $row["id"];
} else {
$uid = 0;
}
if (!$uid) {
$this->wrap(self::STATUS_ERR, array("error" => "LOGIN_ERROR"));
return;
}
if (get_pref("ENABLE_API_ACCESS", $uid)) {
if (authenticate_user($login, $password)) { // try login with normal password
$this->wrap(self::STATUS_OK, array("session_id" => session_id(),
"api_level" => self::API_LEVEL));
} else if (authenticate_user($login, $password_base64)) { // else try with base64_decoded password
$this->wrap(self::STATUS_OK, array("session_id" => session_id(),
"api_level" => self::API_LEVEL));
} else { // else we are not logged in
user_error("Failed login attempt for $login from {$_SERVER['REMOTE_ADDR']}", E_USER_WARNING);
$this->wrap(self::STATUS_ERR, array("error" => "LOGIN_ERROR"));
}
} else {
$this->wrap(self::STATUS_ERR, array("error" => "API_DISABLED"));
}
}
function logout() {
logout_user();
$this->wrap(self::STATUS_OK, array("status" => "OK"));
}
function isLoggedIn() {
$this->wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != ''));
}
function getUnread() {
$feed_id = clean($_REQUEST["feed_id"]);
$is_cat = clean($_REQUEST["is_cat"]);
if ($feed_id) {
$this->wrap(self::STATUS_OK, array("unread" => getFeedUnread($feed_id, $is_cat)));
} else {
$this->wrap(self::STATUS_OK, array("unread" => Feeds::getGlobalUnread()));
}
}
/* Method added for ttrss-reader for Android */
function getCounters() {
$this->wrap(self::STATUS_OK, Counters::getAllCounters());
}
function getFeeds() {
$cat_id = clean($_REQUEST["cat_id"]);
$unread_only = API::param_to_bool(clean($_REQUEST["unread_only"]));
$limit = (int) clean($_REQUEST["limit"]);
$offset = (int) clean($_REQUEST["offset"]);
$include_nested = API::param_to_bool(clean($_REQUEST["include_nested"]));
$feeds = $this->api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested);
$this->wrap(self::STATUS_OK, $feeds);
}
function getCategories() {
$unread_only = API::param_to_bool(clean($_REQUEST["unread_only"]));
$enable_nested = API::param_to_bool(clean($_REQUEST["enable_nested"]));
$include_empty = API::param_to_bool(clean($_REQUEST['include_empty']));
// TODO do not return empty categories, return Uncategorized and standard virtual cats
if ($enable_nested)
$nested_qpart = "parent_cat IS NULL";
else
$nested_qpart = "true";
$sth = $this->pdo->prepare("SELECT
id, title, order_id, (SELECT COUNT(id) FROM
ttrss_feeds WHERE
ttrss_feed_categories.id IS NOT NULL AND cat_id = ttrss_feed_categories.id) AS num_feeds,
(SELECT COUNT(id) FROM
ttrss_feed_categories AS c2 WHERE
c2.parent_cat = ttrss_feed_categories.id) AS num_cats
FROM ttrss_feed_categories
WHERE $nested_qpart AND owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$cats = array();
while ($line = $sth->fetch()) {
if ($include_empty || $line["num_feeds"] > 0 || $line["num_cats"] > 0) {
$unread = getFeedUnread($line["id"], true);
if ($enable_nested)
$unread += Feeds::getCategoryChildrenUnread($line["id"]);
if ($unread || !$unread_only) {
array_push($cats, array("id" => $line["id"],
"title" => $line["title"],
"unread" => $unread,
"order_id" => (int) $line["order_id"],
));
}
}
}
foreach (array(-2,-1,0) as $cat_id) {
if ($include_empty || !$this->isCategoryEmpty($cat_id)) {
$unread = getFeedUnread($cat_id, true);
if ($unread || !$unread_only) {
array_push($cats, array("id" => $cat_id,
"title" => Feeds::getCategoryTitle($cat_id),
"unread" => $unread));
}
}
}
$this->wrap(self::STATUS_OK, $cats);
}
function getHeadlines() {
$feed_id = clean($_REQUEST["feed_id"]);
if ($feed_id !== "") {
if (is_numeric($feed_id)) $feed_id = (int) $feed_id;
$limit = (int)clean($_REQUEST["limit"]);
if (!$limit || $limit >= 200) $limit = 200;
$offset = (int)clean($_REQUEST["skip"]);
$filter = clean($_REQUEST["filter"]);
$is_cat = API::param_to_bool(clean($_REQUEST["is_cat"]));
$show_excerpt = API::param_to_bool(clean($_REQUEST["show_excerpt"]));
$show_content = API::param_to_bool(clean($_REQUEST["show_content"]));
/* all_articles, unread, adaptive, marked, updated */
$view_mode = clean($_REQUEST["view_mode"]);
$include_attachments = API::param_to_bool(clean($_REQUEST["include_attachments"]));
$since_id = (int)clean($_REQUEST["since_id"]);
$include_nested = API::param_to_bool(clean($_REQUEST["include_nested"]));
$sanitize_content = !isset($_REQUEST["sanitize"]) ||
API::param_to_bool($_REQUEST["sanitize"]);
$force_update = API::param_to_bool(clean($_REQUEST["force_update"]));
$has_sandbox = API::param_to_bool(clean($_REQUEST["has_sandbox"]));
$excerpt_length = (int)clean($_REQUEST["excerpt_length"]);
$check_first_id = (int)clean($_REQUEST["check_first_id"]);
$include_header = API::param_to_bool(clean($_REQUEST["include_header"]));
$_SESSION['hasSandbox'] = $has_sandbox;
$skip_first_id_check = false;
$override_order = false;
switch (clean($_REQUEST["order_by"])) {
case "title":
$override_order = "ttrss_entries.title, date_entered, updated";
break;
case "date_reverse":
$override_order = "score DESC, date_entered, updated";
$skip_first_id_check = true;
break;
case "feed_dates":
$override_order = "updated DESC";
break;
}
/* do not rely on params below */
$search = clean($_REQUEST["search"]);
list($headlines, $headlines_header) = $this->api_get_headlines($feed_id, $limit, $offset,
$filter, $is_cat, $show_excerpt, $show_content, $view_mode, $override_order,
$include_attachments, $since_id, $search,
$include_nested, $sanitize_content, $force_update, $excerpt_length, $check_first_id, $skip_first_id_check);
if ($include_header) {
$this->wrap(self::STATUS_OK, array($headlines_header, $headlines));
} else {
$this->wrap(self::STATUS_OK, $headlines);
}
} else {
$this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE'));
}
}
function updateArticle() {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$mode = (int) clean($_REQUEST["mode"]);
$data = clean($_REQUEST["data"]);
$field_raw = (int)clean($_REQUEST["field"]);
$field = "";
$set_to = "";
switch ($field_raw) {
case 0:
$field = "marked";
$additional_fields = ",last_marked = NOW()";
break;
case 1:
$field = "published";
$additional_fields = ",last_published = NOW()";
break;
case 2:
$field = "unread";
$additional_fields = ",last_read = NOW()";
break;
case 3:
$field = "note";
};
switch ($mode) {
case 1:
$set_to = "true";
break;
case 0:
$set_to = "false";
break;
case 2:
$set_to = "NOT $field";
break;
}
if ($field == "note") $set_to = $this->pdo->quote($data);
if ($field && $set_to && count($article_ids) > 0) {
$article_qmarks = arr_qmarks($article_ids);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
$field = $set_to $additional_fields
WHERE ref_id IN ($article_qmarks) AND owner_uid = ?");
$sth->execute(array_merge($article_ids, [$_SESSION['uid']]));
$num_updated = $sth->rowCount();
if ($num_updated > 0 && $field == "unread") {
$sth = $this->pdo->prepare("SELECT DISTINCT feed_id FROM ttrss_user_entries
WHERE ref_id IN ($article_qmarks)");
$sth->execute($article_ids);
while ($line = $sth->fetch()) {
CCache::update($line["feed_id"], $_SESSION["uid"]);
}
}
$this->wrap(self::STATUS_OK, array("status" => "OK",
"updated" => $num_updated));
} else {
$this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE'));
}
}
function getArticle() {
$article_ids = explode(",", clean($_REQUEST["article_id"]));
$sanitize_content = !isset($_REQUEST["sanitize"]) ||
API::param_to_bool($_REQUEST["sanitize"]);
if ($article_ids) {
$article_qmarks = arr_qmarks($article_ids);
$sth = $this->pdo->prepare("SELECT id,guid,title,link,content,feed_id,comments,int_id,
marked,unread,published,score,note,lang,
".SUBSTRING_FOR_DATE."(updated,1,16) as updated,
author,(SELECT title FROM ttrss_feeds WHERE id = feed_id) AS feed_title,
(SELECT site_url FROM ttrss_feeds WHERE id = feed_id) AS site_url,
(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) AS hide_images
FROM ttrss_entries,ttrss_user_entries
WHERE id IN ($article_qmarks) AND ref_id = id AND owner_uid = ?");
$sth->execute(array_merge($article_ids, [$_SESSION['uid']]));
$articles = array();
while ($line = $sth->fetch()) {
$attachments = Article::get_article_enclosures($line['id']);
$article = array(
"id" => $line["id"],
"guid" => $line["guid"],
"title" => $line["title"],
"link" => $line["link"],
"labels" => Article::get_article_labels($line['id']),
"unread" => API::param_to_bool($line["unread"]),
"marked" => API::param_to_bool($line["marked"]),
"published" => API::param_to_bool($line["published"]),
"comments" => $line["comments"],
"author" => $line["author"],
"updated" => (int) strtotime($line["updated"]),
"feed_id" => $line["feed_id"],
"attachments" => $attachments,
"score" => (int)$line["score"],
"feed_title" => $line["feed_title"],
"note" => $line["note"],
"lang" => $line["lang"]
);
if ($sanitize_content) {
$article["content"] = sanitize(
$line["content"],
API::param_to_bool($line['hide_images']),
false, $line["site_url"], false, $line["id"]);
} else {
$article["content"] = $line["content"];
}
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_API) as $p) {
$article = $p->hook_render_article_api(array("article" => $article));
}
$article['content'] = rewrite_cached_urls($article['content']);
array_push($articles, $article);
}
$this->wrap(self::STATUS_OK, $articles);
} else {
$this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE'));
}
}
function getConfig() {
$config = array(
"icons_dir" => ICONS_DIR,
"icons_url" => ICONS_URL);
$config["daemon_is_running"] = file_is_locked("update_daemon.lock");
$sth = $this->pdo->prepare("SELECT COUNT(*) AS cf FROM
ttrss_feeds WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
$config["num_feeds"] = $row["cf"];
$this->wrap(self::STATUS_OK, $config);
}
function updateFeed() {
$feed_id = (int) clean($_REQUEST["feed_id"]);
if (!ini_get("open_basedir")) {
RSSUtils::update_rss_feed($feed_id);
}
$this->wrap(self::STATUS_OK, array("status" => "OK"));
}
function catchupFeed() {
$feed_id = clean($_REQUEST["feed_id"]);
$is_cat = clean($_REQUEST["is_cat"]);
Feeds::catchup_feed($feed_id, $is_cat);
$this->wrap(self::STATUS_OK, array("status" => "OK"));
}
function getPref() {
$pref_name = clean($_REQUEST["pref_name"]);
$this->wrap(self::STATUS_OK, array("value" => get_pref($pref_name)));
}
function getLabels() {
$article_id = (int)clean($_REQUEST['article_id']);
$rv = array();
$sth = $this->pdo->prepare("SELECT id, caption, fg_color, bg_color
FROM ttrss_labels2
WHERE owner_uid = ? ORDER BY caption");
$sth->execute([$_SESSION['uid']]);
if ($article_id)
$article_labels = Article::get_article_labels($article_id);
else
$article_labels = array();
while ($line = $sth->fetch()) {
$checked = false;
foreach ($article_labels as $al) {
if (Labels::feed_to_label_id($al[0]) == $line['id']) {
$checked = true;
break;
}
}
array_push($rv, array(
"id" => (int)Labels::label_to_feed_id($line['id']),
"caption" => $line['caption'],
"fg_color" => $line['fg_color'],
"bg_color" => $line['bg_color'],
"checked" => $checked));
}
$this->wrap(self::STATUS_OK, $rv);
}
function setArticleLabel() {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$label_id = (int) clean($_REQUEST['label_id']);
$assign = API::param_to_bool(clean($_REQUEST['assign']));
$label = Labels::find_caption(Labels::feed_to_label_id($label_id), $_SESSION["uid"]);
$num_updated = 0;
if ($label) {
foreach ($article_ids as $id) {
if ($assign)
Labels::add_article($id, $label, $_SESSION["uid"]);
else
Labels::remove_article($id, $label, $_SESSION["uid"]);
++$num_updated;
}
}
$this->wrap(self::STATUS_OK, array("status" => "OK",
"updated" => $num_updated));
}
function index($method) {
$plugin = PluginHost::getInstance()->get_api_method(strtolower($method));
if ($plugin && method_exists($plugin, $method)) {
$reply = $plugin->$method();
$this->wrap($reply[0], $reply[1]);
} else {
$this->wrap(self::STATUS_ERR, array("error" => 'UNKNOWN_METHOD', "method" => $method));
}
}
function shareToPublished() {
$title = strip_tags(clean($_REQUEST["title"]));
$url = strip_tags(clean($_REQUEST["url"]));
$content = strip_tags(clean($_REQUEST["content"]));
if (Article::create_published_article($title, $url, $content, "", $_SESSION["uid"])) {
$this->wrap(self::STATUS_OK, array("status" => 'OK'));
} else {
$this->wrap(self::STATUS_ERR, array("error" => 'Publishing failed'));
}
}
static function api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested = false) {
$feeds = array();
$pdo = Db::pdo();
$limit = (int) $limit;
$offset = (int) $offset;
$cat_id = (int) $cat_id;
/* Labels */
if ($cat_id == -4 || $cat_id == -2) {
$counters = Counters::getLabelCounters(true);
foreach (array_values($counters) as $cv) {
$unread = $cv["counter"];
if ($unread || !$unread_only) {
$row = array(
"id" => (int) $cv["id"],
"title" => $cv["description"],
"unread" => $cv["counter"],
"cat_id" => -2,
);
array_push($feeds, $row);
}
}
}
/* Virtual feeds */
if ($cat_id == -4 || $cat_id == -1) {
foreach (array(-1, -2, -3, -4, -6, 0) as $i) {
$unread = getFeedUnread($i);
if ($unread || !$unread_only) {
$title = Feeds::getFeedTitle($i);
$row = array(
"id" => $i,
"title" => $title,
"unread" => $unread,
"cat_id" => -1,
);
array_push($feeds, $row);
}
}
}
/* Child cats */
if ($include_nested && $cat_id) {
$sth = $pdo->prepare("SELECT
id, title, order_id FROM ttrss_feed_categories
WHERE parent_cat = ? AND owner_uid = ? ORDER BY id, title");
$sth->execute([$cat_id, $_SESSION['uid']]);
while ($line = $sth->fetch()) {
$unread = getFeedUnread($line["id"], true) +
Feeds::getCategoryChildrenUnread($line["id"]);
if ($unread || !$unread_only) {
$row = array(
"id" => (int) $line["id"],
"title" => $line["title"],
"unread" => $unread,
"is_cat" => true,
"order_id" => (int) $line["order_id"]
);
array_push($feeds, $row);
}
}
}
/* Real feeds */
if ($limit) {
$limit_qpart = "LIMIT $limit OFFSET $offset";
} else {
$limit_qpart = "";
}
if ($cat_id == -4 || $cat_id == -3) {
$sth = $pdo->prepare("SELECT
id, feed_url, cat_id, title, order_id, ".
SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
FROM ttrss_feeds WHERE owner_uid = ?
ORDER BY cat_id, title " . $limit_qpart);
$sth->execute([$_SESSION['uid']]);
} else {
$sth = $pdo->prepare("SELECT
id, feed_url, cat_id, title, order_id, ".
SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
FROM ttrss_feeds WHERE
(cat_id = :cat OR (:cat = 0 AND cat_id IS NULL))
AND owner_uid = :uid
ORDER BY cat_id, title " . $limit_qpart);
$sth->execute([":uid" => $_SESSION['uid'], ":cat" => $cat_id]);
}
while ($line = $sth->fetch()) {
$unread = getFeedUnread($line["id"]);
$has_icon = Feeds::feedHasIcon($line['id']);
if ($unread || !$unread_only) {
$row = array(
"feed_url" => $line["feed_url"],
"title" => $line["title"],
"id" => (int)$line["id"],
"unread" => (int)$unread,
"has_icon" => $has_icon,
"cat_id" => (int)$line["cat_id"],
"last_updated" => (int) strtotime($line["last_updated"]),
"order_id" => (int) $line["order_id"],
);
array_push($feeds, $row);
}
}
return $feeds;
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
static function api_get_headlines($feed_id, $limit, $offset,
$filter, $is_cat, $show_excerpt, $show_content, $view_mode, $order,
$include_attachments, $since_id,
$search = "", $include_nested = false, $sanitize_content = true,
$force_update = false, $excerpt_length = 100, $check_first_id = false, $skip_first_id_check = false) {
$pdo = Db::pdo();
if ($force_update && $feed_id > 0 && is_numeric($feed_id)) {
// Update the feed if required with some basic flood control
$sth = $pdo->prepare(
"SELECT cache_images,".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
FROM ttrss_feeds WHERE id = ?");
$sth->execute([$feed_id]);
if ($row = $sth->fetch()) {
$last_updated = strtotime($row["last_updated"]);
$cache_images = API::param_to_bool($row["cache_images"]);
if (!$cache_images && time() - $last_updated > 120) {
RSSUtils::update_rss_feed($feed_id, true);
} else {
$sth = $pdo->prepare("UPDATE ttrss_feeds SET last_updated = '1970-01-01', last_update_started = '1970-01-01'
WHERE id = ?");
$sth->execute([$feed_id]);
}
}
}
$params = array(
"feed" => $feed_id,
"limit" => $limit,
"view_mode" => $view_mode,
"cat_view" => $is_cat,
"search" => $search,
"override_order" => $order,
"offset" => $offset,
"since_id" => $since_id,
"include_children" => $include_nested,
"check_first_id" => $check_first_id,
"skip_first_id_check" => $skip_first_id_check
);
$qfh_ret = Feeds::queryFeedHeadlines($params);
$result = $qfh_ret[0];
$feed_title = $qfh_ret[1];
$first_id = $qfh_ret[6];
$headlines = array();
$headlines_header = array(
'id' => $feed_id,
'first_id' => $first_id,
'is_cat' => $is_cat);
if (!is_numeric($result)) {
while ($line = $result->fetch()) {
$line["content_preview"] = truncate_string(strip_tags($line["content"]), $excerpt_length);
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) {
$line = $p->hook_query_headlines($line, $excerpt_length, true);
}
$is_updated = ($line["last_read"] == "" &&
($line["unread"] != "t" && $line["unread"] != "1"));
$tags = explode(",", $line["tag_cache"]);
$label_cache = $line["label_cache"];
$labels = false;
if ($label_cache) {
$label_cache = json_decode($label_cache, true);
if ($label_cache) {
if ($label_cache["no-labels"] == 1)
$labels = array();
else
$labels = $label_cache;
}
}
if (!is_array($labels)) $labels = Article::get_article_labels($line["id"]);
$headline_row = array(
"id" => (int)$line["id"],
"guid" => $line["guid"],
"unread" => API::param_to_bool($line["unread"]),
"marked" => API::param_to_bool($line["marked"]),
"published" => API::param_to_bool($line["published"]),
"updated" => (int)strtotime($line["updated"]),
"is_updated" => $is_updated,
"title" => $line["title"],
"link" => $line["link"],
"feed_id" => $line["feed_id"] ? $line['feed_id'] : 0,
"tags" => $tags,
);
if ($include_attachments)
$headline_row['attachments'] = Article::get_article_enclosures(
$line['id']);
if ($show_excerpt)
$headline_row["excerpt"] = $line["content_preview"];
if ($show_content) {
if ($sanitize_content) {
$headline_row["content"] = sanitize(
$line["content"],
API::param_to_bool($line['hide_images']),
false, $line["site_url"], false, $line["id"]);
} else {
$headline_row["content"] = $line["content"];
}
}
// unify label output to ease parsing
if ($labels["no-labels"] == 1) $labels = array();
$headline_row["labels"] = $labels;
$headline_row["feed_title"] = $line["feed_title"] ? $line["feed_title"] :
$feed_title;
$headline_row["comments_count"] = (int)$line["num_comments"];
$headline_row["comments_link"] = $line["comments"];
$headline_row["always_display_attachments"] = API::param_to_bool($line["always_display_enclosures"]);
$headline_row["author"] = $line["author"];
$headline_row["score"] = (int)$line["score"];
$headline_row["note"] = $line["note"];
$headline_row["lang"] = $line["lang"];
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_API) as $p) {
$headline_row = $p->hook_render_article_api(array("headline" => $headline_row));
}
$headline_row['content'] = rewrite_cached_urls($headline_row['content']);
array_push($headlines, $headline_row);
}
} else if (is_numeric($result) && $result == -1) {
$headlines_header['first_id_changed'] = true;
}
return array($headlines, $headlines_header);
}
function unsubscribeFeed() {
$feed_id = (int) clean($_REQUEST["feed_id"]);
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE
id = ? AND owner_uid = ?");
$sth->execute([$feed_id, $_SESSION['uid']]);
if ($row = $sth->fetch()) {
Pref_Feeds::remove_feed($feed_id, $_SESSION["uid"]);
$this->wrap(self::STATUS_OK, array("status" => "OK"));
} else {
$this->wrap(self::STATUS_ERR, array("error" => "FEED_NOT_FOUND"));
}
}
function subscribeToFeed() {
$feed_url = clean($_REQUEST["feed_url"]);
$category_id = (int) clean($_REQUEST["category_id"]);
$login = clean($_REQUEST["login"]);
$password = clean($_REQUEST["password"]);
if ($feed_url) {
$rc = Feeds::subscribe_to_feed($feed_url, $category_id, $login, $password);
$this->wrap(self::STATUS_OK, array("status" => $rc));
} else {
$this->wrap(self::STATUS_ERR, array("error" => 'INCORRECT_USAGE'));
}
}
function getFeedTree() {
$include_empty = API::param_to_bool(clean($_REQUEST['include_empty']));
$pf = new Pref_Feeds($_REQUEST);
$_REQUEST['mode'] = 2;
$_REQUEST['force_show_empty'] = $include_empty;
if ($pf){
$data = $pf->makefeedtree();
$this->wrap(self::STATUS_OK, array("categories" => $data));
} else {
$this->wrap(self::STATUS_ERR, array("error" =>
'UNABLE_TO_INSTANTIATE_OBJECT'));
}
}
// only works for labels or uncategorized for the time being
private function isCategoryEmpty($id) {
if ($id == -2) {
$sth = $this->pdo->prepare("SELECT COUNT(id) AS count FROM ttrss_labels2
WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
return $row["count"] == 0;
} else if ($id == 0) {
$sth = $this->pdo->prepare("SELECT COUNT(id) AS count FROM ttrss_feeds
WHERE cat_id IS NULL AND owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
return $row["count"] == 0;
}
return false;
}
}

View File

@@ -1,824 +0,0 @@
<?php
class Article extends Handler_Protected {
function csrf_ignore($method) {
$csrf_ignored = array("redirect", "editarticletags");
return array_search($method, $csrf_ignored) !== false;
}
function redirect() {
$id = clean($_REQUEST['id']);
$sth = $this->pdo->prepare("SELECT link FROM ttrss_entries, ttrss_user_entries
WHERE id = ? AND id = ref_id AND owner_uid = ?
LIMIT 1");
$sth->execute([$id, $_SESSION['uid']]);
if ($row = $sth->fetch()) {
$article_url = $row['link'];
$article_url = str_replace("\n", "", $article_url);
header("Location: $article_url");
return;
} else {
print_error(__("Article not found."));
}
}
/*
function view() {
$id = clean($_REQUEST["id"]);
$cids = explode(",", clean($_REQUEST["cids"]));
$mode = clean($_REQUEST["mode"]);
// in prefetch mode we only output requested cids, main article
// just gets marked as read (it already exists in client cache)
$articles = array();
if ($mode == "") {
array_push($articles, $this->format_article($id, false));
} else if ($mode == "zoom") {
array_push($articles, $this->format_article($id, true, true));
} else if ($mode == "raw") {
if (isset($_REQUEST['html'])) {
header("Content-Type: text/html");
print '<link rel="stylesheet" type="text/css" href="css/default.css"/>';
}
$article = $this->format_article($id, false, isset($_REQUEST["zoom"]));
print $article['content'];
return;
}
$this->catchupArticleById($id, 0);
if (!$_SESSION["bw_limit"]) {
foreach ($cids as $cid) {
if ($cid) {
array_push($articles, $this->format_article($cid, false, false));
}
}
}
print json_encode($articles);
} */
/*
private function catchupArticleById($id, $cmode) {
if ($cmode == 0) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
unread = false,last_read = NOW()
WHERE ref_id = ? AND owner_uid = ?");
} else if ($cmode == 1) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
unread = true
WHERE ref_id = ? AND owner_uid = ?");
} else {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
unread = NOT unread,last_read = NOW()
WHERE ref_id = ? AND owner_uid = ?");
}
$sth->execute([$id, $_SESSION['uid']]);
$feed_id = $this->getArticleFeed($id);
CCache::update($feed_id, $_SESSION["uid"]);
}
*/
static function create_published_article($title, $url, $content, $labels_str,
$owner_uid) {
$guid = 'SHA1:' . sha1("ttshared:" . $url . $owner_uid); // include owner_uid to prevent global GUID clash
if (!$content) {
$pluginhost = new PluginHost();
$pluginhost->load_all(PluginHost::KIND_ALL, $owner_uid);
$pluginhost->load_data();
foreach ($pluginhost->get_hooks(PluginHost::HOOK_GET_FULL_TEXT) as $p) {
$extracted_content = $p->hook_get_full_text($url);
if ($extracted_content) {
$content = $extracted_content;
break;
}
}
}
$content_hash = sha1($content);
if ($labels_str != "") {
$labels = explode(",", $labels_str);
} else {
$labels = array();
}
$rc = false;
if (!$title) $title = $url;
if (!$title && !$url) return false;
if (filter_var($url, FILTER_VALIDATE_URL) === FALSE) return false;
$pdo = Db::pdo();
$pdo->beginTransaction();
// only check for our user data here, others might have shared this with different content etc
$sth = $pdo->prepare("SELECT id FROM ttrss_entries, ttrss_user_entries WHERE
guid = ? AND ref_id = id AND owner_uid = ? LIMIT 1");
$sth->execute([$guid, $owner_uid]);
if ($row = $sth->fetch()) {
$ref_id = $row['id'];
$sth = $pdo->prepare("SELECT int_id FROM ttrss_user_entries WHERE
ref_id = ? AND owner_uid = ? LIMIT 1");
$sth->execute([$ref_id, $owner_uid]);
if ($row = $sth->fetch()) {
$int_id = $row['int_id'];
$sth = $pdo->prepare("UPDATE ttrss_entries SET
content = ?, content_hash = ? WHERE id = ?");
$sth->execute([$content, $content_hash, $ref_id]);
if (DB_TYPE == "pgsql"){
$sth = $pdo->prepare("UPDATE ttrss_entries
SET tsvector_combined = to_tsvector( :ts_content)
WHERE id = :id");
$params = [
":ts_content" => mb_substr(strip_tags($content ), 0, 900000),
":id" => $ref_id];
$sth->execute($params);
}
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET published = true,
last_published = NOW() WHERE
int_id = ? AND owner_uid = ?");
$sth->execute([$int_id, $owner_uid]);
} else {
$sth = $pdo->prepare("INSERT INTO ttrss_user_entries
(ref_id, uuid, feed_id, orig_feed_id, owner_uid, published, tag_cache, label_cache,
last_read, note, unread, last_published)
VALUES
(?, '', NULL, NULL, ?, true, '', '', NOW(), '', false, NOW())");
$sth->execute([$ref_id, $owner_uid]);
}
if (count($labels) != 0) {
foreach ($labels as $label) {
Labels::add_article($ref_id, trim($label), $owner_uid);
}
}
$rc = true;
} else {
$sth = $pdo->prepare("INSERT INTO ttrss_entries
(title, guid, link, updated, content, content_hash, date_entered, date_updated)
VALUES
(?, ?, ?, NOW(), ?, ?, NOW(), NOW())");
$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()) {
$ref_id = $row["id"];
if (DB_TYPE == "pgsql"){
$sth = $pdo->prepare("UPDATE ttrss_entries
SET tsvector_combined = to_tsvector( :ts_content)
WHERE id = :id");
$params = [
":ts_content" => mb_substr(strip_tags($content ), 0, 900000),
":id" => $ref_id];
$sth->execute($params);
}
$sth = $pdo->prepare("INSERT INTO ttrss_user_entries
(ref_id, uuid, feed_id, orig_feed_id, owner_uid, published, tag_cache, label_cache,
last_read, note, unread, last_published)
VALUES
(?, '', NULL, NULL, ?, true, '', '', NOW(), '', false, NOW())");
$sth->execute([$ref_id, $owner_uid]);
if (count($labels) != 0) {
foreach ($labels as $label) {
Labels::add_article($ref_id, trim($label), $owner_uid);
}
}
$rc = true;
}
}
$pdo->commit();
return $rc;
}
function editArticleTags() {
$param = clean($_REQUEST['param']);
$tags = Article::get_article_tags($param);
$tags_str = join(", ", $tags);
print_hidden("id", "$param");
print_hidden("op", "article");
print_hidden("method", "setArticleTags");
print "<header class='horizontal'>" . __("Tags for this article (separated by commas):")."</header>";
print "<section>";
print "<textarea dojoType='dijit.form.SimpleTextarea' rows='4'
style='height : 100px; font-size : 12px; width : 98%' id='tags_str'
name='tags_str'>$tags_str</textarea>
<div class='autocomplete' id='tags_choices'
style='display:none'></div>";
print "</section>";
print "<footer>";
print "<button dojoType='dijit.form.Button'
type='submit' class='alt-primary' onclick=\"dijit.byId('editTagsDlg').execute()\">".__('Save')."</button> ";
print "<button dojoType='dijit.form.Button'
onclick=\"dijit.byId('editTagsDlg').hide()\">".__('Cancel')."</button>";
print "</footer>";
}
function setScore() {
$ids = explode(",", clean($_REQUEST['id']));
$score = (int)clean($_REQUEST['score']);
$ids_qmarks = arr_qmarks($ids);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
score = ? WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
$sth->execute(array_merge([$score], $ids, [$_SESSION['uid']]));
print json_encode(["id" => $ids, "score" => (int)$score]);
}
function getScore() {
$id = clean($_REQUEST['id']);
$sth = $this->pdo->prepare("SELECT score FROM ttrss_user_entries WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$id, $_SESSION['uid']]);
$row = $sth->fetch();
$score = $row['score'];
print json_encode(["id" => $id, "score" => (int)$score]);
}
function setArticleTags() {
$id = clean($_REQUEST["id"]);
$tags_str = clean($_REQUEST["tags_str"]);
$tags = array_unique(trim_array(explode(",", $tags_str)));
$this->pdo->beginTransaction();
$sth = $this->pdo->prepare("SELECT int_id FROM ttrss_user_entries WHERE
ref_id = ? AND owner_uid = ? LIMIT 1");
$sth->execute([$id, $_SESSION['uid']]);
if ($row = $sth->fetch()) {
$tags_to_cache = array();
$int_id = $row['int_id'];
$sth = $this->pdo->prepare("DELETE FROM ttrss_tags WHERE
post_int_id = ? AND owner_uid = ?");
$sth->execute([$int_id, $_SESSION['uid']]);
foreach ($tags as $tag) {
$tag = Article::sanitize_tag($tag);
if (!Article::tag_is_valid($tag)) {
continue;
}
if (preg_match("/^[0-9]*$/", $tag)) {
continue;
}
// print "<!-- $id : $int_id : $tag -->";
if ($tag != '') {
$sth = $this->pdo->prepare("INSERT INTO ttrss_tags
(post_int_id, owner_uid, tag_name)
VALUES (?, ?, ?)");
$sth->execute([$int_id, $_SESSION['uid'], $tag]);
}
array_push($tags_to_cache, $tag);
}
/* update tag cache */
sort($tags_to_cache);
$tags_str = join(",", $tags_to_cache);
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries
SET tag_cache = ? WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$tags_str, $id, $_SESSION['uid']]);
}
$this->pdo->commit();
$tags = Article::get_article_tags($id);
$tags_str = $this->format_tags_string($tags, $id);
$tags_str_full = join(", ", $tags);
if (!$tags_str_full) $tags_str_full = __("no tags");
print json_encode(array("id" => (int)$id,
"content" => $tags_str, "content_full" => $tags_str_full));
}
function completeTags() {
$search = clean($_REQUEST["search"]);
$sth = $this->pdo->prepare("SELECT DISTINCT tag_name FROM ttrss_tags
WHERE owner_uid = ? AND
tag_name LIKE ? ORDER BY tag_name
LIMIT 10");
$sth->execute([$_SESSION['uid'], "$search%"]);
print "<ul>";
while ($line = $sth->fetch()) {
print "<li>" . $line["tag_name"] . "</li>";
}
print "</ul>";
}
function assigntolabel() {
return $this->labelops(true);
}
function removefromlabel() {
return $this->labelops(false);
}
private function labelops($assign) {
$reply = array();
$ids = explode(",", clean($_REQUEST["ids"]));
$label_id = clean($_REQUEST["lid"]);
$label = Labels::find_caption($label_id, $_SESSION["uid"]);
$reply["info-for-headlines"] = array();
if ($label) {
foreach ($ids as $id) {
if ($assign)
Labels::add_article($id, $label, $_SESSION["uid"]);
else
Labels::remove_article($id, $label, $_SESSION["uid"]);
$labels = $this->get_article_labels($id, $_SESSION["uid"]);
array_push($reply["info-for-headlines"],
array("id" => $id, "labels" => $this->format_article_labels($labels)));
}
}
$reply["message"] = "UPDATE_COUNTERS";
print json_encode($reply);
}
function getArticleFeed($id) {
$sth = $this->pdo->prepare("SELECT feed_id FROM ttrss_user_entries
WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$id, $_SESSION['uid']]);
if ($row = $sth->fetch()) {
return $row["feed_id"];
} else {
return 0;
}
}
static function format_article_enclosures($id, $always_display_enclosures,
$article_content, $hide_images = false) {
$result = Article::get_article_enclosures($id);
$rv = '';
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FORMAT_ENCLOSURES) as $plugin) {
$retval = $plugin->hook_format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images);
if (is_array($retval)) {
$rv = $retval[0];
$result = $retval[1];
} else {
$rv = $retval;
}
}
unset($retval); // Unset to prevent breaking render if there are no HOOK_RENDER_ENCLOSURE hooks below.
if ($rv === '' && !empty($result)) {
$entries_html = array();
$entries = array();
$entries_inline = array();
foreach ($result as $line) {
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ENCLOSURE_ENTRY) as $plugin) {
$line = $plugin->hook_enclosure_entry($line);
}
$url = $line["content_url"];
$ctype = $line["content_type"];
$title = $line["title"];
$width = $line["width"];
$height = $line["height"];
if (!$ctype) $ctype = __("unknown type");
//$filename = substr($url, strrpos($url, "/")+1);
$filename = basename($url);
$player = format_inline_player($url, $ctype);
if ($player) array_push($entries_inline, $player);
# $entry .= " <a target=\"_blank\" href=\"" . htmlspecialchars($url) . "\" rel=\"noopener noreferrer\">" .
# $filename . " (" . $ctype . ")" . "</a>";
$entry = "<div onclick=\"popupOpenUrl('".htmlspecialchars($url)."')\"
dojoType=\"dijit.MenuItem\">$filename ($ctype)</div>";
array_push($entries_html, $entry);
$entry = array();
$entry["type"] = $ctype;
$entry["filename"] = $filename;
$entry["url"] = $url;
$entry["title"] = $title;
$entry["width"] = $width;
$entry["height"] = $height;
array_push($entries, $entry);
}
if ($_SESSION['uid'] && !get_pref("STRIP_IMAGES") && !$_SESSION["bw_limit"]) {
if ($always_display_enclosures ||
!preg_match("/<img/i", $article_content)) {
foreach ($entries as $entry) {
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ENCLOSURE) as $plugin)
$retval = $plugin->hook_render_enclosure($entry, $hide_images);
if ($retval) {
$rv .= $retval;
} else {
if (preg_match("/image/", $entry["type"])) {
if (!$hide_images) {
$encsize = '';
if ($entry['height'] > 0)
$encsize .= ' height="' . intval($entry['height']) . '"';
if ($entry['width'] > 0)
$encsize .= ' width="' . intval($entry['width']) . '"';
$rv .= "<p><img
alt=\"".htmlspecialchars($entry["filename"])."\"
src=\"" .htmlspecialchars($entry["url"]) . "\"
" . $encsize . " /></p>";
} else {
$rv .= "<p><a target=\"_blank\" rel=\"noopener noreferrer\"
href=\"".htmlspecialchars($entry["url"])."\"
>" .htmlspecialchars($entry["url"]) . "</a></p>";
}
if ($entry['title']) {
$rv.= "<div class=\"enclosure_title\">${entry['title']}</div>";
}
}
}
}
}
}
if (count($entries_inline) > 0) {
//$rv .= "<hr clear='both'/>";
foreach ($entries_inline as $entry) { $rv .= $entry; };
$rv .= "<br clear='both'/>";
}
$rv .= "<div class=\"attachments\" dojoType=\"fox.form.DropDownButton\">".
"<span>" . __('Attachments')."</span>";
$rv .= "<div dojoType=\"dijit.Menu\" style=\"display: none;\">";
foreach ($entries as $entry) {
if ($entry["title"])
$title = " &mdash; " . truncate_string($entry["title"], 30);
else
$title = "";
if ($entry["filename"])
$filename = truncate_middle(htmlspecialchars($entry["filename"]), 60);
else
$filename = "";
$rv .= "<div onclick='popupOpenUrl(\"".htmlspecialchars($entry["url"])."\")'
dojoType=\"dijit.MenuItem\">".$filename . $title."</div>";
};
$rv .= "</div>";
$rv .= "</div>";
}
return $rv;
}
static function get_article_tags($id, $owner_uid = 0, $tag_cache = false) {
$a_id = $id;
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT DISTINCT tag_name,
owner_uid as owner FROM ttrss_tags
WHERE post_int_id = (SELECT int_id FROM ttrss_user_entries WHERE
ref_id = ? AND owner_uid = ? LIMIT 1) ORDER BY tag_name");
$tags = array();
/* check cache first */
if ($tag_cache === false) {
$csth = $pdo->prepare("SELECT tag_cache FROM ttrss_user_entries
WHERE ref_id = ? AND owner_uid = ?");
$csth->execute([$id, $owner_uid]);
if ($row = $csth->fetch()) $tag_cache = $row["tag_cache"];
}
if ($tag_cache) {
$tags = explode(",", $tag_cache);
} else {
/* do it the hard way */
$sth->execute([$a_id, $owner_uid]);
while ($tmp_line = $sth->fetch()) {
array_push($tags, $tmp_line["tag_name"]);
}
/* update the cache */
$tags_str = join(",", $tags);
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET tag_cache = ? WHERE ref_id = ?
AND owner_uid = ?");
$sth->execute([$tags_str, $id, $owner_uid]);
}
return $tags;
}
static function format_tags_string($tags) {
if (!is_array($tags) || count($tags) == 0) {
return __("no tags");
} else {
$maxtags = min(5, count($tags));
$tags_str = "";
for ($i = 0; $i < $maxtags; $i++) {
$tags_str .= "<a class=\"tag\" href=\"#\" onclick=\"Feeds.open({feed:'".$tags[$i]."'})\">" . $tags[$i] . "</a>, ";
}
$tags_str = mb_substr($tags_str, 0, mb_strlen($tags_str)-2);
if (count($tags) > $maxtags)
$tags_str .= ", &hellip;";
return $tags_str;
}
}
static function format_article_labels($labels) {
if (!is_array($labels)) return '';
$labels_str = "";
foreach ($labels as $l) {
$labels_str .= sprintf("<div class='label'
style='color : %s; background-color : %s'>%s</div>",
$l[2], $l[3], $l[1]);
}
return $labels_str;
}
static function format_article_note($id, $note, $allow_edit = true) {
if ($allow_edit) {
$onclick = "onclick='Plugins.Note.edit($id)'";
$note_class = 'editable';
} else {
$onclick = '';
$note_class = '';
}
return "<div class='article-note $note_class'>
<i class='material-icons'>note</i>
<div $onclick class='body'>$note</div>
</div>";
return $str;
}
static function get_article_enclosures($id) {
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT * FROM ttrss_enclosures
WHERE post_id = ? AND content_url != ''");
$sth->execute([$id]);
$rv = array();
while ($line = $sth->fetch()) {
if (file_exists(CACHE_DIR . '/images/' . sha1($line["content_url"]))) {
$line["content_url"] = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($line["content_url"]);
}
array_push($rv, $line);
}
return $rv;
}
static function purge_orphans() {
// purge orphaned posts in main content table
if (DB_TYPE == "mysql")
$limit_qpart = "LIMIT 5000";
else
$limit_qpart = "";
$pdo = Db::pdo();
$res = $pdo->query("DELETE FROM ttrss_entries WHERE
NOT EXISTS (SELECT ref_id FROM ttrss_user_entries WHERE ref_id = id) $limit_qpart");
if (Debug::enabled()) {
$rows = $res->rowCount();
Debug::log("Purged $rows orphaned posts.");
}
}
static function catchupArticlesById($ids, $cmode, $owner_uid = false) {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
$ids_qmarks = arr_qmarks($ids);
if ($cmode == 1) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
unread = true
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else if ($cmode == 2) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
unread = NOT unread,last_read = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else {
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
unread = false,last_read = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
}
$sth->execute(array_merge($ids, [$owner_uid]));
/* update ccache */
$sth = $pdo->prepare("SELECT DISTINCT feed_id FROM ttrss_user_entries
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
$sth->execute(array_merge($ids, [$owner_uid]));
while ($line = $sth->fetch()) {
CCache::update($line["feed_id"], $owner_uid);
}
}
static function getLastArticleId() {
$pdo = DB::pdo();
$sth = $pdo->prepare("SELECT ref_id AS id FROM ttrss_user_entries
WHERE owner_uid = ? ORDER BY ref_id DESC LIMIT 1");
$sth->execute([$_SESSION['uid']]);
if ($row = $sth->fetch()) {
return $row['id'];
} else {
return -1;
}
}
static function get_article_labels($id, $owner_uid = false) {
$rv = array();
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT label_cache FROM
ttrss_user_entries WHERE ref_id = ? AND owner_uid = ?");
$sth->execute([$id, $owner_uid]);
if ($row = $sth->fetch()) {
$label_cache = $row["label_cache"];
if ($label_cache) {
$tmp = json_decode($label_cache, true);
if (!$tmp || $tmp["no-labels"] == 1)
return $rv;
else
return $tmp;
}
}
$sth = $pdo->prepare("SELECT DISTINCT label_id,caption,fg_color,bg_color
FROM ttrss_labels2, ttrss_user_labels2
WHERE id = label_id
AND article_id = ?
AND owner_uid = ?
ORDER BY caption");
$sth->execute([$id, $owner_uid]);
while ($line = $sth->fetch()) {
$rk = array(Labels::label_to_feed_id($line["label_id"]),
$line["caption"], $line["fg_color"],
$line["bg_color"]);
array_push($rv, $rk);
}
if (count($rv) > 0)
Labels::update_cache($owner_uid, $id, $rv);
else
Labels::update_cache($owner_uid, $id, array("no-labels" => 1));
return $rv;
}
static function sanitize_tag($tag) {
$tag = trim($tag);
$tag = mb_strtolower($tag, 'utf-8');
$tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
if (DB_TYPE == "mysql") {
$tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
}
return $tag;
}
static function tag_is_valid($tag) {
if (!$tag || is_numeric($tag) || mb_strlen($tag) > 250)
return false;
return true;
}
}

View File

@@ -1,62 +0,0 @@
<?php
class Auth_Base {
private $pdo;
function __construct() {
$this->pdo = Db::pdo();
}
/**
* @SuppressWarnings(unused)
*/
function check_password($owner_uid, $password) {
return false;
}
/**
* @SuppressWarnings(unused)
*/
function authenticate($login, $password) {
return false;
}
// Auto-creates specified user if allowed by system configuration
// Can be used instead of find_user_by_login() by external auth modules
function auto_create_user($login, $password = false) {
if ($login && defined('AUTH_AUTO_CREATE') && AUTH_AUTO_CREATE) {
$user_id = $this->find_user_by_login($login);
if (!$password) $password = make_password();
if (!$user_id) {
$salt = substr(bin2hex(get_random_bytes(125)), 0, 250);
$pwd_hash = encrypt_password($password, $salt, true);
$sth = $this->pdo->prepare("INSERT INTO ttrss_users
(login,access_level,last_login,created,pwd_hash,salt)
VALUES (?, 0, null, NOW(), ?,?)");
$sth->execute([$login, $pwd_hash, $salt]);
return $this->find_user_by_login($login);
} else {
return $user_id;
}
}
return $this->find_user_by_login($login);
}
function find_user_by_login($login) {
$sth = $this->pdo->prepare("SELECT id FROM ttrss_users WHERE
login = ?");
$sth->execute([$login]);
if ($row = $sth->fetch()) {
return $row["id"];
} else {
return false;
}
}
}

View File

@@ -1,163 +0,0 @@
<?php
class Backend extends Handler {
function loading() {
header("Content-type: text/html");
print __("Loading, please wait...") . " " .
"<img src='images/indicator_tiny.gif'>";
}
function digestTest() {
if (isset($_SESSION['uid'])) {
header("Content-type: text/html");
$rv = Digest::prepare_headlines_digest($_SESSION['uid'], 1, 1000);
print "<h1>HTML</h1>";
print $rv[0];
print "<h1>Plain text</h1>";
print "<pre>".$rv[3]."</pre>";
} else {
print error_json(6);
}
}
private function display_main_help() {
$info = get_hotkeys_info();
$imap = get_hotkeys_map();
$omap = array();
foreach ($imap[1] as $sequence => $action) {
if (!isset($omap[$action])) $omap[$action] = array();
array_push($omap[$action], $sequence);
}
print "<ul class='panel panel-scrollable hotkeys-help' style='height : 300px'>";
print "<h2>" . __("Keyboard Shortcuts") . "</h2>";
foreach ($info as $section => $hotkeys) {
print "<li><hr></li>";
print "<li><h3>" . $section . "</h3></li>";
foreach ($hotkeys as $action => $description) {
if (is_array($omap[$action])) {
foreach ($omap[$action] as $sequence) {
if (strpos($sequence, "|") !== FALSE) {
$sequence = substr($sequence,
strpos($sequence, "|")+1,
strlen($sequence));
} else {
$keys = explode(" ", $sequence);
for ($i = 0; $i < count($keys); $i++) {
if (strlen($keys[$i]) > 1) {
$tmp = '';
foreach (str_split($keys[$i]) as $c) {
switch ($c) {
case '*':
$tmp .= __('Shift') . '+';
break;
case '^':
$tmp .= __('Ctrl') . '+';
break;
default:
$tmp .= $c;
}
}
$keys[$i] = $tmp;
}
}
$sequence = join(" ", $keys);
}
print "<li>";
print "<div class='hk'><code>$sequence</code></div>";
print "<div class='desc'>$description</div>";
print "</li>";
}
}
}
}
print "</ul>";
}
function help() {
$topic = basename(clean($_REQUEST["topic"])); // only one for now
if ($topic == "main") {
$info = get_hotkeys_info();
$imap = get_hotkeys_map();
$omap = array();
foreach ($imap[1] as $sequence => $action) {
if (!isset($omap[$action])) $omap[$action] = array();
array_push($omap[$action], $sequence);
}
print "<ul class='panel panel-scrollable hotkeys-help' style='height : 300px'>";
$cur_section = "";
foreach ($info as $section => $hotkeys) {
if ($cur_section) print "<li>&nbsp;</li>";
print "<li><h3>" . $section . "</h3></li>";
$cur_section = $section;
foreach ($hotkeys as $action => $description) {
if (is_array($omap[$action])) {
foreach ($omap[$action] as $sequence) {
if (strpos($sequence, "|") !== FALSE) {
$sequence = substr($sequence,
strpos($sequence, "|")+1,
strlen($sequence));
} else {
$keys = explode(" ", $sequence);
for ($i = 0; $i < count($keys); $i++) {
if (strlen($keys[$i]) > 1) {
$tmp = '';
foreach (str_split($keys[$i]) as $c) {
switch ($c) {
case '*':
$tmp .= __('Shift') . '+';
break;
case '^':
$tmp .= __('Ctrl') . '+';
break;
default:
$tmp .= $c;
}
}
$keys[$i] = $tmp;
}
}
$sequence = join(" ", $keys);
}
print "<li>";
print "<div class='hk'><code>$sequence</code></div>";
print "<div class='desc'>$description</div>";
print "</li>";
}
}
}
}
print "</ul>";
}
print "<footer class='text-center'>";
print "<button dojoType='dijit.form.Button'
onclick=\"return dijit.byId('helpDlg').hide()\">".__('Close this window')."</button>";
print "</footer>";
}
}

View File

@@ -1,211 +0,0 @@
<?php
class CCache {
static function zero_all($owner_uid) {
$pdo = Db::pdo();
$sth = $pdo->prepare("UPDATE ttrss_counters_cache SET
value = 0 WHERE owner_uid = ?");
$sth->execute([$owner_uid]);
$sth = $pdo->prepare("UPDATE ttrss_cat_counters_cache SET
value = 0 WHERE owner_uid = ?");
$sth->execute([$owner_uid]);
}
static function remove($feed_id, $owner_uid, $is_cat = false) {
$feed_id = (int) $feed_id;
if (!$is_cat) {
$table = "ttrss_counters_cache";
} else {
$table = "ttrss_cat_counters_cache";
}
$pdo = Db::pdo();
$sth = $pdo->prepare("DELETE FROM $table WHERE
feed_id = ? AND owner_uid = ?");
$sth->execute([$feed_id, $owner_uid]);
}
static function update_all($owner_uid) {
$pdo = Db::pdo();
if (get_pref('ENABLE_FEED_CATS', $owner_uid)) {
$sth = $pdo->prepare("SELECT feed_id FROM ttrss_cat_counters_cache
WHERE feed_id > 0 AND owner_uid = ?");
$sth->execute([$owner_uid]);
while ($line = $sth->fetch()) {
CCache::update($line["feed_id"], $owner_uid, true);
}
/* We have to manually include category 0 */
CCache::update(0, $owner_uid, true);
} else {
$sth = $pdo->prepare("SELECT feed_id FROM ttrss_counters_cache
WHERE feed_id > 0 AND owner_uid = ?");
$sth->execute([$owner_uid]);
while ($line = $sth->fetch()) {
print CCache::update($line["feed_id"], $owner_uid);
}
}
}
static function find($feed_id, $owner_uid, $is_cat = false,
$no_update = false) {
// "" (null) is valid and should be cast to 0 (uncategorized)
// everything else i.e. tags are not
if (!is_numeric($feed_id) && $feed_id)
return;
$feed_id = (int) $feed_id;
if (!$is_cat) {
$table = "ttrss_counters_cache";
} else {
$table = "ttrss_cat_counters_cache";
}
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT value FROM $table
WHERE owner_uid = ? AND feed_id = ?
LIMIT 1");
$sth->execute([$owner_uid, $feed_id]);
if ($row = $sth->fetch()) {
return $row["value"];
} else {
if ($no_update) {
return -1;
} else {
return CCache::update($feed_id, $owner_uid, $is_cat);
}
}
}
static function update($feed_id, $owner_uid, $is_cat = false,
$update_pcat = true, $pcat_fast = false) {
// "" (null) is valid and should be cast to 0 (uncategorized)
// everything else i.e. tags are not
if (!is_numeric($feed_id) && $feed_id)
return;
$feed_id = (int) $feed_id;
$prev_unread = CCache::find($feed_id, $owner_uid, $is_cat, true);
/* When updating a label, all we need to do is recalculate feed counters
* because labels are not cached */
if ($feed_id < 0) {
CCache::update_all($owner_uid);
return;
}
if (!$is_cat) {
$table = "ttrss_counters_cache";
} else {
$table = "ttrss_cat_counters_cache";
}
$pdo = Db::pdo();
if ($is_cat && $feed_id >= 0) {
/* Recalculate counters for child feeds */
if (!$pcat_fast) {
$sth = $pdo->prepare("SELECT id FROM ttrss_feeds
WHERE owner_uid = :uid AND
(cat_id = :cat OR (:cat = 0 AND cat_id IS NULL))");
$sth->execute([":uid" => $owner_uid, ":cat" => $feed_id]);
while ($line = $sth->fetch()) {
CCache::update((int)$line["id"], $owner_uid, false, false);
}
}
$sth = $pdo->prepare("SELECT SUM(value) AS sv
FROM ttrss_counters_cache, ttrss_feeds
WHERE ttrss_feeds.id = feed_id AND
(cat_id = :cat OR (:cat = 0 AND cat_id IS NULL)) AND
ttrss_counters_cache.owner_uid = :uid AND
ttrss_feeds.owner_uid = :uid");
$sth->execute([":uid" => $owner_uid, ":cat" => $feed_id]);
$row = $sth->fetch();
$unread = (int) $row["sv"];
} else {
$unread = (int) Feeds::getFeedArticles($feed_id, $is_cat, true, $owner_uid);
}
$tr_in_progress = false;
try {
$pdo->beginTransaction();
} catch (Exception $e) {
$tr_in_progress = true;
}
$sth = $pdo->prepare("SELECT feed_id FROM $table
WHERE owner_uid = ? AND feed_id = ? LIMIT 1");
$sth->execute([$owner_uid, $feed_id]);
if ($sth->fetch()) {
$sth = $pdo->prepare("UPDATE $table SET
value = ?, updated = NOW() WHERE
feed_id = ? AND owner_uid = ?");
$sth->execute([$unread, $feed_id, $owner_uid]);
} else {
$sth = $pdo->prepare("INSERT INTO $table
(feed_id, value, owner_uid, updated)
VALUES
(?, ?, ?, NOW())");
$sth->execute([$feed_id, $unread, $owner_uid]);
}
if (!$tr_in_progress) $pdo->commit();
if ($feed_id > 0 && $prev_unread != $unread) {
if (!$is_cat) {
/* Update parent category */
if ($update_pcat) {
$sth = $pdo->prepare("SELECT cat_id FROM ttrss_feeds
WHERE owner_uid = ? AND id = ?");
$sth->execute([$owner_uid, $feed_id]);
if ($row = $sth->fetch()) {
CCache::update((int)$row["cat_id"], $owner_uid, true, true, true);
}
}
}
} else if ($feed_id < 0) {
CCache::update_all($owner_uid);
}
return $unread;
}
}

View File

@@ -1,216 +0,0 @@
<?php
class Counters {
static function getAllCounters() {
$data = Counters::getGlobalCounters();
$data = array_merge($data, Counters::getVirtCounters());
$data = array_merge($data, Counters::getLabelCounters());
$data = array_merge($data, Counters::getFeedCounters());
$data = array_merge($data, Counters::getCategoryCounters());
return $data;
}
static function getCategoryCounters() {
$ret_arr = array();
/* Labels category */
$cv = array("id" => -2, "kind" => "cat",
"counter" => Feeds::getCategoryUnread(-2));
array_push($ret_arr, $cv);
$pdo = DB::pdo();
$sth = $pdo->prepare("SELECT ttrss_feed_categories.id AS cat_id, value AS unread,
(SELECT COUNT(id) FROM ttrss_feed_categories AS c2
WHERE c2.parent_cat = ttrss_feed_categories.id) AS num_children
FROM ttrss_feed_categories, ttrss_cat_counters_cache
WHERE ttrss_cat_counters_cache.feed_id = ttrss_feed_categories.id AND
ttrss_cat_counters_cache.owner_uid = ttrss_feed_categories.owner_uid AND
ttrss_feed_categories.owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
while ($line = $sth->fetch()) {
$line["cat_id"] = (int) $line["cat_id"];
if ($line["num_children"] > 0) {
$child_counter = Feeds::getCategoryChildrenUnread($line["cat_id"], $_SESSION["uid"]);
} else {
$child_counter = 0;
}
$cv = array("id" => $line["cat_id"], "kind" => "cat",
"counter" => $line["unread"] + $child_counter);
array_push($ret_arr, $cv);
}
/* Special case: NULL category doesn't actually exist in the DB */
$cv = array("id" => 0, "kind" => "cat",
"counter" => (int) CCache::find(0, $_SESSION["uid"], true));
array_push($ret_arr, $cv);
return $ret_arr;
}
static function getGlobalCounters($global_unread = -1) {
$ret_arr = array();
if ($global_unread == -1) {
$global_unread = Feeds::getGlobalUnread();
}
$cv = array("id" => "global-unread",
"counter" => (int) $global_unread);
array_push($ret_arr, $cv);
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT COUNT(id) AS fn FROM
ttrss_feeds WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
$subscribed_feeds = $row["fn"];
$cv = array("id" => "subscribed-feeds",
"counter" => (int) $subscribed_feeds);
array_push($ret_arr, $cv);
return $ret_arr;
}
static function getVirtCounters() {
$ret_arr = array();
for ($i = 0; $i >= -4; $i--) {
$count = getFeedUnread($i);
if ($i == 0 || $i == -1 || $i == -2)
$auxctr = Feeds::getFeedArticles($i, false);
else
$auxctr = 0;
$cv = array("id" => $i,
"counter" => (int) $count,
"auxcounter" => (int) $auxctr);
// if (get_pref('EXTENDED_FEEDLIST'))
// $cv["xmsg"] = getFeedArticles($i)." ".__("total");
array_push($ret_arr, $cv);
}
$feeds = PluginHost::getInstance()->get_feeds(-1);
if (is_array($feeds)) {
foreach ($feeds as $feed) {
$cv = array("id" => PluginHost::pfeed_to_feed_id($feed['id']),
"counter" => $feed['sender']->get_unread($feed['id']));
if (method_exists($feed['sender'], 'get_total'))
$cv["auxcounter"] = $feed['sender']->get_total($feed['id']);
array_push($ret_arr, $cv);
}
}
return $ret_arr;
}
static function getLabelCounters($descriptions = false) {
$ret_arr = array();
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT id,caption,SUM(CASE WHEN u1.unread = true THEN 1 ELSE 0 END) AS unread, COUNT(u1.unread) AS total
FROM ttrss_labels2 LEFT JOIN ttrss_user_labels2 ON
(ttrss_labels2.id = label_id)
LEFT JOIN ttrss_user_entries AS u1 ON u1.ref_id = article_id
WHERE ttrss_labels2.owner_uid = :uid AND u1.owner_uid = :uid
GROUP BY ttrss_labels2.id,
ttrss_labels2.caption");
$sth->execute([":uid" => $_SESSION['uid']]);
while ($line = $sth->fetch()) {
$id = Labels::label_to_feed_id($line["id"]);
$cv = array("id" => $id,
"counter" => (int) $line["unread"],
"auxcounter" => (int) $line["total"]);
if ($descriptions)
$cv["description"] = $line["caption"];
array_push($ret_arr, $cv);
}
return $ret_arr;
}
static function getFeedCounters($active_feed = false) {
$ret_arr = array();
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT ttrss_feeds.id,
ttrss_feeds.title,
".SUBSTRING_FOR_DATE."(ttrss_feeds.last_updated,1,19) AS last_updated,
last_error, value AS count
FROM ttrss_feeds, ttrss_counters_cache
WHERE ttrss_feeds.owner_uid = ?
AND ttrss_counters_cache.owner_uid = ttrss_feeds.owner_uid
AND ttrss_counters_cache.feed_id = ttrss_feeds.id");
$sth->execute([$_SESSION['uid']]);
while ($line = $sth->fetch()) {
$id = $line["id"];
$count = $line["count"];
$last_error = htmlspecialchars($line["last_error"]);
$last_updated = make_local_datetime($line['last_updated'], false);
if (Feeds::feedHasIcon($id)) {
$has_img = filemtime(Feeds::getIconFile($id));
} else {
$has_img = false;
}
if (date('Y') - date('Y', strtotime($line['last_updated'])) > 2)
$last_updated = '';
$cv = array("id" => $id,
"updated" => $last_updated,
"counter" => (int) $count,
"has_img" => (int) $has_img);
if ($last_error)
$cv["error"] = $last_error;
// if (get_pref('EXTENDED_FEEDLIST'))
// $cv["xmsg"] = getFeedArticles($id)." ".__("total");
if ($active_feed && $id == $active_feed)
$cv["title"] = truncate_string($line["title"], 30);
array_push($ret_arr, $cv);
}
return $ret_arr;
}
}

View File

@@ -1,116 +0,0 @@
<?php
class Db
{
/* @var Db $instance */
private static $instance;
/* @var IDb $adapter */
private $adapter;
private $link;
/* @var PDO $pdo */
private $pdo;
private function __clone() {
//
}
private function legacy_connect() {
user_error("Legacy connect requested to " . DB_TYPE, E_USER_NOTICE);
$er = error_reporting(E_ALL);
switch (DB_TYPE) {
case "mysql":
$this->adapter = new Db_Mysqli();
break;
case "pgsql":
$this->adapter = new Db_Pgsql();
break;
default:
die("Unknown DB_TYPE: " . DB_TYPE);
}
if (!$this->adapter) {
print("Error initializing database adapter for " . DB_TYPE);
exit(100);
}
$this->link = $this->adapter->connect(DB_HOST, DB_USER, DB_PASS, DB_NAME, defined('DB_PORT') ? DB_PORT : "");
if (!$this->link) {
print("Error connecting through adapter: " . $this->adapter->last_error());
exit(101);
}
error_reporting($er);
}
// this really shouldn't be used unless a separate PDO connection is needed
// normal usage is Db::pdo()->prepare(...) etc
public function pdo_connect() {
$db_port = defined('DB_PORT') && DB_PORT ? ';port=' . DB_PORT : '';
$db_host = defined('DB_HOST') && DB_HOST ? ';host=' . DB_HOST : '';
try {
$pdo = new PDO(DB_TYPE . ':dbname=' . DB_NAME . $db_host . $db_port,
DB_USER,
DB_PASS);
} catch (Exception $e) {
print "<pre>Exception while creating PDO object:" . $e->getMessage() . "</pre>";
exit(101);
}
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
if (DB_TYPE == "pgsql") {
$pdo->query("set client_encoding = 'UTF-8'");
$pdo->query("set datestyle = 'ISO, european'");
$pdo->query("set TIME ZONE 0");
$pdo->query("set cpu_tuple_cost = 0.5");
} else if (DB_TYPE == "mysql") {
$pdo->query("SET time_zone = '+0:0'");
if (defined('MYSQL_CHARSET') && MYSQL_CHARSET) {
$pdo->query("SET NAMES " . MYSQL_CHARSET);
}
}
return $pdo;
}
public static function instance() {
if (self::$instance == null)
self::$instance = new self();
return self::$instance;
}
public static function get() {
if (self::$instance == null)
self::$instance = new self();
if (!self::$instance->adapter) {
self::$instance->legacy_connect();
}
return self::$instance->adapter;
}
public static function pdo() {
if (self::$instance == null)
self::$instance = new self();
if (!self::$instance->pdo) {
self::$instance->pdo = self::$instance->pdo_connect();
}
return self::$instance->pdo;
}
}

View File

@@ -1,85 +0,0 @@
<?php
class Db_Mysqli implements IDb {
private $link;
private $last_error;
function connect($host, $user, $pass, $db, $port) {
if ($port)
$this->link = mysqli_connect($host, $user, $pass, $db, $port);
else
$this->link = mysqli_connect($host, $user, $pass, $db);
if ($this->link) {
$this->init();
return $this->link;
} else {
print("Unable to connect to database (as $user to $host, database $db): " . mysqli_connect_error());
exit(102);
}
}
function escape_string($s, $strip_tags = true) {
if ($strip_tags) $s = strip_tags($s);
return mysqli_real_escape_string($this->link, $s);
}
function query($query, $die_on_error = true) {
$result = @mysqli_query($this->link, $query);
if (!$result) {
$this->last_error = @mysqli_error($this->link);
@mysqli_query($this->link, "ROLLBACK");
user_error("Query $query failed: " . ($this->link ? $this->last_error : "No connection"),
$die_on_error ? E_USER_ERROR : E_USER_WARNING);
}
return $result;
}
function fetch_assoc($result) {
return mysqli_fetch_assoc($result);
}
function num_rows($result) {
return mysqli_num_rows($result);
}
function fetch_result($result, $row, $param) {
if (mysqli_data_seek($result, $row)) {
$line = mysqli_fetch_assoc($result);
return $line[$param];
} else {
return false;
}
}
function close() {
return mysqli_close($this->link);
}
function affected_rows($result) {
return mysqli_affected_rows($this->link);
}
function last_error() {
return mysqli_error($this->link);
}
function last_query_error() {
return $this->last_error;
}
function init() {
$this->query("SET time_zone = '+0:0'");
if (defined('MYSQL_CHARSET') && MYSQL_CHARSET) {
mysqli_set_charset($this->link, MYSQL_CHARSET);
}
return true;
}
}

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