Commit Graph
1031 Commits
Author SHA1 Message Date
Alexandre Alapetite 7e8ef628d9 Better doc and log proxy cases (#9436)
Fix https://github.com/FreshRSS/FreshRSS/issues/9423
And strip redundant proxy scheme.
And add a few more unit tests.
2026-10-13 19:50:38 +02:00
abp002andAlejandro Borrego 48547b4a6c Reduce memory consumption of export (#9350)
Contributes to #6139 (export part; the import will follow in another PR)

Exporting favourites was collecting the whole file in memory. With many articles, it fails with the default PHP memory limit.

## Changes
- New `Minz_View::helperToFile()`, similar to `helperToString()`, writing the output of a helper into a file by chunks of 64 KiB instead of keeping it in memory.
- `FreshRSS_Export_Service`: the `generate*()` methods now return the path of a temporary file instead of the content, and `zip()` builds the archive from those files (`ZipArchive::addFile()` instead of `addFromString()`). Temporary files are created in `TMP_PATH` (already required to be writable) and deleted at the end of the script.
- The export is sent with `readfile()`: Web UI, `cli/export-zip-for-user.php`, `cli/export-opml-for-user.php` and the OPML export of the Google Reader API.
- Note: this changes what `FreshRSS_Export_Service` returns (a path instead of the content). All callers in the repository are updated, and I could not find any public extension using this service.

## Measurements
Peak PHP memory (`memory_get_peak_usage(true)`), SQLite database with synthetic starred articles:

| Scenario | Before | After |
|---|---|---|
| Web, export your favourites, 10,000 articles (69 MB JSON) | 136 MB (fails with the default `memory_limit` of 128 MB) | 6 MB |
| Web, ZIP (OPML + favourites + labelled articles + 4 feeds), 10,000 articles | 152 MB | 6 MB |
| CLI `export-zip-for-user.php`, 10,000 articles | 189 MB (fails with 128 MB) | 8 MB |
| Web, export your favourites, 30,000 articles (208 MB JSON) | 404 MB | 10 MB |

Co-authored-by: Alejandro Borrego <210245044+abp002@users.noreply.github.com>
2026-10-05 21:47:19 +02:00
Nikolai ShcheglovandAlexandre Alapetite a93bd27b7d Fix muting of feeds gone with HTTP 410 (#9391)
* Fix muting of feeds gone with HTTP 410

A feed answering HTTP 410 Gone is meant to be muted (#3561), but:

* Since #6037, FreshRSS_Feed_Exception extends Minz_Exception, which
  resets any code other than its own error levels to 0, so the check for
  410 never matched. FreshRSS_Feed_Exception now keeps the HTTP status
  code it is given.
* FreshRSS_FeedDAO::mute() mutes by negating the TTL, which leaves a feed
  with the default TTL (0) unmuted. Mute the feed the way the feed
  settings and dynamic OPML categories do instead.

* A bit of cleaning

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-10-03 09:59:51 +02:00
Alexandre Alapetite a8e5e6300b Better enforce feed limits (#9357)
E.g. when added from API
Fix https://github.com/FreshRSS/FreshRSS/issues/8822#issuecomment-5781019651
2026-09-28 10:41:29 +02:00
5a270c02b0 Rotate session ID on all authenticated transitions (CWE-384) (#9333)
* auth: rotate session ID on all authenticated transitions (session fixation)

The PHP session ID was regenerated on explicit form login, reauth, logout and
password change, but NOT when access is (re)established from a remember-me
cookie, via HTTP authentication, or on self-registration auto-login (CWE-384),
so a fixed session ID could become authenticated.

Regenerate the session ID at those transitions too: in FreshRSS_Auth::init
when accessControl() first grants access (covers remember-me restore and HTTP
auth; skipped for auth_type 'none', which has no authentication boundary), and
in the self-registration auto-login path, mirroring the existing form-login
idiom. Also enable session.use_strict_mode before session_start as defence in
depth so an uninitialised, attacker-supplied session ID is not adopted.

* auth: avoid duplicate cookie during session rotation

* Code preferences, shorten some comments

---------

Co-authored-by: Inverle <inverle@proton.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-09-28 10:39:21 +02:00
GigiandAlexandre Alapetite cd42dc7509 Bound ZIP import against decompression bombs (CWE-409) (#9332)
* importExport: bound ZIP import against decompression bombs

ZIP import read every archive member into memory via getFromIndex() with no
limit on member count, per-member or total uncompressed size, or compression
ratio, so a small crafted upload could exhaust memory (CWE-409).

Enforce a member-count limit, per-member and aggregate uncompressed-size
limits and a compression-ratio ceiling from the central-directory metadata,
and additionally cap the real decompressed size while streaming each member
(the declared size is attacker-controlled and can under-report). Oversized or
over-compressed members are skipped and the loop stops once the budget is
reached; a realistic export (an OPML plus a few JSON files) stays well within
the limits.

* Fix lookup by name and total calculation

* Fix locateName regression
Duplicate entry name: keep only the first occurrence, but allow full paths

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-09-27 21:25:08 +02:00
Gigi d5ad610c3d Make the login challenge nonce one-time (CWE-294) (#9334)
* auth: make the login challenge nonce one-time

The per-login challenge nonce stored in the session was never consumed or
cleared, so a captured challenge stayed replayable and survived logout
(CWE-294). The client (p/scripts/extra.js) already fetches a fresh nonce for
every attempt, so consuming it does not affect legitimate retries.

Consume the nonce (unset it) as soon as it is read for verification in form
login, reauthentication, password change and account self-deletion, and clear
it in FreshRSS_Auth::removeAccess so logout invalidates any outstanding
challenge.

* auth: don't clear the nonce in removeAccess (breaks login)

removeAccess() runs on every unauthenticated request via FreshRSS_Auth::init()
-- including the login POST itself, before formLoginAction reads the nonce -- so
clearing it there rejected every login with 'Invalid session'. Thanks
@jamalkamaladdin for the measured catch. The one-time guarantee is preserved by
consuming the nonce when it is read for verification (login/reauth/password
change/self-deletion); logout no longer needs a separate clear.
2026-09-27 12:21:17 +02:00
GigiandAlexandre Alapetite a6253486c0 Require POST+CSRF for self-update mutations; guard post-config step (CWE-352) (#9335)
* update: require POST + CSRF for updater mutations; guard post-config step

The self-update check/apply mutations were reachable via GET, which is never
anti-CSRF validated (the token is only checked on POST), and the privileged
post-configuration step (do_post_update()/PostUpdate hook, removal of the
staged update file) was additionally reauth-exempt and GET-reachable (CWE-352).

- checkAction and the apply branch now require POST, so the global anti-CSRF
  token check applies; the update page's Check/Apply controls are POST forms
  carrying the token.
- The post_conf finalisation is bound to a one-time session marker set by the
  successful apply right before its internal redirect, so it only runs as that
  continuation, not via a direct cross-site GET (independent of reauth config).
- Reauth is now required consistently, including for apply/post_conf (was
  exempted); the legitimate post_conf redirect still passes on the reauth done
  moments earlier by the apply request.

Status/index endpoints remain GET and read-only.

* Reduce some comments

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-09-26 17:02:22 +02:00
Ethan Stoner 5fb62464e9 Call check_url_before_add hook when previewing a feed (#9343)
The GET branch of feedController::addAction() built a FreshRSS_Feed
straight from the requested URL and loaded it, without invoking the
Minz_HookType::CheckUrlBeforeAdd hook. Only the POST branch, through
addFeed(), ran the hook.

Extensions that turn a website URL into a feed URL (e.g. RSS-Bridge)
were therefore bypassed on the confirmation page reached from the
bookmarklet: the raw website URL was probed, and since such sites do
not advertise a feed, the preview reported that no feed could be
added. Submitting the very same form then succeeded, because the POST
path did apply the hook.

Run the hook at the start of the GET branch so the preview probes the
URL that would actually be subscribed to, and reuse the existing
"not added" feedback when an extension refuses the URL, matching what
addFeed() does when the hook returns null.

The documented contract is that the hook "will be executed every time
a URL is added", so the preview should not be an exception.

Fixes #9339
2026-09-26 15:24:10 +02:00
Alexandre Alapetiteandkta1kri 64f7f24c6e Improve WebSub security (#9330)
* Improve WebSub security
New keys are now more random

* Refuse to downgrade from HTTPS to HTTP

Co-authored-by: kta1kri <77656895+kta1kri@users.noreply.github.com>
2026-09-20 17:25:43 +02:00
Alexandre Alapetite 8067fd994b Use PHP SensitiveParameter (#9322)
https://www.php.net/class.sensitiveparameter
Active from PHP 8.2+ but parses without effect on PHP 8.1
2026-09-19 19:52:07 +02:00
Alexandre Alapetite ad4723cb1f Warn during session regenerate fail (#9311)
Might provide more logs for e.g. https://github.com/FreshRSS/FreshRSS/discussions/9305
2026-09-19 14:33:25 +02:00
Morax cc45db5ef8 Fix adaptive reading state after marking articles read (#9290)
Closes #9288

Changes proposed in this pull request:

The mark-as-read links contain the resolved state, so the automatic reading preference becomes an explicit `state=2` filter after the redirect. Once the last unread article is marked read, the page stays empty instead of showing all articles.

- Keep the requested state separately from the resolved state used by the action. A redirect state of `0` reapplies the reading preference; explicit filters still survive the redirect as intended by #9007.
- Carry that value through the menu, footer and “Load more” link. This matters with oldest-first pagination, where the last footer supplies the menu button's action.
- Add request-state and rendered-pagination tests, plus a changelog entry.

How to test the feature manually:

1. With both read and unread articles, choose “Show unreads if any, all articles otherwise”. Mark all as read from the footer, then repeat using the top button. The resulting page should show the read articles.
2. Set one article per page and oldest-first sorting. Load the remaining articles before marking all as read; the same fallback should work.
3. Select an explicit unread filter: marking everything read should still leave an empty unread view. Also check a favourite filter with a search term, including “Mark selection unread”; the search and favourite scope should remain intact.

Validation: `make NO_DOCKER=1 fix-all` and `make NO_DOCKER=1 test-all` pass with PHP 8.5.9 (719 PHPUnit tests, 1,378 assertions). Browser checks passed against SQLite in Chromium (desktop and mobile viewport) and Firefox. The original footer fails three of the six new rendered-pagination cases.
2026-09-11 08:18:09 +02:00
Ali GündoğarandAlexandre Alapetite 970b135190 Ignore security-relevant OPML attributes for dynamic OPML sources (#9276)
* Ignore security-relevant OPML attributes for dynamic OPML sources

OPML content can come from an untrusted source (a dynamic OPML category re-fetches its remote content on every refresh cycle). Such content must not be able to configure feed cURL parameters (CURLOPT_COOKIE, CURLOPT_PROXY, CURLOPT_POSTFIELDS, etc.) nor create further dynamic OPML categories. importOpml() therefore defaults to untrusted, and the local file import controller explicitly opts in.

* Review

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-09-09 16:11:38 +02:00
Inverle c4b96b253c Fix auth CSRFs (login and register actions) (#9171)
This is done by getting rid of the CSRF check allowlist, and instead of refreshing the CSRF token on every unauthenticated page load, only refreshing the token when needed (on login, logout, account delete, HTTP auth user change)

In addition, the actualize actions are now correctly protected as well.

The most notable part is that the user create action could potentially be exploited by another website for XSS on a given instance, by submitting the register form with known credentials, and then by the attacker logging into the same account forcibly registered by a malicious website, and setting custom User JS code.
Login action was likely not exploitable in any meaningful way, because of the JS nonce.

It's worth noting one risk of merging this PR, some vulnerabilities discovered in the past had reduced impact due to CSRF token refreshing on every page load: https://github.com/FreshRSS/FreshRSS/security/advisories/GHSA-jf4v-f8p2-8xvq#:~:text=It%27s%20important,FreshRSS%5FAuth%3A%3AremoveAccess%28%29%2E
2026-09-08 09:19:01 +02:00
Inverle 5a842b25ee Format unread counter in title on page load (#9263)
For consistency with JS: https://github.com/FreshRSS/FreshRSS/pull/8577#issuecomment-4017150896
2026-09-06 21:36:12 +02:00
András Marczinkó 99ee090236 Fix PHP Warning: Cannot modify header information headers already sent (#9267)
Closes #9229

I restored my accidently deleted patches with @Inverle fixes. This fix is still works perfectly. Sorry. (https://github.com/FreshRSS/FreshRSS/pull/9230)

How to test the feature manually:

With this change:
1. Go to FreshRSS UserCSS extension config
2. Enable the UserCSS and set the CSS code in it.
3. Go to FreshRSS feeds page.
4. Hit the ctrl + F5 button and see no errors in the nginx log.
2026-09-06 21:32:48 +02:00
Alexandre Alapetite f902047204 Offer to add a prefix when resharing user labels (#9236)
* Offer to add a prefix when resharing user labels
Improve https://github.com/FreshRSS/FreshRSS/pull/8112

* make fix-all
2026-08-28 08:51:57 +02:00
Alexandre Alapetite 2701b7dff4 Cleaner limit anonymous mode to default user (#9235)
While waiting for https://github.com/FreshRSS/FreshRSS/issues/1553
2026-08-25 16:17:36 +02:00
Alexandre AlapetiteandFrans de Jonge 2ceb78fefb Dev updates: composer, hadolint, typos... (#9219)
* Dev updates: composer, hadolint, typos...
Composer compatibility PHP 8.6.
Manual updates not supported by Dependabot

* Fix hadolint

* Update lib/Minz/Url.php

Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>

* Add sha256sum for composer

---------

Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
2026-08-18 09:36:01 +02:00
Alexandre Alapetite ae672979c2 Fix saving user queries (#9190)
Fix https://github.com/FreshRSS/FreshRSS/issues/9188
2026-08-09 22:05:41 +02:00
InverleandAlexandre Alapetite e1ce0b0519 Sanitize all fields during JSON import (#9168)
* Sanitize all fields during JSON import

fix [GHSA-hx52-rwx7-wg87](https://github.com/FreshRSS/FreshRSS/security/advisories/GHSA-hx52-rwx7-wg87)

* Remove URL crendentials from logs

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-08-05 22:00:10 +02:00
Jam Balaya e39b5898f1 Ignore empty categories when sizing export feed selector (#9154)
Follow-up to #9079.

## Summary

- Ignore empty categories when sizing the export feed selector.
- Keep the selector height aligned with the category headings and feeds that are actually rendered.
- Preserve the grouped export choices and export request format introduced in #9079.

## Root cause

The view skipped categories without feeds when rendering `<optgroup>` elements, but the `size` calculation counted every category returned by the DAO. Empty categories therefore increased the listbox height without adding any visible content.

## Before / After

| Before | After |
| --- | --- |
| <img width="1400" height="1000" alt="9079-followup-before-1400x1000" src="https://github.com/user-attachments/assets/f225d97f-8486-4e7d-87eb-cdafd42b53a5" /> | <img width="1400" height="1000" alt="9079-followup-after-1400x1000" src="https://github.com/user-attachments/assets/aeb254e0-6410-4f6f-a0d8-3051c5dd2c83" /> |
| `size="10"` for two category headings and two feeds, leaving unnecessary empty space. | `size="4"` for the same data, matching the four rendered rows. |
2026-08-01 15:00:41 +02:00
dd2ef8ffa6 Group export feeds by category (#9079)
* Group export feeds by category

## What changed
- Groups feed choices in the import/export page by their existing FreshRSS category.
- Preserves the current export request format and selection behavior.

## Why
The export selector previously mixed feeds from every category, making a category-specific export difficult to assemble.

Closes #5555

* Declare export feed count view data

* Fix select size

---------

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-29 16:58:54 +02:00
36d973a3bd Filter global view feed list by state and search (#9132)
* Filter global feed list by favorites

Fixes #4564.

The global view now filters its category/feed list when the Favorites filter is active, using the same article state and search criteria used when a feed is opened. Empty categories are omitted rather than showing feeds that would open to no matching articles.

* Fix favorite feed state query typing

* Rework logic, including searches

After reflection, I have revised the logic so that in the global view, the categories and feeds shown are always filtered by the search query (if any) and the states (if consequential):

<img width="2012" height="846" alt="image" src="https://github.com/user-attachments/assets/1fd693fa-9184-4601-85ed-2579e3b7299c" />


This makes the global view more useful and distinct in my opinion (this was otherwise not a view I was using myself, but now it will be).

Note that this behaviour would be challenging to port to the normal view for performance reasons.

* Show the matching counts in the global view UI instead of number of unreads

* Spelling

* Fix case of STATE_ALL

* Minor: more explicit logic

---------

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-29 16:36:06 +02:00
Jeremy Nguyen a8a4264af2 Add remote IP address to warning on form login (#8942)
Closes #8941
Add remote IP address to warning on form login
2026-07-24 17:47:00 +02:00
2ff32b0254 Move article header/footer options from Reading to Display configuration (#9139)
* Move article header/footer options from Reading to Display configuration

Feed title, authors and date, article icons position, and tags options
control the article header/footer layout, so they belong next to the
existing top/bottom line icon settings on the Display configuration
page rather than on Reading.

Fixes #6866.

Co-Authored-By: Claude <noreply@anthropic.com>

* Move section further down

---------

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-23 13:36:34 +02:00
6996447d54 Allow selecting a category in feed add URLs (#9047)
* Support categories in feed add URLs

## Summary

- Honour the `category` query parameter when opening the add-feed form.
- Keep the existing `cat_id` parameter as a backwards-compatible fallback.
- Use `category` for the add-feed links in the subscription view.

This makes the URL proposed in #4160 select the intended category:
`?c=feed&a=add&category=1&url_rss=https://example.com/feed.xml`.

## Validation

- PHP syntax checks for the changed controller and templates.
- Focused request-parameter checks for explicit `category`, legacy `cat_id`, and the default category.

Fixes #4160


* Simplify and small fixes

---------

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-21 17:47:56 +02:00
46f5e13019 Refresh only feeds in the current view (#9060)
* Refresh only feeds in the current view

Closes #8025. Passes the current feed or category selection to the refresh endpoint and limits the batch accordingly. 

* Fix case of Dynamic OPML, and keep feed order

---------

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-21 17:46:55 +02:00
1b8eb17858 Add per-feed option to show or hide enclosures (#9015)
* Add per-feed option to show or hide enclosures

Some feeds attach lots of enclosures (images, audio, video, downloads)
that clutter the article view for readers who only want the text. Add
a per-feed "Show enclosures" setting (Feed configuration > Display),
defaulting to "by default" (i.e. shown, matching current behaviour),
following the same ternary-attribute pattern used by existing
per-feed overrides such as show_unread_count and read_upon_gone.

Also apply the preference to the GReader-compat API content call and
the Fever API entry serialization, so third-party clients like Reeder
and Fever apps respect it too, not just the normal web UI. Renamed the
translation key to display_enclosures to match the underlying feed
attribute name.

Fixes #4999

* i18n: fr

* Fix Fever API

* Remove accessor

* Forgotten fix

---------

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-20 18:45:21 +02:00
7321c1e9fb Add log search to the logs page (#9059)
* Add log search to the logs page

Closes #8471. Adds a search field and filters log level, timestamp, and message before pagination

* Fix log search formatting

* Type the log search view property

* Fix missing i18n
Another option could be `index.menu.search_short`

* Fix form submit

---------

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-18 23:26:32 +02:00
Gerard Alvear PorrasandGerard Alvear f4fc014973 Improve extension list fetch diagnostics (#9055)
Closes #4914. Includes HTTP status and cURL error details when the extension list request fails

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
2026-07-18 22:36:46 +02:00
Inverle 0f0b696cdf Display slider after submit for all buttons in feed/update.phtml (#8999)
Slight improvement of https://github.com/FreshRSS/FreshRSS/pull/8612
2026-07-11 17:04:12 +02:00
ad78838ffc Keep search and state filters when marking articles as read (#9007)
* 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>
2026-07-09 21:13:14 +02:00
Jam Balaya ce7a196034 Remove unused simple layout (#8998)
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`.
2026-07-07 15:04:11 +02:00
2188070aa6 Add option to keep or reset article sort order when navigating (#8969)
* 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>
2026-07-07 11:04:31 +02:00
Jam BalayaandAlexandre Alapetite 271257be42 Fix statistics repartition averages (#8996)
* Fix statistics repartition averages

* Minor code preference

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-07 10:42:53 +02:00
Inverle a3badc2d2e Fix bad header filtering bypass (#8964)
* Fix bad header filtering bypass

* Sanitize every save using `_attribute('curl_params')`

* Sanitize every read using `attributeArray('curl_params')`
2026-07-02 15:08:32 +02:00
dependabot[bot]andAlexandre Alapetite 93e83943a3 chore(deps-dev): bump phpstan/phpstan from 2.2.2 to 2.2.3 (#8962)
* chore(deps-dev): bump phpstan/phpstan from 2.2.2 to 2.2.3

Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.2 to 2.2.3.
- [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits)

---
updated-dependencies:
- dependency-name: phpstan/phpstan
  dependency-version: 2.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Ignore a could of redundant tests, for keeping additional safety

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-01 21:35:39 +02:00
InverleandAlexandre Alapetite dcec27c69d Add SSRF mitigations using filter_var and CURLOPT_RESOLVE (#8400)
* 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 https://github.com/moodle/moodle/blob/6e82b46a480826d1a85394d9e5087f7d82d1dd52/lib/filelib.php#L3818 and https://github.com/symfony/symfony/blob/8.1/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php

https://github.com/FreshRSS/simplepie/pull/76
https://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>
2026-06-28 18:51:04 +02:00
Inverle 931d62ab08 Use timing safe comparison for tokens everywhere (#8945)
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.
2026-06-27 14:02:20 +02:00
Inverle bf2de1baea Fix access control in rss and opml actions (#8912)
Due to missing `return` statement while using `redirect: false`, no `exit()` is done during `Minz_Error::error()`

Regression from https://github.com/FreshRSS/FreshRSS/pull/8434
2026-06-05 22:38:17 +02:00
Alexandre Alapetite 4b019ab697 New option to hide badges showing number of unread articles (#8844)
Inspired by @Dan-Q's  *Reducing Phantom Obligation in FreshRSS* https://danq.me/2026/02/06/phantom-obligation-vs-freshrss/

<img width="512" height="105" alt="image" src="https://github.com/user-attachments/assets/df93118b-36f3-4175-a681-b3f81fe63ff2" />

Results in:

<img width="306" height="181" alt="image" src="https://github.com/user-attachments/assets/1c8ccaa6-4fcf-48a4-bf98-0ace43a68861" />

Can be overridden at category and feed levels.
2026-05-20 20:25:20 +02:00
Alexandre Alapetite 8e007af39d Fiix cookies with some browsers (#8867)
Fix https://github.com/FreshRSS/FreshRSS/issues/8850
Fix login with e.g. SeaMonkey
Regression due to https://github.com/FreshRSS/FreshRSS/pull/8778
2026-05-19 23:03:51 +02:00
Alexandre Alapetite c3fa374f25 Fix empty entry Generator (#8863)
* Fix empty entry Generator
Fix https://github.com/FreshRSS/FreshRSS/issues/8857
Regression from https://github.com/FreshRSS/FreshRSS/pull/8789
This is due to https://bugs.php.net/bug.php?id=77515

* Use EmptyIterator instead

* Syntax update
2026-05-19 21:45:20 +02:00
Alexandre Alapetite 66568f0283 TXT import compatibility HTML comments (#8837)
Discard lines starting with `<` for e.g. HTML/XML comments, tags
Compatibility with TXT format of https://atlasflux.saynete.net
Follow-up of https://github.com/FreshRSS/FreshRSS/pull/8818
2026-05-15 12:10:55 +02:00
8863cdcaf8 feat(import): accept .txt URL lists alongside OPML/JSON/ZIP (#8818)
* feat(import): accept .txt URL lists alongside OPML/JSON/ZIP

Detects .txt by extension and wraps the URL list into a minimal OPML
document so the existing import pipeline handles dedup, categories and
feed limits unchanged. Blank lines, `#` comments and a UTF-8 BOM are
skipped; lines that don't parse as URLs are logged and dropped without
aborting the batch.

Works through both `cli/import-for-user.php` and the web import form.

* utf8BOM

* ENT_COMPAT

---------

Co-authored-by: Bjørn A. Andersen <polybjorn@users.noreply.github.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-05-12 10:52:00 +02:00
Alexandre Alapetite c2619d9a26 Fix search in shared user queries (#8789)
The internal search was shown in the UI as the user search.
This was due to the lazy nature of the Generator. Improve the try/catch behaviour at the same time.

How to test:
* Make a user query with a search parameter
* Share the user query as HTML
* Observe the search field (should be empty with this PR,  while it contained the internal search before)
2026-05-08 09:05:55 +02:00
0c93b26cb5 feat(import): support category field in JSON feed import (#8786)
* Extension to JSON imports: interpret
      "origin": {
        "category": "..."
      },
as category for FreshRSS. When importing a new feed configure the feed
into this category. Creates missiong categories on the fly.

* Fix syntax

* fix(import): tighten JSON category import follow-ups

- Use strict comparison `!== false` after `addCategory()` so a (theoretical)
  zero return from auto-increment is not silently treated as failure
  (matches review feedback on #5638).
- Modernise property declaration to typed property syntax, matching the
  surrounding `$entryDAO`/`$feedDAO` style.
- Pass `$username` to `createCategoryDao()` in `importFile()` so CLI
  imports for a non-current user create categories on the right account,
  matching the adjacent entry/feed DAO instantiations.
- Trim the requested category name and skip if it is empty after trim,
  so a whitespace-only `"category"` value falls back to the default
  category instead of creating a junk row with an empty name. Also
  normalises trailing/leading whitespace so `"  Tech News  "` reuses an
  existing `"Tech News"` category.

* Update app/Controllers/importExportController.php

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>

---------

Co-authored-by: Robert Dahlem <robert.dahlem@gmx.net>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Co-authored-by: Bjørn A. Andersen <polybjorn@users.noreply.github.com>
2026-05-04 15:02:03 +02:00
226239d157 Bump phpstan/phpstan from 2.1.46 to 2.1.54 (#8753)
* Bump phpstan/phpstan from 2.1.46 to 2.1.54

---
updated-dependencies:
- dependency-name: phpstan/phpstan
  dependency-version: 2.1.54
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fixes some PHPStan issues, including compatibility PHP 8.2-
Follow-up of https://github.com/FreshRSS/FreshRSS/pull/8713

Co-authored-by: Copilot <copilot@github.com>

* Bump PHPStan-strict-rules

* Fix PHPStan for PHP 8.3

* Ignore PHPStan warning for PHP 8.2 and PHP 8.3

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Co-authored-by: Copilot <copilot@github.com>
2026-05-02 21:16:27 +02:00