### Motivation
- The GReader API enabled `JSON_THROW_ON_ERROR` globally and streams items with `json_encode()` without catching `JsonException`, so malformed UTF-8 in feed-derived fields could trigger an uncaught exception and abort streaming responses (resulting in 500/partial responses).
### Description
- Add `JSON_INVALID_UTF8_SUBSTITUTE` to `JSON_OPTIONS` in `p/api/greader.php` so `json_encode()` substitutes invalid UTF-8 bytes instead of failing while keeping `JSON_THROW_ON_ERROR` and other flags unchanged.
### Testing
- Ran a syntax check with `php -l p/api/greader.php`, which completed successfully.
------
[Codex Task](https://chatgpt.com/codex/cloud/tasks/task_e_69b8831ec0c48324810ea9ec05c16493)
* prefer feed.icon
Closes#5518
Changes proposed in this pull request:
- When a feed provides an icon URL (<image><url> in RSS 2.0/1.0, <atom:icon>/<atom:logo> in Atom, icon/favicon fields in JSON Feed), that URL is stored as a feedIconUrl attribute on the feed and used as the primary source for favicon downloads, instead of scraping the feed's website for <link rel="icon"> tags.
- If the feed-provided icon URL fails to return a valid image, the existing fallback chain (website HTML favicon search → /favicon.ico) is preserved.
Custom favicons uploaded by users always take priority and are never overridden.
How to test the feature manually:
1. Add an RSS feed that includes a <image><url> element (e.g. an RSSHub feed: `https://rsshub.app/youtube/channel/UC2cRwTuSWxxEtrRnT4lrlQA`). After actualization, confirm the feed's favicon matches the avatar image from the feed, not the Bilibili site favicon.
2. Add an Atom feed containing <atom:icon> or <atom:logo> Confirm the feed icon is used.
3. Add a JSON Feed (spec: icon field). Confirm icon is preferred over favicon when both are present.
4. Temporarily point a feed's <image><url> to a broken URL. Confirm FreshRSS falls back to the website favicon silently.
5. Upload a custom favicon for a feed, then actualize it. Confirm the custom favicon is not replaced.
<img width="470" height="317" alt="image" src="https://github.com/user-attachments/assets/17445154-d94c-44d6-b7e7-019bf24c5767" />
* fix(favicon): use htmlspecialchars_decode for feed image URL
* Decode quotes as well
* New function in our SimplePie fork
https://github.com/FreshRSS/simplepie/pull/73
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Convert feed view into grid where appropriate
This makes the feed view prettier on mobile, if thumbnails and summary are shown, as discussed in https://github.com/FreshRSS/FreshRSS/discussions/8629
**Changes proposed in this pull request:**
- Converts Feed Item list into multiple lines
- But only if both thumbnails and summaries are shown
The code is quite different from what I had done in my own hack: There I had used `display: flex` and then counted items, but that only works for my own hack: here we don't know how many items a given user may have, so we use `display:grid` instead with name grid areas.
**How to test the feature manually:**
1. Ensure you enable both thumbnails and summaries
2. Check out the feed view on mobile
3. Play around with enabling and disabling bookmarks, share button, etc.
**Pull request checklist:**
- [x] clear commit messages
- [x] code manually tested
- [ ] unit tests written (optional if too hard)
- [ ] documentation updated
**Screenshots:**
<img width="458" height="1173" alt="SCR-20260324-jnte" src="https://github.com/user-attachments/assets/1d5c1615-961a-4953-8157-f9f8a5470545" />
<img width="459" height="1172" alt="SCR-20260324-jnwf" src="https://github.com/user-attachments/assets/96ffcea3-384f-4de9-9c50-3366022713d8" />
* remove gap
* fix lint errors
* fix style errors
* remove whitespaces
* Reduce use of slow `:has()` CSS selector
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* New SQL wrapper function `fetchInt()`
* Favour use of `fetchAssoc()`, `fetchInt()`, `fetchColumn()`
* Favour Nowdoc / Heredoc syntax for SQL
* Update indenting to PHP 8.1+ convention
* Favour `bindValue()` instead of position `?` when possible
* Favour `bindValue()` over `bindParam()`
* More uniform and robust syntax when using `bindValue()`, checking return code
* Add option to hide sidebar by default (#8515)
Closes https://github.com/FreshRSS/FreshRSS/issues/8515
Changes proposed in this pull request:
- Add `sidebar_hidden_by_default` user preference (default: `false`)
- Add a checkbox in the Display settings page to toggle this preference
- Hide the sidebar on page load in `normal` and `reader` views when the preference is enabled and no session state exists
- Add English translation key; mark all other languages as `TODO` for translators
How to test the feature manually:
1. Go to Settings → Display, tick "Hide sidebar by default", save
2. Go back to the main view (normal or reader): the sidebar should be hidden on load
3. Toggle the sidebar manually — it should open/stay open for the rest of the session
4. Open a new tab: the sidebar should be hidden again
5. Go to Settings → Display, untick the option, save: the sidebar should now be visible on load as usual
6. Check that the sidebar is always visible on Settings pages regardless of the preference
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: PR comment
* Update app/i18n/pl/conf.php
* i18n: fr
* make fix-all
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Inverle <inverle@proton.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Implement filter on last modified date by server
Especially relevant for API, to get the modified changes: the API will now return the articles that are new or which content has been modified since `ot`:
fix https://github.com/FreshRSS/FreshRSS/issues/7304
fix https://github.com/FreshRSS/FreshRSS/issues/2566https://github.com/jocmp/capyreader/discussions/533#discussioncomment-11341808
New corresponding search operator `mdate:` and new UI:
<img width="650" height="627" alt="image" src="https://github.com/user-attachments/assets/8ba02937-abc7-44bf-b718-cf269cc37caf" />
* Migration from existing id column
* Fix auto-update
* Index after update for performance
* Minor comment
* Minor whitespace
* Fix regex
* Minor .gitignore
* Changelog and warning
* Update app/i18n/pl/gen.php
Co-authored-by: Inverle <inverle@proton.me>
* make fix-all
* Optimise SQL auto-update
For speed and resilience
* Minor SQLite change of sequence
* Changelog
* Speed optimisation: No DEFAULT 0
* Better migration
* Revert small bug
* Prepare filtering on multiple dates for API
* make fix-all
* Update tests
* Remaining manual merge
* Update versions
* Remove warnings no longer relevant in changelog
* Implement in API, and COALESCE
* No lastModified when adding new article
* Rework logic
* Sort IS NOT NULL
* Remove forgotten lastModified
---------
Co-authored-by: Inverle <inverle@proton.me>
Closes https://github.com/FreshRSS/FreshRSS/issues/7330
- Default behavior is same
- Added FreshRSS_Context::userConf()->html5_disable_notif so that, it determines weather user wants notification. (will not show any even it has permission) (i want default false so disable, so old configs get proper default values)
- Added button such that checking it makes it request permission too
<img width="707" height="119" alt="image" src="https://github.com/user-attachments/assets/a0fdbc4d-9f15-4644-8753-f0e6c979677f" />
- test notification actually happening (how can i trigger it, do i have to wait it), this code fixes permissions.
Follow-up of https://github.com/FreshRSS/FreshRSS/pull/2630 now that we have PHP 7.3+ (even PHP 8.1+).
* The new syntax natively supports `samesite`, and also avoids the need of re-setting all parameters.
* Use automatic path instead of own function `getCookieDir()`.
Follow-up of https://github.com/FreshRSS/FreshRSS/pull/8446
* Sanitize lifetime of session cookies from PHP ini to avoid likely invalid/misunderstood values
Closes https://github.com/FreshRSS/FreshRSS/issues/8435
Changes proposed in this pull request:
- update validator links to use the same open-url handler with prefix + encoding
- ensure the validator link reflects the current #url field value before opening
- keep existing open-url behavior for other links unchanged
How to test the feature manually:
1. Open feed edit (or add feed) form.
2. Change the feed URL in the URL field.
3. Click “Check the validity of the feed” and verify it opens the validator with the updated URL.
* Add a reference to the check where the warning is triggered
Note:
This is the place where the admin sees the browser pointing to.
Adding this information here allows the admin to find the matching
documentation entry.
* Update Content-Security-Policy section of english ServerConfig documentation
Note:
This fixes some minor formating/typping issues and adds some clarity to
the fact that this warning is also triggered on correctly configured
hosts, simply due to the nature of how correctness of CSP rules are checked.
* Move CSP infor source-code comment into console.info
Note:
Improve visibility of why this is happening.
* Point towards static website documentation instead of git
This URL should also remain more fixed even accross new branches/releases.
Co-authored-by: Inverle <inverle@proton.me>
* Minor fixes
* Remove overwrite part
---------
Co-authored-by: Inverle <inverle@proton.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Bump to API level 4 for Fever; add with_ids so we can mass-change read/unread/saved/unsaved on lists of articles.
As discussed in https://github.com/FreshRSS/FreshRSS/issues/8305
A small enhancement to the Fever API to vastly increase efficiency when bulk marking items.
-
How to test the feature manually:
The client at:
https://sources.vsta.org:7100/FeedMonkey/file?name=js/Fever.js&ci=tip
has code to talk to this API enhancement (when the API level says it's supported). In particular walkArticles().
* Update indentation per PR check on Github
* make fix-all
* Line length
* Fix many typing issues
* is_numeric
* Update comment
---------
Co-authored-by: Andy Valencia <ajv-899-334-8894@vsta.org>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Follow-up of https://github.com/FreshRSS/FreshRSS/pull/8281
todo:
* [x] Include labels (prefix `t_`) too
* [x] Keep sidebar scrollTop when using the nav menu
* [ ] ~~Make this work in the reader view's sidebar too~~ for separate PR
* [x] Prevent whole page from scrolling on `scrollIntoView()` call, just scroll in the sidebar (probably related: https://github.com/FreshRSS/FreshRSS/pull/8306#issuecomment-3647414618)
This TODO will be done in a separate PR since it requires optimizing the sidebar toggle code.
edit: it does work on Chrome already though, but only if `#stream` isn't too large / breaks randomly (Firefox is slower it seems)
Previously if you were to go to for example *Subscription management* and filter a feed, the feed wouldn't be visible in a sidebar with lots of feeds, since you'd have to scroll to it first. Now, this is no longer the case.
Note that if the navigation comes from the sidebar itself, the original behavior remains. (scroll into previous `scrollTop` value of sidebar)
Also improves experience of using shift+j/k (see https://github.com/FreshRSS/FreshRSS/pull/8057)
Fix https://github.com/FreshRSS/FreshRSS/issues/8268
To better support user management on FreshRSS instance with many users.
SQL speed improved. On a reduced test with 5 users, including some large accounts (PostgreSQL on a very tiny and slow server), improving from ~2.3s to ~1.8s, which gives ~20% speed improvement.
Then tested with 1000 users, with only the default feed (on my old desktop computer):
```sh
for i in {1..1000}; do ./cli/create-user.php --user=freshrss$i --password=freshrss; done
app/actualize_script.php
cli/access-permissions.sh
```
SQLite:
```console
$ time cli/user-info.php | wc -l
1001
real 0m1.366s
user 0m0.908s
sys 0m0.475s
```
PostgreSQL:
```console
$ time cli/user-info.php | wc -l
1001
real 0m28.498s
user 0m12.137s
sys 0m2.217s
```
MariaDB:
```console
# time ./cli/user-info.php | wc -l
1001
real 0m49.485s
user 0m1.276s
sys 0m2.258s
```
Yes, SQLite is much faster - not a surprise for such use-cases, where the TCP connection is not re-used.
I have added some CLI options to disable some statistics:
```sh
cli/user-info.php --no-db-size --no-db-counts
```
For the Web UI, I have disabled detailed user statistics if it takes too long, and retrieve missing user statistics asynchronously via JavaScript. Lazy loading of the user details based on IntersectionObserver, with maximum 10 requests in parallel.
Web UI tested on 1000 users as well. Checked with SeaMonkey.
There was not enough difference between selected and not-selected articles, especially not due to hovering using the same style.
Already relevant as is, and more relevant for a future feature depending on selected article.
Additional help welcome to further improve the styling, in this PR or a future one, also for the other themes.
In this example, the third article is the one selected:
<img width="443" height="189" alt="image" src="https://github.com/user-attachments/assets/06168e9f-1b91-47e2-9b64-4fd8ca6c7186" />
* Compensate the increased border-left-width
* Implement in Pafat
* Implement in Nord
* Shorten comment
* Implement in Alternative Dark
* Implement for Flat
* Implement for Swage
* Implement in Ansum and Mapco
* Call stylelint_fix before rtlcss
So that RTLCSS includes the fixes from previous step
* Housekeeping lib_rss.php
`lib_rss.php` had become much too large, especially after https://github.com/FreshRSS/FreshRSS/pull/7924
Moved most functions to other places.
Mostly no change of code otherwise (see comments).
* Extension: composer run-script phpstan-third-party
* Fix 7307 - Scroll after load
Closes https://github.com/FreshRSS/FreshRSS/issues/7307
- Wrap the body of `loadDynamicTags()` into a Promise
- inside `mylabels()`, wait for the promise to complete, then scroll
- when `loadDynamicTags()` is not being called, return a self-resolving promise
How to test the feature manually:
1. Click on an long article
2. press keyboard shortcut `l` to go to "My labels"
3. page should scroll down
4. the `My labels` popup should be fully visible
* Rewritten as async/await
* Explicit HTTP Accept application/json
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>