**Change(s)**
- Add missing punctuation
- Add missing spaces in the date format
- Add some new translations
**File(s) changed**
`app/i18n/zh-TW/{admin,conf,gen,install,sub}.php`
`README.md`
`README.fr.md`
Labels (tags) were sorted by the database's raw byte/ASCII collation
via ORDER BY name, so accented or non-Latin label names sorted after
all ASCII names instead of near their base letter.
Apply the same FreshRSS_Context::localeCompare() sort used for
categories, feeds, and shares (#8985) to TagDAO::listTags(), fixing
the last remaining sub-item of this issue; the others were already
resolved by #6212.
Fixes#6211
Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Explain that mod_auth_openidc defaults to client_secret_basic since
our reference Apache configuration does not set
OIDCProviderTokenEndpointAuth. If the identity provider's discovery
metadata advertises a token_endpoint_auth_methods_supported array
instead, mod_auth_openidc picks the first mutually supported method
from that array rather than falling back to client_secret_basic.
Also point to the .htaccess override for administrators who need
something else.
This only addresses the token-endpoint-auth-method part of #6102;
the redirect-URI-port question raised in that issue is out of scope
here.
Addresses the token-endpoint-auth-method part of #6102
Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
* Keep search and state filters when marking articles as read
Marking articles as read (e.g. "mark all as read") POSTs to
entry?a=read and, for non-AJAX requests, redirects back to the index.
That redirect rebuilt its parameters without the current `search` and
`state`, so the view fell back to the default state and the active
search was dropped, forcing the user to re-apply their filter.
Carry `search` and `state` through the redirect, mirroring the existing
handling for `order`/`sort`, so the filtered view is preserved.
Fixes#8671
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Reduce comments a bit
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Add a :lang(ja)-scoped font-family in the shared frss.css so kanji/kana
render with Japanese glyphs on Japanese UIs, without affecting Chinese
(zh-CN/zh-TW) users. Revives the intent of PR #5671 with a
cross-platform font list. frss.rtl.css is regenerated via rtlcss.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Saving custom CSS/JS rendered the response from the pre-save state (no
Post/Redirect/Get), so the stylesheet cache-busting URL was stale and
the change only appeared after a manual reload, which reads as erratic
behavior. Redirect back to the extension config after save, matching
the pattern used elsewhere in the codebase.
Fixes#8795
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixes `Uncaught SyntaxError: redeclaration of const freshrssSliderLoadEvent` error shown in console on e.g. `/i/?c=subscription&id=1` due to https://github.com/FreshRSS/FreshRSS/pull/8973
* WebSub: ignore http/https scheme difference in Self URL comparison
The PubSubHubbub endpoint logged 'Self URL does not match registered
canonical URL' whenever a feed's self URL differed from the registered
canonical URL only by http vs https (common with http->https redirects),
spamming the log. Compare the URLs ignoring the http/https scheme while
still requiring host/path/query to match exactly.
Closes#3087
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Stylistic preferences
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
## Problem
The feed **tree** (left sidebar) and the **feed-title view** (main body) order feeds differently for names with accents or symbols. As reported in #8300, a Spanish feed starting with `Ú` appears **last** in the tree, after every ASCII-named feed.
The tree is sorted in PHP by `FreshRSS_Category::sortFeeds()` with `strnatcasecmp()`, which compares raw UTF-8 **bytes** — so any name starting with a non-ASCII character (`0xC3…` for `Á`, `Ñ`, `Ú`, …) sorts after `A–Z`. The main view is sorted by the database according to its collation, which orders accented letters near their base letter — hence the mismatch (as @Alkarex noted in the issue).
Closes#8300.
## Change
`sortFeeds()` now compares with a `Collator` (`ext-intl`, already a hard requirement) for the user's language, falling back to the previous `strnatcasecmp()` if a collator cannot be created:
```php
$collator = \Collator::create(FreshRSS_Context::hasUserConf() ? FreshRSS_Context::userConf()->language : '');
```
Sorting a Spanish set, before / after:
```
old: banana, Manzana, nube, Zorro, Ábaco, Ñandú, Úlcera, árbol
new: Ábaco, árbol, banana, Manzana, nube, Ñandú, Úlcera, Zorro
```
This makes the tree order locale-aware and much closer to the main view. Exact parity with the main view still depends on the database collation (which is admin/DB-specific and cannot be reproduced generically in PHP), but the "accented names sort last" problem is gone.
* Feeds tree: sort feed names with locale-aware collation
The feed tree sorted names byte-wise with strnatcasecmp(), which places
names starting with a non-ASCII character (e.g. an accented capital such
as "U-acute") after every ASCII name. Use a Collator for the user's
language so accented and non-ASCII names sort near their base letter,
closer to the database collation used on the main feed-title view.
For https://github.com/FreshRSS/FreshRSS/issues/8300
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Guard Collator with class_exists() so intl stays optional
The intl extension is only a recommended (not required) dependency, but
sortFeeds() called \Collator::create() unconditionally when a user language
was set. On installs without intl the \Collator class does not exist, so the
call raised a fatal "Class \"Collator\" not found" — effectively promoting
intl from recommended to required.
Guard the call with class_exists('Collator'): when intl is absent, fall back
to the previous byte-wise strnatcasecmp() sort. Locale-aware ordering stays a
progressive enhancement and the listed requirements are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Shorten the sortFeeds() collation comment per review
Trim the rationale comment to a concise four lines as suggested in review,
keeping the locale-aware/fallback and class_exists() reasoning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Stylistic preference
* Factorise localeCompare
* Minor code preference
* Preserve natural locale sort behaviour
* Avoid repeated locale lookup in comparator
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Closes#6223.
Supersedes the investigation draft in #6765.
## Summary
- Removed the remaining explicit uses of the `simple` layout from the email-validation flows.
- Deleted `app/layout/simple.phtml` so those pages now use the shared `layout.phtml`.
- Kept the existing `disable_aside` behavior for the profile page while an email validation token is pending.
## Notes
The replacement for `simple.phtml` is the normal shared layout. Access while email validation is pending is still constrained by the existing `FreshRSS::checkEmailValidated()` route guard, which allows the validation/profile/logout paths and forwards other routes to `user#validateEmail`.
* Add option to keep or reset article sort order when navigating
Adds a new per-user reading option, "Keep the current sort order when
navigating between categories and feeds" (config key: sticky_sort), under
Settings > Reading > "Left navigation: Categories". It defaults to enabled,
which reproduces the current behaviour exactly.
Background (addresses #8953)
----------------------------
Once a sort/order was active in one view, it was propagated to every sidebar
link and to the "mark all as read" redirect. Opening another category then
ignored that category's own defaultSort/defaultOrder, because FreshRSS_Context
only applies those defaults when sort/order are absent from the request. As a
result the current view's sort (whether chosen manually or resolved from that
view's default) "bled" into the next category/feed.
Behaviour of the new option
---------------------------
- ON (default): unchanged. The current sort/order stays sticky while
navigating, exactly as before.
- OFF: sidebar links and the "mark all as read" redirect no longer carry
sort/order, so each target resolves its own order in FreshRSS_Context:
feed/category defaultSort/defaultOrder, falling back to the global Reading
sort criterion when none is configured.
order/sort propagation across the UI (for reference)
----------------------------------------------------
- Categories & feeds (sidebar): now gated by sticky_sort (app/layout/aside_feed.phtml).
- "Mark all as read" -> next category, both the top navigation button and the
stream-footer button: now gated by sticky_sort
(app/Controllers/entryController.php::readAction, non-AJAX redirect).
- Custom user queries: unaffected. A query's link is a self-contained URL built
from that query's own saved parameters, so switching query A -> B never
carried A's sort; no option is needed there.
- Stream transition links when sorting by category/feed name: intentionally
left unchanged (out of scope; the sort is intrinsic to that grouping).
Compatibility
-------------
- Not a breaking change: the default (true) reproduces the previous behaviour.
- No database migration. The setting is a standard user configuration value
with its default declared in config-user.default.php, so existing users keep
the previous behaviour transparently.
- The read/unread state filter still persists across navigation regardless of
this option.
i18n
----
English strings authored; en-US marked as IGNORE. Translations for the other
25 languages were produced with an automated (AI) translation service and
should be reviewed by native speakers.
* Update app/i18n/en/conf.php
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
* Comments preference
* Update app/i18n/en/conf.php
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
* i18n updates and comment
* Update translations to match reworded sticky_sort strings
Re-applies the 24 non-English/French translations of the "sticky_sort"
reading option after the English and French wording was updated to
"Keep manual sort order during navigation".
* improve Dutch
* i18n DIRTY
* i18n en: custom sort order
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
- Add a new `EntriesRead` / `entries_read` hook for extensions.
- Trigger the hook when `markRead()` changes entry read/unread state for explicit entry IDs.
- Cover both the default DAO path and the SQLite/PGSQL DAO path.
- Add Minz hook tests for the new hook signature and argument passing.
## Why
Extensions can already react to favorite/bookmark changes through `EntriesFavorite`. This adds the matching read/unread hook requested in #4051 for extensions that need to collect stats or react to manual read state changes.
Closes#4051
Two "uses cache" LOG_DEBUG syslog() calls were emitted on every feed
refresh regardless of the simplepie_syslog_enabled setting, so users who
disabled it still saw "FreshRSS SimplePie uses cache for ..." spam in
syslog (issue #8540). Guard both with simplepie_syslog_enabled, like the
neighbouring SimplePie GET logs already are. Real error and fatal logs
are left untouched.
For https://github.com/FreshRSS/FreshRSS/issues/8540
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Generate new articles notification string in PHP
Closes#6470
Changes proposed in this pull request:
- Generate notifications string in PHP
- Use plurals support
* New articles notification: switch to plural forms
* README status
* fix paramInt invocation
* i18n
* make fix-all
* Apply suggestion from @Frenzie
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Migrate markdownlint-cli to markdownlint-cli2
Replace markdownlint-cli with markdownlint-cli2 and consolidate the three
separate configuration points (.markdownlint.json rules, .markdownlintignore
excludes, and the '**/*.md' glob in the npm scripts) into a single
.markdownlint-cli2.jsonc holding config + globs + ignores.
The npm script names (markdownlint / markdownlint_fix) are unchanged, so the
CI step and the test/fix compound scripts need no change. markdownlint-cli2
v0.23.0 bundles the same markdownlint v0.41.0, so linting behaviour is
identical (112 files, 0 errors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Add JSONC to .editorconfig
* Reduce obvious comments
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Feeds moved between instances via OPML lost their refresh interval,
forcing manual reconfiguration. Add a 'frss:ttl' attribute (signed, so a
negative value also carries the muted state) to the OPML export, and read
it back on import. Documented in docs/en/developers/OPML.md.
For https://github.com/FreshRSS/FreshRSS/issues/5914
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
A failed Fever API auth returns {"auth":0} with HTTP 200, so from the web
server log a failure is indistinguishable from a success and fail2ban
cannot act. The failure was already logged to API_LOG, but without the
client IP. Append the remote IP (Minz_Request::connectionRemoteAddress())
so a fail2ban filter watching data/users/_/log_api.txt can ban repeated
attempts.
For https://github.com/FreshRSS/FreshRSS/issues/6814
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Previously the Docker healthcheck script exited with a bare die(1) /
die(2) and no message, so users only saw 'unhealthy' with no clue why.
Print an informative reason to STDERR before each failing exit (a
connection/HTTP error, or a response that is not a FreshRSS page),
without changing the exit codes relied on by Docker HEALTHCHECK.
For https://github.com/FreshRSS/FreshRSS/issues/8026
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: report new articles count per feed in actualize_script output
Closes#8291
## Changes proposed in this pull request
`app/actualize_script.php` now reports, for each feed that received new articles, how many new articles were fetched — in addition to the existing per-user summary. This makes batch refreshes (cron / systemd timers) much more informative.
Example output:
```
FreshRSS actualize admin…
10 new article(s) from feed: FreshRSS releases
3 new article(s) from feed: Some other blog
```
The feature is implemented **entirely within the CLI script**, reusing the existing extension-hook mechanism that the script already uses for the actualization mutex — no change to `feedController` or to any other behaviour:
- `FeedBeforeActualize` records each feed's name.
- `EntryBeforeAdd` counts new entries per feed. This hook fires only in the "genuinely new GUID" branch of the actualization loop (updated articles go through `EntryBeforeUpdate`), so the count matches the controller's own `$nbNewArticles` counter. Because the counting hook is registered after `$app->init()`, it runs last in the chain and is skipped for entries dropped by another extension — i.e. it counts only articles that are actually added.
Counts are printed through the script's existing `notice()` helper, so they reach STDOUT regardless of the `environment` setting (unlike `Minz_Log::notice`, which is suppressed in `production`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Minor
* Remove tab
The tab character is escaped in some logs, e.g.
```
[notice] --- \x091 new article(s) from feed: L’Express
```
* Sort per-feed output and also report updated articles
Addresses review feedback on #8948:
- Sort the per-feed report by descending article count, then feed name.
- Also count updated articles (via EntryBeforeUpdate) and report them
alongside new ones, e.g. '10 new, 2 updated article(s) from feed: X'.
EntryBeforeAdd fires only for new articles (new-GUID branch), so
updated articles need the separate EntryBeforeUpdate hook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Reduced code
* Do not report feeds with no new or updated articles
FeedBeforeActualize initializes every actualized feed in $feedStatistics
and fires before the TTL/mute skips, so skipped or unchanged feeds would
otherwise print an empty count (' article(s) from feed: X'). Skip them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Minor code logic preference
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
"Follow-up" of https://github.com/FreshRSS/FreshRSS/pull/8612 (not fixing a regression, just a bug)
Some features like URL observers and custom favicon JS (e.g. *Reset to default* button) wouldn't work, due to `extra.js` not being loaded properly on pages where the slider content was already included in the HTML.
* Worked on Turkish translation
* Add my name to CREDITS.md
* Run make fix-all
* Fix credits
---------
Co-authored-by: Utku Tibet <utkutibet@Utkus-MacBook-Pro.local>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Fixes regression caused by https://github.com/FreshRSS/FreshRSS/pull/8335
To reproduce it, open any dropdown in the aside, reload the page, and try to open the same dropdown - it will disappear
To explain the issue more clearly, the dropdown menu wouldn't show because earlier its opacity is set to 0%, and after clicking the same dropdown after a page reload, the hashchange event wouldn't trigger to restore the previous opacity, and also the second listener would trigger to remove the dropdown if you opened it again.
Maybe https://github.com/FreshRSS/FreshRSS/issues/6156 is relevant here
Also a CSS change to stop showing the dropdown toggle icon of the previously opened dropdown after a page reload while the dropdown was open.
* Add SSRF mitigations using `filter_var` and `CURLOPT_RESOLVE`
The idea is to prevent FreshRSS from sending any HTTP requests to internal services, except for the ones that are explicitly allowed in the config.
Based on 6e82b46a48/lib/filelib.php (L3818) and https://github.com/symfony/symfony/blob/8.1/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.phphttps://github.com/FreshRSS/simplepie/pull/76https://github.com/FreshRSS/simplepie/pull/78
* Add allowlist setting in Web UI
* make readme
* Update app/i18n/fr/admin.php
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* make readme again
* make readme
* Further work
Still WIP and needs testing etc.
* Readd previous if check for domain combination allowlist
* Turn POST to GET after redirect
* Improve
* Update config.default.php
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* make readme
* Skip SSRF check if `CURLOPT_PROXY` is set
* make readme
* Fix `!empty()` mistake
* Respect max redirects feed option when fetching with `httpGet()`
* Respect max redirects during SimplePie fetching + fix bypass
bypass fix: `CURLOPT_FOLLOWLOCATION` was moved below so that emulated redirects are enforced.
* Avoid FreshRSS and Minz code in SimplePie
https://github.com/FreshRSS/FreshRSS/pull/8400#discussion_r2935375980
* Corrected hook code
* phpdoc wrong return type
* Add CIDR support in allowlist
* Implement simple DNS caching
* Suppress `dns_get_record()` warnings
* A bit of proof-reading
* Minor typo
* Fix proxy logic
* Fix HTTP POST redirect logic
* Proofread checkCIDR
Add fixes for several situations
* Remove credentials from URL in logs
* Ensure `CURLOPT_FOLLOWLOCATION` is `false` by setting it at the end
* Fix codesniffer long line
* Fix potential bypass due to wrong return value
If there were no records returned by `dns_get_record()`, no overrides to `CURLOPT_RESOLVE` would get passed,
and a potential bypass could occur, when cURL would try to resolve the domain by itself.
* Put the URL at the end in logs
* Add documentation and environment variable support
* make readme
* Fix wrong behavior in case of IP
* Fix duplicate selector in CSS
* Minor type check change
* i18n fr, en
* Minor type check change
* Fix whitespace i18n fr
* make fix-all
* Fix `$ips_ok` not being returned after domain records were cached
* make readme
* PHPStan fix
* make readme
* Minor syntax in SimplePie
* Only return `null` if no allowed IPs were found
* Add wildcard *, help message
* Consistent docs with help message
* i18n: pl
* SimplePie compatibility PHP 7.2
* make fix-all
* Sync SimplePie
* https://github.com/FreshRSS/simplepie/pull/76
* 💥 Breaking change in the Changelog
* Document `INTERNAL_HOST_ALLOWLIST` in Docker docs
* Remove `Cookie` and `Authorization` headers in `httpGet()` during cross-origin redirect
* Minor whitespace
And same comment convention than below
* Remove authentication headers and change POST to GET on redirect in SimplePie
* Remove .local in Docker example
* Fill in default ports when comparing URL origins
* Remove .local from other places than the Docker example
* Rewrite WebSub subscribe to use `httpGet()`
* make fix-all
* Also unset `CURLOPT_USERPWD` during redirects
* phpcs fix
* Always unset `CURLOPT_FOLLOWLOCATION`
* Bump SimplePie
https://github.com/FreshRSS/simplepie/pull/78
* Update logic for CURLOPT_FOLLOWLOCATION
* Fix PHPStan
* Changelog fix security section
* Update most common RSS Bridge case
https://hub.docker.com/r/rssbridge/rss-bridge
* Replace misleading 127.0.0.1:8080 example for Docker
This does not make sense for a Docker container
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* fix: move disable button before remove button in user management
The order of action buttons when modifying a user was inconsistent.
The "disable" button appeared after the "remove" button, which is
counterintuitive since disabling a user is a less destructive action
than removing one.
This commit reorders the buttons in `details.phtml` to reflect the
severity of each action, from least to most destructive:
update → purge → promote/demote → disable/enable → delete
Closes#8877
* Remove blank lines
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
I already changed some places that were using normal string comparison to hash_equals before, but didn't check other places.
Now the checks should be consistent.