* 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.
Closes#8882
- avoid recommending `/usr/share/` for writable FreshRSS data in the general install docs
- update the Debian/Ubuntu git install path and symlink example to `/var/www/FreshRSS`
- add an FAQ entry for PHP-FPM hardening that can make `/usr/` read-only despite correct Unix permissions
This reverts commit 5c5b0b9e66 (PR #8933).
The `transform: translateZ(0)` added to #nav_entries promotes the element to
its own compositor layer, but a non-`none` transform also makes the element a
containing block for `position: fixed` descendants and establishes a new
stacking context. That side effect introduced a regression, so this restores
the previous behaviour while a less invasive fix for #8829 is worked out.
Reverting the whole PR (CSS + CREDITS.md) for a clean rollback.
* fix(themes): keep article nav bar pinned to bottom in iOS standalone PWAs
On iOS standalone PWAs, WebKit fails to repaint `position: fixed` elements
after a programmatic scroll, so the article navigation bar (#nav_entries)
drifts to the middle of the screen after tapping the prev/next buttons
(which scroll the article container in JS). This does not happen in regular
Safari, where the browser chrome provides repaint hooks the standalone shell
lacks.
Promote #nav_entries to its own compositor layer (transform: translateZ(0))
so WebKit keeps it pinned to the viewport across programmatic scrolls.
This supersedes #8833, whose `top: auto` was a no-op: nothing in the
stylesheet ever sets a `top` value on #nav_entries, so it could not affect
positioning.
Fixes#8829
* Add Antonio Santos to CREDITS.md
* fix(themes): drop redundant will-change on #nav_entries
The compositor layer that keeps #nav_entries pinned in iOS standalone PWAs
comes entirely from transform: translateZ(0). The accompanying
will-change: transform was redundant — it only hints at upcoming changes and
adds nothing to an element already promoted to its own layer.
Removing it leaves the bar pinned across programmatic scrolls (verified on an
iOS standalone PWA), with no behavioural change.
Based on maintainer feedback on #8933.