* Preserve explicit sort overrides when searching
Closes#9466
Changes proposed in this pull request:
- Keep an explicit sort override when submitting the header search form, including `c.name`, `f.name` and `lastUserModified`.
- Continue using the already validated `FreshRSS_Context::$sort` value; omit the hidden field when no non-empty override was requested.
- Add header-rendering regression tests for all nine supported sorts, absent/empty overrides and invalid input.
* A bit more fixing
* Fix line mistake
* Forgotten plaintext
* Fix test
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
## Problem
``p/api/fever.php`` logs the full Fever API key on every authentication failure:
```php
Minz_Log::warning('Fever API: wrong credentials! ' . $feverKey .
' ; Remote IP address=' . Minz_Request::connectionRemoteAddress(), API_LOG);
```
The Fever key is the user's equivalent API password and it does not expire, so it lands in ``data/users/<user>/log_api.txt`` (written at the default production log level) where anyone with log read access can harvest it. Failed-auth attempts are exactly the case where the submitted credential should never be persisted — this looks like a leftover from before the earlier log-scrubbing pass (#5001).
## Fix
Keep the remote IP address (useful for brute-force monitoring) but drop the key from the message. One-line change; no behavior change otherwise.
Closes#4616
The _Force HTTPS_ mechanism (`./force-https.default.txt` and `./data/force-https.txt`) was not documented. This adds a short section to the admin page about configuration without a user interface.
* Fix local DNS leak when fetching feeds through a proxy
Follow-up to #9390: `getRetryAfterFile()` passed the full URL to
`Minz_Request::serverIsPublic()`, which then resolved the target hostname
with the local resolver (`dns_get_record()`), even when a proxy was
configured. With a socks5h/socks4a proxy this leaks the target hostname
to the local DNS and defeats remote name resolution.
- `Minz_Request::serverIsPublic()`: add an `$allowDnsResolve` parameter,
and forward it to the recursive call so the guard holds for the whole
recursion.
- `FreshRSS_http_Util::getRetryAfterFile()`: do not resolve the host name
when a proxy is configured (`$proxy === ''`), since the proxy is
responsible for DNS.
---------
Co-authored-by: Inverle <inverle@proton.me>
For example to prevent an accidental tab closing during feeds refresh.
(beforeunload confirmation is used to achieve this, similar to leave validation)
* Update registerHook docs with newer PHP 8.1+ syntax for callables
* Use the correct function name for the second hook
* Use correct return type for the second hook function
See https://www.php.net/manual/function.version-compare.php
The extension's minimum required FreshRSS version must be compared against the currently running version, not the other way around.
Assuming the running FreshRSS version is `1.30.1`:
| Minimum required version | Expected status | Before this fix | After this fix |
|---|---|---|---|
| `1.27.0` | ✅ Compatible | ❌ Incompatible | ✅ Compatible |
| `1.30.1` | ✅ Compatible | ✅ Compatible | ✅ Compatible |
| `1.31.0` | ❌ Incompatible | ✅ Compatible | ❌ Incompatible |
At max-width 840px the text button is hidden and only the dropdown toggle
is shown. Origine restores its left border and radius with a rule that
used `#nav_menu_read_all .btn` until #9205 turned the ID into a class.
`.nav_menu_read_all .btn` is now weaker than the generic
`.stick .btn + .dropdown > .btn` and `.group .dropdown:last-child > .btn`
rules, so the toggle lost its left border and rounded left corners.
Use the same selector as the other themes.
Closes#9428
* Update websub_pending entry in sub.php
Translated the websub_pending attribute entry in sub.php in german
* make fix-all
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* docs: updates turkish websub_pending entry in sub.php
Modified the websub_pending attribute entry in sub.php in turkish
* make fix-all
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
The hidden CSRF input was the first child of the form, directly before the
Mark as read button. Theme rules such as `.stick input + .btn` and
`.stick .btn:first-child` therefore treated the button as the second element
of an input + button group: no left border and no rounded left corners.
Move the input back inside the dropdown, as it was before #9205.
Fixes#9359
Changes proposed in this pull request:
- In `app/layout/nav_menu.phtml`, move the hidden `_csrf` input of the "Mark as read" form back inside the `#nav_menu_read` dropdown (where it was before #9205), instead of being the first child of the form right before the `read_all` button.
- Because of that input, many theme rules written for `input + .btn` groups (`.stick input + .btn`, and `.btn:first-child` no longer matching) removed the left border and the rounded left corners of the "Mark as read" button. This affected Alternative Dark, Origine and the themes built on them (Dark, Dark-pink, Origine-compact), Pafat, and gave a stray separator and square corners in Flat, Ansum and Mapco.
* Sanitizer: block data: URI scheme, keep data:image for images
FreshRSS_SimplePieCustom only disallowed the `javascript` scheme, so
`data:text/html,...` URIs in feed content survived
sanitization on <a href>, <iframe src>, <audio|video|source|track src>.
The default CSP neutralises these on FreshRSS pages, but clients and
deployments without that CSP (API WebViews, extensions using amendCsp(),
proxies stripping headers) could execute them. Same class as
GHSA-3fw6-j8m4-82vj / #9272.
- Sanitize::is_allowed_uri(): element/attribute-aware check that still
permits inert `data:image/...` on img[src], image[src], video[poster]
while blocking `data:` elsewhere; replace_urls() now uses it.
is_allowed_scheme() is unchanged for backward compatibility.
- SimplePieCustom: disallow_uri_schemes(['javascript','data']).
- Tests: 4 blocked-input cases, a data:image keep-alive test and an
`unsafe:` prefix test. They fail without the patch and pass with it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Sync SimplePie
https://github.com/FreshRSS/simplepie/pull/92
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Closes#9394
## Summary
The _User CSS_ and _User JS_ sections already existed in the user documentation, but they did not say where to find the settings. This explains that each extension has a gear icon (_Manage_) next to its switch, and gives the steps to use both extensions.
## Changes
- `docs/en/users/05_Configuration.md`: describe the extension list (switch, gear icon, name), add the steps for User CSS, and the same for User JS with a note that it might ask to sign in again.
- `docs/en/users/05_Configuration.md`, `docs/en/admins/11_Themes.md`, `docs/en/contributing.md`: the "CustomCSS extension" links pointed to FreshRSS/Extensions, where it is no longer available. They now point to the built-in User CSS section.
- `docs/en/admins/15_extensions.md`, `lib/core-extensions/README.md`: list the two core extensions with links to their documentation. Also fix the `.lib/core-extensions` path.
- `CREDITS.md`: add my name.
Only the English documentation is changed.
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>
Translate the last untranslated Japanese strings and mark the Lithuanian
endonym as IGNORE, consistent with the other language names.
Co-authored-by: akinechan <1593847+akine@users.noreply.github.com>
Follow-up to #9369, as discussed in https://github.com/FreshRSS/FreshRSS/pull/9369#issuecomment-5954861516
Changes proposed in this pull request:
- In the reader view, the date in the line under the article title had a fixed `width: 155px`. A long date such as "28 September 2026 at 18:30" needs about 190px, so at desktop width it broke into two lines. This was already the case before #9369.
- `.content .subtitle.horizontal-list .item.date` now uses the 155px as `min-width` and has `white-space: nowrap`, so the box grows to fit the date and the date stays on one line.
- Short dates are unchanged: they keep the 155px box and the same position.
- If the feed name, authors and date do not fit on one row, the date moves to the next line as a whole (the `flex-wrap` from #9369), instead of breaking in the middle.
- In the `max-width: 840px` block the date is reset to `min-width: 0; white-space: normal`, so nothing can overflow on very narrow screens. The phone layout from #9369 is unchanged.
- `frss.rtl.css` was regenerated with `rtlcss`.
If we were to remove or rename `errorController`, an HTML injection could potentially be introduced due to the fallback code path, that directly echoes the logs without any escaping.
(but mainly removing it to reduce the amount of code, and since it's unused)
* Fix NAT64 feed fetching on IPv6-only hosts
Closes#9371
Regression from https://github.com/FreshRSS/FreshRSS/pull/9195
Changes proposed in this pull request:
* Fix feed fetching on IPv6-only hosts using NAT64.
* Preserve SSRF protection by validating the IPv4 address embedded in RFC 6052 NAT64 addresses before allowing it in `CURLOPT_RESOLVE`.
* Add regression tests for both public and private/reserved NAT64 addresses.
* Minor formating
* Add test tearDown
* RFC8215 64:ff9b:1::/48
Credits: @onurcangnc
---------
Co-authored-by: Viktor Platov <TapuGitHub@users.noreply.github.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Fix domain-wide Retry-After
- Fix the domain-wide HTTP `Retry-After` for public servers, which has not applied since #8195 (FreshRSS 1.28.0).
- `getRetryAfterFile()` passed the bare host name to `Minz_Request::serverIsPublic()`. That function expects a URL, so it always returned `false`.
- So a 429 or 503 paused only the URL that got it, despite the log saying "For that domain, will first retry after …", and FreshRSS went on requesting the other feeds of that server. With several Reddit feeds, for instance, each one got its own 429 in turn.
- `serverIsPublic()` now gets the URL. Its answer is kept per host for the rest of the request, because it resolves the host name (`dns_get_record()`), and a refresh may load many feeds of the same host.
- Fix the per-URL `Retry-After` of servers on a local network (#8195), which never applied to feeds whose URL, as stored, differs from the URL as requested.
- Stored URLs are HTML-encoded, e.g. `&` in an RSS-Bridge URL, and may end with `#force_feed`.
- `FreshRSS_Feed::load()` checked the stored URL, while the pause is stored for the URL as requested. It now checks the requested URL, as `loadJson()` and `loadHtmlXpath()` already do.
* Minor final
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* 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>
Follow-up to #9370, applying @math-GH's review:
- `latest`: „Veröffentlichte Version“ instead of „Versionierte Veröffentlichung“. I didn't go back to „Stabile Version“ because #9270 moved the English away from "stable" on purpose, as @Inverle pointed out; „Veröffentlichte Version“ pairs with „Aktueller Entwicklungsstand“ for edge.
- „berücksichtigen“ instead of „einbeziehen“, in both include labels.
- `global_hint` without „Sie“, as suggested.
In the reader view, the feed name, authors and date share one
horizontal-list row whose items are `flex: 0 0 auto` and whose date is
a fixed 155px wide. The row could neither wrap nor shrink, so a long
feed name or author list pushed the date past the article box, and on
a phone the whole page scrolled sideways.
Let the row wrap and let its items shrink, so a long feed name is
ellipsized and the authors wrap inside the box. On narrow screens the
date then sits on its own line, so drop its fixed width there.
Rows that already fit are laid out as before.
Closes#9360