* Add a link checker workflow using lychee
Depends on #9207 (merged)
https://github.com/lycheeverse/lychee-action
* Adjust cron to run once every month instead
* Apply suggestions
* Pass zizmor's pedantic audit
* Set `failIfEmpty: false` for lychee action
* Add offline Lychee check to catch broken links after renames
* Add workaround for `--files-from` option not respecting `extensions`
* Escape `?` correctly in `.lycheeignore`
* Don't accept timeouts, retry instead
* Bump link checker workflow to `ubuntu-26.04`
* Always scope to host correctly in `.lycheeignore`
* Set `hidden = true` in config to ensure that dotfiles are scanned
* Exclude issue template from the scan
* with open
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
convertJsonToRss() HTML-escapes item fields before rendering the RSS,
but the is_string() guard skipped 'tags', which is always an array.
An unescaped & in a JSON category then produced invalid XML, so
SimplePie rejected the whole document and the feed showed as empty.
Minz_Helper::htmlspecialchars_utf8() already handles arrays,
so the guard is dropped. Add a test for array and string categories.
fix https://github.com/FreshRSS/FreshRSS/issues/9488
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes#4942
- Explain in *How to update* that data and extensions stay in the Docker volumes, so updating only means running a newer image with the same volumes, and recommend a back-up first.
- Add the Docker Compose variant (`pull` then `up` with the same `-f` files).
- Warn to keep the same major version of the PostgreSQL image when updating FreshRSS (a new major version refuses to start with older data), pointing to *Migrate database*, and to check `.env` variable names when replacing Compose files with newer copies.
Note: The changes in this PR are entirely AI-authored but manually reviewed and tested
## Summary
Translate the remaining French prose and French-derived identifiers in the main FreshRSS/Minz codebase to English.
Why: helps to understand the code
## Scope
- Minz comments, PHPDoc, and paginator identifiers
- Application comments and related identifiers
- Installer database identifiers
- Theme metadata descriptions and the identified CSS comments, previous discussion: https://github.com/FreshRSS/FreshRSS/pull/8241#discussion_r2553181653
- Related call sites and view helpers
External libraries and intentional i18n/docs content are excluded.
* New hook: `Minz_HookType::AutofillField` for auth views
Currently to be used in the Demo extension, because it overrides both the formLogin and reauth views (for autofilling username/password), it can be broken every time there is a slight change in core.
For example, after https://github.com/FreshRSS/FreshRSS/pull/9358.
In the future the hook can be extended to support more fields.
Demo extension PR: https://github.com/FreshRSS/xExtension-Demo/pull/6
* Suggested alternative without returning HTML code
* 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>