- 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.
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.
## 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.
For example to prevent an accidental tab closing during feeds refresh.
(beforeunload confirmation is used to achieve this, similar to leave validation)
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
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>
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`.
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
Since #9170, opening the menu of a label in the sidebar throws
`TypeError: Cannot set properties of null (setting 'href')`, as the
menu of labels has no website link. The rest of the click handler is
then skipped, so for instance the menu does not flip upwards when there
is no room below it.
Assisted-by: Claude <noreply@anthropic.com>
* Check SVG optimization in the tests CI workflow with SVGO
Previously it was only recommended by the documentation that SVG images should be optimized using [SVGOMG](https://github.com/jakearchibald/svgomg), the web GUI version of https://github.com/svg/svgo (CLI)
Now all SVG images are optimized, and optimization is mandatory enforced by the tests CI workflow.
\+ SVGs are sanitized of potentially malicious JS code using the https://svgo.dev/docs/plugins/removeScripts/ plugin
* Output prettified SVGs instead and remove unchanged files from stdout
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Use `removeScripts` SVGO plugin to reduce risk of malicious SVGs
* Move the CLI options to the JS config instead
* Disable `removeComments` plugin included in the default preset
* editorconfig, and easier config
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* 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>
* 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
* Allow jumping from category to feed with keyboard shortcuts
* It's now possible to jump from categories to feeds (press shift+j when focused on a category)
* Similarly, it's possible to jump from a feed to the current category by using alt+k
* If the very first feed is focused and the user presses shift+k, there is no jump to the last unread feed in that category anymore, instead nothing happens.
* Same if the very last is focused and the user presses shift+j (no jump to the first unread feed in that category)
* Alt+home/end (first/last category) now jumps regardless if the first/last category is read or unread
* In addition, shift+home/end does not jump between unread feeds anymore, now it jumps between the first visible feed in the category and the last visible feed in the category
* Alt+h (jump to next unread category) now fall backs to the same behavior as alt+j (jump to next category) if there are no more unread categories after
* Remove dead `jump_to_unread` code in `prev_feed`
* Jump to first/last categories even if they're not unread
* Ensure jump is only to visible items
* Focus current category if jumping from feed
* `delayedClick(null)` will now throw an error
* Remove stale dynamic favicon links
- remove every previous dynamic favicon link before appending the updated unread-count icon
- prevent stale duplicate favicon links from being preferred by browsers
Fixes#8237.
* Replace dynamic favicon in place
---------
Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Inverle <inverle@proton.me>
Minimal changes borrowed from https://github.com/FreshRSS/FreshRSS/pull/8990
Allows usage of <kbd>Ctrl/Shift</kbd> keys with the open website shortcut, so that you can open an article either in a new background tab or a new window.
Doesn't work in Chromium browsers, the website opens normally in a new tab there instead. (Firefox only)
GReaderAPI::authorizationToUser() compared the long-lived GReader
bearer token with the non-constant-time === operator instead of
hash_equals(), unlike every other token/secret comparison in the
codebase (Auth::checkCsrfProtection(), FeverAPI, the OPML/RSS feed
token check, and the short-lived GReader token check a few lines
below in the same file), all of which already use hash_equals().
This brings the long-lived token check in line with the existing
hash_equals(sha1(...), $token) pattern already used for the
short-lived token a few lines below.
Co-authored-by: senti-man <sentiman49@gmail.com>
HTML injections outside of a form with a `_csrf` field set, could instead refer to the CSRF token form with a payload such as:
```html
<button form="post-csrf" formaction="./i/?c=auth&a=logout" formmethod="post">Continue reading</button>
```
While the `post-csrf` form is useful for reducing the amount of code needed (no need to repeat the `_csrf` field in forms), I don't think it's worth the added security risk.
* feat(api): Add detection of JSON feed when subscribing via the Google Reader API.
* Added self to CREDITS.md
* Rather than making a network request, use heuristics based on the URL string to determine if the feed URL likely points to a JSON feed.
* Minor syntax preference and comment reduction
* Minor syntax
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Detect only the share menu that belongs directly to the share dropdown.
An extension can add an unrelated nested `.dropdown-menu`. The previous descendant selector then skipped share-menu creation, leaving share URL placeholders unreplaced.
Fixes#7820.
Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
* Fix auto-share shortcut on first use
- Re-read the share actions after creating the lazy-loaded share menu.
- Restore the documented one-key share behavior when exactly one action is configured.
## Why
The shortcut checked the action count before it built the menu. On first use it therefore found no action and required a second press.
Fixes#7299.
* Slight variable scope adjustment
---------
Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Redirect away from wrong URL paths to fix displaying of relative URLs
* Also strip path info for API pages
* Error for API
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* 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>
Closes#3662. Closes the extension settings slider when its reset/cancel control is used while preserving unsaved-change confirmation.
---------
Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
* 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>
* fix: stop the unread poller after the session ends
refreshUnreads() polls nbUnreadsPerFeed every 2 minutes and never checks
the response status, so once the session ends it keeps polling a dead
session indefinitely. Each request is a 401 (auth proxy / OIDC image) or
403 (stock); behind fail2ban the repeated failures ban the client IP.
Handle it like the other badAjax(status == 403) call sites: on a 401/403,
reload to re-authenticate instead of retrying.
* Enlarge HTTP 4xx status range
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Use :user-invalid instead of :invalid for form field styling
Every theme styled required inputs and selects with :invalid, which
matches as soon as an empty required field renders, before the user
has even touched it. This made fields such as the username on the
"Add user" admin form show a red border on first load, with nothing
wrong yet.
Switch to :user-invalid, which only matches after the user has
interacted with the field (e.g. on blur or submit), so a field only
turns red once it is actually flagged as invalid by the user's own
input. Browser support for :user-invalid is now good enough across
evergreen browsers (~89%, per caniuse) for this to be safe, which
was the blocker that stopped a previous attempt at this fix in #3724.
Fixes#3647
* CSS: support legacy Gecko invalid styles
* Drop legacy -moz-ui-invalid
Not important enough
---------
Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Dispatch a JS event when an entry finishes being marked read/unread
Extensions that render their own read/unread indicator outside of the
main article list (e.g. a custom reading pane) had no way to know when
a mark read/unread AJAX request completed, so their own UI could get
stuck (e.g. a spinner never reset).
Dispatch a `freshrss:entryStateChange` event on `document` with the
entry id and new state once the toggle has been applied, following the
existing `freshrss:*` JavaScript event convention used elsewhere in
main.js.
Fixes#8862
* Minor preferences
---------
Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
`document.createEvent('Event')` followed by `Event.initEvent()` is
deprecated (https://developer.mozilla.org/en-US/docs/Web/API/Event/initEvent).
Replace the two remaining usages in `main.js` with the standard
`new Event(name, { bubbles, cancelable })` constructor, matching the
pattern already used in `extra.js`. The `bubbles` and `cancelable`
flags are kept identical (both `true`) so dispatch behaviour is
unchanged.
Fixes#5152
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add a :lang(ja)-scoped font-family in the shared frss.css so kanji/kana
render with Japanese glyphs on Japanese UIs, without affecting Chinese
(zh-CN/zh-TW) users. Revives the intent of PR #5671 with a
cross-platform font list. frss.rtl.css is regenerated via rtlcss.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>