* Fix non-ASCII characters in scripts of fetched HTML pages
To add a <base> to a fetched HTML page, enforceHtmlBase() re-serialises it
with libxml2 saveHTML(). For a whole document, libxml2 2.9 only knows the
charset from <meta http-equiv="Content-Type">: with <meta charset="UTF-8">
or a byte order mark, it writes every non-ASCII character as an HTML entity.
In text that is harmless, but inside <script> entities are not decoded, so
e.g. the JSON read by HTML + XPath + JSON feeds gets "Wall Street’s"
instead of "Wall Street’s", which then shows in titles.
UTF-8 documents are now serialised node by node, which writes UTF-8.
Other charsets keep the previous output.
* Clarify comment about the byte order mark
* Remove comment about the byte order mark
* Minor test syntax preference
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Extensions that process new articles in bulk (summaries, exports,
notifications) have no way to react to the end of a refresh:
freshrss_user_maintenance runs before the feeds are fetched, and new
articles stay in the temporary table until the whole loop is done.
The new FeedsAfterActualize hook runs once the new articles are
committed, with the refreshed feeds whose articles changed and the
number of new articles: after a cron or web refresh (actualizeAction,
including the commit-only request of the web interface) and after
actualizeFeedsAndCommit (CLI, API, WebSub, feed added).
Co-authored-by: ChrisB85 <792899+ChrisB85@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Replace bcrypt challenge auth with direct password comparison
Closes#2428
Highlights:
* Replaced the bcrypt challenge authentication flow with direct password comparison against the bcrypt hash stored in the user configuration.
* Removed the `bcrypt.js` dependency, so authentication no longer depends on JavaScript.
* Removed the nonce and challenge mechanism and the `nonce` endpoint from the `javascript` controller.
* Removed the JavaScript and HTML code used for the previous authentication flow.
* Removed the `<noscript>` blocks that were only needed for the previous authentication flow.
* Updated `FreshRSS_FormAuth::checkCredentials()` to accept the username, stored bcrypt hash, and plain-text password directly.
* The stored password hash must still use the expected bcrypt format.
* Empty or whitespace-only passwords are rejected even if the hash would otherwise match.
* The bcrypt 72-byte limit is enforced during normal authentication.
* Existing bcrypt passwords are handled correctly when the 72-byte limit cuts through a UTF-8 character.
* Password input is no longer trimmed before authentication or password changes. Leading and trailing whitespace is therefore kept as part of the password.
* Added `FreshRSS_FormAuth::passwordRequirementsMet()` to replace `FreshRSS_password_Util::check()`.
* New passwords must be between 7 and 72 bytes by default.
* The minimum length check can be disabled where needed.
* Password requirements are now checked server-side for all password creation and update paths.
* Added client-side byte-length validation for password fields using `TextEncoder` and `data-max-bytes`.
* This gives better feedback for UTF-8 passwords.
* Server-side validation remains authoritative.
* Updated password handling in authentication, re-authentication, self-deletion, user creation, and user update paths so passwords are not trimmed automatically.
* Added `FormAuthTest` to cover:
* password length limits in bytes
* UTF-8 passwords around the 72-byte limit
* bcrypt truncation behavior during authentication
* malformed and non-bcrypt hashes
* empty passwords
* authentication failures and log messages
* Removed `bcrypt.js` from the documented dependencies and updated the authentication documentation.
* Updated the installation and authentication views and JavaScript to match the new password handling.
This simplifies the authentication flow, removes the client-side bcrypt challenge, makes password validation consistent between the client and server, and adds more test coverage while keeping compatibility with existing bcrypt password hashes.
Because the password is now sent directly to the server instead of being processed by `bcrypt.js`, deployments should use HTTPS. Documentation for HTTPS setup can be handled separately.
* Add test case with password truncated down to 71 characters
* Compare log messages too during form auth tests
* Mark the `getLastLog()` method as impure
* Fix: Check password reqs against new password instead of current
* Fix: Add custom JS validator for UTF-8, handle cut-off UTF-8 during login
* Fix: Enforce password requirements everywhere server-side
* Add TODO for updateUser in CLI
* make readme
* Fix extra.js in install.php
* Don't report validity yet if the password is too short or not filled out
* Simplify checking of whether to report validity
* Fix: Do not trim password from user's input
* Also `trim: false` in some userController paths
* Use the proper argument order for PHPUnit's `assertSame()`
* Remove `maxlength` in favor of `data-max-bytes`
But add `pattern` again for some nojs support
* make readme
* composer run-script translations
* Update i18n string for web form admin setting
* i18n: fr
* Minor whitespace
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
convertJsonToRss() collected item attachments into
$rssItem['attachments'], but FreshRSS_Entry::fromArray() ignores that
key, so attachments from JSON Feed and JSON dot notation feeds never
reached the generated RSS and were lost. Store them in the entry's
`enclosures` attribute instead, HTML-escaped like the other fields.
- Attachments of JSON Feed and JSON dot notation feeds (`itemAttachment`, `itemAttachmentUrl`, `itemAttachmentType`, `itemAttachmentLength`) were collected by `convertJsonToRss()` into `$rssItem['attachments']`, but `FreshRSS_Entry::fromArray()` never reads that key. So the generated RSS had no `<media:content>` for them and the enclosures were silently dropped. For example, a JSON Feed podcast item with `"attachments": [{"url": "https://example.net/1.mp3", "mime_type": "audio/mpeg"}]` showed up without its audio file.
- The attachments are now stored in the entry's `enclosures` attribute, which is what `rss.phtml` renders. The URL and type are HTML-escaped like the other fields, the length is kept only when numeric, and attachments without a URL are skipped.
- Unit test converting a JSON item with an attachment whose URL contains `&`, asserting the `<media:content url="…&…" type="audio/mpeg" length="1234">` element and valid XML. It fails without the fix.
Closes#3952
- Replace the outdated Rue89 walkthrough and the list of site-specific selectors (Rue89, PCINpact, Lesnumériques, Phoronix) with step-by-step instructions to find a selector, based on a generic HTML example: use the page source (JavaScript is not run), pick the element containing only the article text, prefer an id or class shared by all articles, and check with the preview button.
- Explain how the retrieved content is used: all matching elements are kept, only new or updated articles are retrieved (with *Reload articles* for existing ones), the content action, the conditions for content retrieval, what happens when nothing matches, sanitized `id`/`class` attributes, and the cookie and user agent options.
- Document *CSS selector of the elements to remove*, including its use without an article CSS selector, where classes must be matched with `[data-sanitized-class~="…"]`.
- Normalize label names parsed from Google Reader JSON categories during import, using the same escaping helper as the other imported string fields.
- Decode stored label names before returning them to the dynamic tag dropdown.
- Build dynamic tag dropdown entries with DOM text nodes instead of HTML string concatenation.
* Add Arabic translation
Add a complete Arabic (ar) locale: all interface strings in
app/i18n/ar, Arabic plural forms (nplurals=6), registration of the
language name in gen.lang.ar for all languages, and the regenerated
translation status tables in the READMEs.
* Fix Arabic plural rule flagged by PHPStan
* 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)