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.
* Fix infinite redirect loop due to `SCRIPT_NAME` in `PATH_INFO`
* Move to Minz_Request::pathInfo()
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Limit the length and parentheses nesting depth of a search query.
* Use exception
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* 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>
* Log and block invalid challenge formats during login
Fixes some unexpected behaviors during a user login, including strange errors in the log related to JSON.
* Ignore JSON decode errors for URL unserializing
* Document the format a bit
* Only allow http/https URI schemes for enclosure and thumbnail URLs
Feed enclosure URLs were stored and rendered without URI scheme validation:
SimplePie's scheme check only applies to URLs inside content HTML
(Sanitize::replace_urls()), not to enclosure metadata, so a `javascript:`
URL survived the whole pipeline and was served as a clickable link in the
article view, including through the Google Reader compatible API.
Add FreshRSS_http_Util::isAllowedUrlScheme() and apply it:
- at store time, when building enclosure attributes in FreshRSS_Feed
- at render time, in FreshRSS_Entry::content(), so entries already stored
in existing databases are protected as well
https://github.com/FreshRSS/FreshRSS/security/advisories/GHSA-3fw6-j8m4-82vj
* Reuse existing SimplePie is_remote_uri() function
* Use more realistic test examples
* Add tests for protocol relative
And simplify tests further
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* SimplePie: Harden cache deserialization calls
To reduce impact if a maliciously provided cache file is somehow loaded, e.g. from a manipulated backup or some kind of external arbitrary file write vulnerability.
* Sync our SimplePie
https://github.com/FreshRSS/simplepie/pull/90
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Reword explanations for edge/latest channels
Highlight the fact that we do not backport bug fixes or security fixes to previous releases.
Simplify also a slightly obsolte MySQL doc to favour SQLite
* Merge i18n: az
* Update Polish translations to specify rolling/versioned releases
Co-authored-by: Inverle <inverle@proton.me>
* Update 05_Release_new_version.md
---------
Co-authored-by: Inverle <inverle@proton.me>
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
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.
* Fix marking filtered label articles as read in SQLite
Remove the undefined entry alias from SQLite markReadTag filters. Add SQLite regression coverage for search and state filters, label boundaries, the maximum entry ID, and unread cache updates.
Fixes#9214
* Test label read filters across supported databases
Run the entry DAO regression cases against configurable SQLite, PostgreSQL, MySQL, or MariaDB connections using temporary tables. Match compressed content storage on MySQL/MariaDB, cover marking articles unread, and document how to run each backend.
* Keep only fix
Will rework test approach in future work
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Gate greader/json share formats behind Share by OPML
Follow-up to #9246, per @Alkarex's suggestion on GHSA-rpmm-h4hx-5p6p.
The greader and json share formats expose origin.feedUrl (the feed's subscription
URL). Sharing feed URLs is what the "Share by OPML" option explicitly means, whereas
"Share by RSS" is meant to share article content. This moves greader and json in
p/api/query.php from the shareRss gate to the shareOpml gate, so feed URLs are only
served when the owner opted into OPML sharing.
Note: moved json alongside greader too, since both emit feedUrl (same toGReader path).
* sharedUrlGreader: gate on shareOpml
* Move greader share link under Share by OPML group
p/api/query.php serialized entries with mode 'freshrss', putting the full stored
feed URL (including any user:pass@ credentials) into origin.feedUrl for f=json and
f=greader, exposing them to anonymous recipients of a "Share by RSS" link.
Call Feed::url(includeCredentials: false) on this path to strip the credentials,
matching the logging and OPML export paths.
Refs GHSA-rpmm-h4hx-5p6p
- Reviewed all remaining Spanish translations marked as TODO or DIRTY
- Updated translations against the current English source
- Removed obsolete TODO, DIRTY, and IGNORE markers after verification
- Brought Spanish translation completion to 100%
- Reviewed all remaining pt-BR translations marked as DIRTY
- Updated or validated translations against the current English source
- Removed obsolete DIRTY markers after verification
* Fix: infinite recursion in sanitizeHTML() when maxLength truncation doesn't converge
Sanitizing can grow a truncated HTML fragment (e.g. an unclosed tag gets
auto-closed), so the previous recursive shrink-and-retry could hit a fixed
point and recurse forever, crashing on a stack overflow. Bound the retries
and fall back to a hard truncation that always terminates.
Add unit tests for FreshRSS_SimplePieCustom::sanitizeHTML(), the XSS
sanitization applied to all untrusted feed content, which had no coverage.
* Update app/Models/SimplePieCustom.php
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
* Some refactoring
Avoid two mb_strcut in a row. More cases for fallback. Better fallback. More edge cases.
* Address trailing incomplete tag or entity
* Whitespace reduction
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
* Add configurable log_level system setting
Minz_Log currently only distinguishes 'production' (errors and
warnings only) from any other environment (everything, including
debug messages), with no granularity in between.
Introduce an optional 'log_level' system setting (error, warning,
notice, info, or debug) that overrides the verbosity implied by
'environment'. It defaults to an empty string, which preserves the
exact current behaviour.
Fixes#7594
* Slight rewrite
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
The loop building the media credits assigned instead of appending, so an enclosure with several credits only kept the last one.
`app/views/index/rss.phtml` builds the `<media:credit>` elements for an enclosure in a loop, but assigns instead of appends:
```php
$mediaCredits = '';
foreach ($credits as $credit) {
$mediaCredits = '<media:credit>' . $credit . '</media:credit>';
}
```
So an enclosure with three credits emits only the last one. Every iteration throws away what the previous one built.
This came in with #6272 ("Allow multiple authors on enclosures", which closed#5066). Parsing and display got multi-credit support, but FreshRSS's own generated RSS still passes on a single credit, so a downstream reader subscribing to a FreshRSS feed sees the same symptom #5066 described.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* i18n(id): complete Indonesian translation
Translate the 130 remaining `// TODO` entries in app/i18n/id/ and refresh
the 5 `// DIRTY` ones, bringing Indonesian from 88% to 100%.
Terminology follows the strings already shipped (umpan, kategori, artikel,
belum dibaca, tandai, pintasan). Placeholders, inline markup and
documentation links are preserved unchanged. Entries with no natural
Indonesian form are marked `// IGNORE` rather than left silently as
English: the `lt` language endonym, `gen.search.url`, and the API error
marker.
Replace `peladen` with `server` throughout, including in strings that
predate this change, as `peladen` is unfamle
doing so, fix a mistranslation where the "Browser" field of the system
information panel read `Peladen`, i.e. ser
All edits were made through cli/manipulateME
progress tables were regenerated with `composer run-script translations`.
Refs #6349.
* i18n(id): Fix Indonesian translations for update messages and sorting options