185 Commits
Author SHA1 Message Date
Alexandre Alapetite fd3f75b3a2 Require PHP 8.2+ (#9470)
Drop PHP 8.1 (EOL December 2025), drop Ubuntu 22.04 (EOL May 2027, and there is now 24.04 and 26.04 LTS).
PHP 8.1 is already EOL, and Debian, Alpine, Fedora, Red Hat, Synology have dropped PHP 8.1 already.
See https://github.com/FreshRSS/FreshRSS/discussions/5474#discussioncomment-16791937

With PHP 8.6 around the corner and in particular a relevant `FILTER_FLAG_GLOBAL_RANGE` in PHP 8.2+.
* https://www.php.net/releases/8.2
* https://www.php.net/filter.constants.validation#constant.filter-flag-global-range
* https://www.php.net/class.sensitiveparameter
* https://php.watch/versions/8.2/readonly-classes
* https://www.php.net/function.ini-parse-quantity

Allows updating to PHPUnit 11.x instead of 10.x
https://phpunit.de/supported-versions.html

Contributes to https://github.com/FreshRSS/FreshRSS/issues/9475

* Add regression test for FILTER_FLAG_GLOBAL_RANGE
https://github.com/FreshRSS/FreshRSS/pull/9470#discussion_r4371168720
2026-10-26 21:02:32 +01:00
Frans de JongeandAlexandre Alapetite 54391ce1fb Fix Retry-After cache when proxy is used (#9508)
* Fix Retry-After cache when proxy is used

Because of #8029 and #8218, a proxy would never apply Retry-After.

Possibly related to the problem described in <https://github.com/FreshRSS/FreshRSS/pull/7760#issuecomment-6395796130> by @DamonHD.

* work around line length 168 instead of 165

* Update app/Models/Feed.php

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-10-26 20:44:26 +01:00
Shaurya Kesarwani 931fb41640 Keep JSON feed attachments as enclosures (#9510)
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="…&amp;…" type="audio/mpeg" length="1234">` element and valid XML. It fails without the fix.
2026-10-26 08:47:23 +01:00
Ron RichterandClaude Opus 5.5 22c3128b23 Fix escaping of JSON dot notation categories (#9489)
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>
2026-10-23 23:06:29 +02:00
spa77kandAlexandre Alapetite c5dd38b0d5 Preserve explicit sort overrides when searching (#9467)
* 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>
2026-10-19 20:27:22 +02:00
Alexandre Alapetite 7e8ef628d9 Better doc and log proxy cases (#9436)
Fix https://github.com/FreshRSS/FreshRSS/issues/9423
And strip redundant proxy scheme.
And add a few more unit tests.
2026-10-13 19:50:38 +02:00
0187b15c8e Sanitizer: block data: URI scheme while keeping data:image for images (#9417)
* 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>
2026-10-11 20:36:09 +02:00
abp002andAlejandro Borrego 48547b4a6c Reduce memory consumption of export (#9350)
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>
2026-10-05 21:47:19 +02:00
Alexandre Alapetite c7396a3a68 Update to GT\CssXPath 1.6.0 (#9411)
https://github.com/phpgt/CssXPath/releases/tag/v1.6.0
2026-10-05 21:44:57 +02:00
d5e13066bc Fix NAT64 feed fetching on IPv6-only hosts (#9372)
* 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>
2026-10-03 21:24:33 +02:00
Nikolai ShcheglovandAlexandre Alapetite f65d2760e3 Fix domain-wide Retry-After (#9390)
* 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. `&amp;` 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>
2026-10-03 10:22:47 +02:00
Nikolai ShcheglovandAlexandre Alapetite a93bd27b7d Fix muting of feeds gone with HTTP 410 (#9391)
* 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>
2026-10-03 09:59:51 +02:00
Inverle 1f055da218 Avoid writing to log files during unit tests (#9373)
* Avoid writing to log files during unit tests

* Translate PHPDoc of `record()`

* Move environment variable into `phpunit.xml` and use tabs for indents
2026-09-30 21:50:47 +02:00
Inverle 01afc061d7 Slightly more robust SessionTest and cleanup (#9376)
* Slightly more robust SessionTest and cleanup

Improvement of #9311

I noticed that my `/tmp` directory got filled up with random `sess_*` files and empty `frss_test_sessions_*` directories after repeatedly running PHPUnit, which means there was no proper cleanup being done in the tests.

See the first commit for changes, since I also moved the test to a more appropriate directory name (alongside `tests/lib/Minz`, `tests/app/Minz` was newly added in #9311).

Before the PR:
* Check output of `ls -lah /tmp/{frss_test_sessions_*,sess_*}; rm -r /tmp/{frss_test_sessions_*,sess_*}` to confirm there are initially no temporary files existing.
* Run `composer run phpunit`.
* Check output of the previous command again to confirm there is a new `sess_*` file and an empty `frss_test_sessions_*` directory.

After the PR:
* Check output of `ls -lah /tmp/{frss_test_sessions_*,sess_*}; rm -r /tmp/{frss_test_sessions_*,sess_*}` to confirm that no temporary files exist initially.
* Run `composer run phpunit`.
* Check output of the previous command again to confirm there are no temporary files, since they are now being cleaned up correctly.

* Move SessionTest from tests/app/Minz to tests/lib/Minz
2026-09-30 21:34:27 +02:00
Alexandre Alapetite ad4723cb1f Warn during session regenerate fail (#9311)
Might provide more logs for e.g. https://github.com/FreshRSS/FreshRSS/discussions/9305
2026-09-19 14:33:25 +02:00
Jamal Kamaladdinoglu 2bb901d9c4 Accept a trusted proxy address given without a subnet (#9301)
TRUSTED_PROXY=192.168.1.1 is ignored and only 192.168.1.1/32 is honoured. checkCIDR() splits the range on the slash and rejects the value when the mask part is missing, so an address written without a subnet never matches.

An address without a subnet is now read as a single host: /32 for IPv4 and /128 for IPv6. That is the format docs/en/admins/09_AccessControl.md points at.

Host names such as gateway.localdomain are still not resolved.

tests/app/Utils/httpUtilTest.php gains 16 cases for checkCIDR, two of which fail without this change.

Refs #9210
2026-09-13 22:34:39 +02:00
Morax cc45db5ef8 Fix adaptive reading state after marking articles read (#9290)
Closes #9288

Changes proposed in this pull request:

The mark-as-read links contain the resolved state, so the automatic reading preference becomes an explicit `state=2` filter after the redirect. Once the last unread article is marked read, the page stays empty instead of showing all articles.

- Keep the requested state separately from the resolved state used by the action. A redirect state of `0` reapplies the reading preference; explicit filters still survive the redirect as intended by #9007.
- Carry that value through the menu, footer and “Load more” link. This matters with oldest-first pagination, where the last footer supplies the menu button's action.
- Add request-state and rendered-pagination tests, plus a changelog entry.

How to test the feature manually:

1. With both read and unread articles, choose “Show unreads if any, all articles otherwise”. Mark all as read from the footer, then repeat using the top button. The resulting page should show the read articles.
2. Set one article per page and oldest-first sorting. Load the remaining articles before marking all as read; the same fallback should work.
3. Select an explicit unread filter: marking everything read should still leave an empty unread view. Also check a favourite filter with a search term, including “Mark selection unread”; the search and favourite scope should remain intact.

Validation: `make NO_DOCKER=1 fix-all` and `make NO_DOCKER=1 test-all` pass with PHP 8.5.9 (719 PHPUnit tests, 1,378 assertions). Browser checks passed against SQLite in Chromium (desktop and mobile viewport) and Firefox. The original footer fails three of the six new rendered-pagination cases.
2026-09-11 08:18:09 +02:00
Alexandre Alapetite 61d7925af1 Config + increase default values for search max length and depth (#9280)
Fix https://github.com/FreshRSS/FreshRSS/discussions/9279
Follow-up of https://github.com/FreshRSS/FreshRSS/pull/9277

Values can be overridden in `data/config.php`, or in `./data/config.custom.php` before the install process
2026-09-10 22:14:55 +02:00
AdamKorczandAlexandre Alapetite dc5cf8221b Limit the length and parentheses nesting depth of a search query (#9277)
* Limit the length and parentheses nesting depth of a search query.

* Use exception

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-09-09 16:12:41 +02:00
Ali GündoğarandAlexandre Alapetite 18dfada959 Only allow http/https URI schemes for enclosure and thumbnail URLs (#9272)
* Only allow http/https URI schemes for enclosure and thumbnail URLs

Feed enclosure URLs were stored and rendered without URI scheme validation:
SimplePie's scheme check only applies to URLs inside content HTML
(Sanitize::replace_urls()), not to enclosure metadata, so a `javascript:`
URL survived the whole pipeline and was served as a clickable link in the
article view, including through the Google Reader compatible API.

Add FreshRSS_http_Util::isAllowedUrlScheme() and apply it:
- at store time, when building enclosure attributes in FreshRSS_Feed
- at render time, in FreshRSS_Entry::content(), so entries already stored
  in existing databases are protected as well

https://github.com/FreshRSS/FreshRSS/security/advisories/GHSA-3fw6-j8m4-82vj

* Reuse existing SimplePie is_remote_uri() function

* Use more realistic test examples

* Add tests for protocol relative
And simplify tests further

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-09-09 00:14:13 +02:00
Alexandre Alapetite f902047204 Offer to add a prefix when resharing user labels (#9236)
* Offer to add a prefix when resharing user labels
Improve https://github.com/FreshRSS/FreshRSS/pull/8112

* make fix-all
2026-08-28 08:51:57 +02:00
07c939d84f Fix: infinite recursion in sanitizeHTML() when maxLength truncation doesn't converge (#9043)
* Fix: infinite recursion in sanitizeHTML() when maxLength truncation doesn't converge

Sanitizing can grow a truncated HTML fragment (e.g. an unclosed tag gets
auto-closed), so the previous recursive shrink-and-retry could hit a fixed
point and recurse forever, crashing on a stack overflow. Bound the retries
and fall back to a hard truncation that always terminates.

Add unit tests for FreshRSS_SimplePieCustom::sanitizeHTML(), the XSS
sanitization applied to all untrusted feed content, which had no coverage.

* Update app/Models/SimplePieCustom.php

Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>

* Some refactoring
Avoid two mb_strcut in a row. More cases for fallback. Better fallback. More edge cases.

* Address trailing incomplete tag or entity

* Whitespace reduction

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
2026-08-28 08:47:04 +02:00
Luis Carlos SimitanaandAlexandre Alapetite 03e871e5be Add configurable log_level system setting (#9185)
* Add configurable log_level system setting

Minz_Log currently only distinguishes 'production' (errors and
warnings only) from any other environment (everything, including
debug messages), with no granularity in between.

Introduce an optional 'log_level' system setting (error, warning,
notice, info, or debug) that overrides the verbosity implied by
'environment'. It defaults to an empty string, which preserves the
exact current behaviour.

Fixes #7594

* Slight rewrite

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-08-25 16:18:30 +02:00
TowyTowyandClaude Opus 5 95842d81c1 Fix only the last <media:credit> being written to the RSS output (#9174)
The loop building the media credits assigned instead of appending, so an enclosure with several credits only kept the last one.

`app/views/index/rss.phtml` builds the `<media:credit>` elements for an enclosure in a loop, but assigns instead of appends:

```php
$mediaCredits = '';
foreach ($credits as $credit) {
    $mediaCredits = '<media:credit>' . $credit . '</media:credit>';
}
```

So an enclosure with three credits emits only the last one. Every iteration throws away what the previous one built.

This came in with #6272 ("Allow multiple authors on enclosures", which closed #5066). Parsing and display got multi-credit support, but FreshRSS's own generated RSS still passes on a single credit, so a downstream reader subscribing to a FreshRSS feed sees the same symptom #5066 described.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:49:52 +02:00
fa8e8a4400 Fix “mark as read older than…” widening the active search (#9173)
* Fix “mark as read older than…” widening the active search

Sibling searches of a `FreshRSS_BooleanSearch` are combined by OR, so
`prepend()` added the maximum publication date used by the “mark as read →
articles older than one day / one week” action as one more OR term instead of
an extra restriction.

With an active search and the stream sorted by publication date, that action
therefore marked as read every article older than the cut-off (ignoring the
search) plus every article matching the search at any date.

`prepend()` now wraps the existing expression first, reusing the logic that
`enforce()` already used for the same purpose.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix SQL space

* Slight reduction of comments

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-08-04 22:09:12 +02:00
0dc3431168 Preserve negative timezone offsets in date intervals (#9071)
* Preserve negative timezone offsets in date intervals

## Summary

- Preserve the sign of ISO 8601 timezone offsets while normalizing date intervals.
- Add regression coverage for a negative offset.

## Why

The previous normalization removed every hyphen, turning `-05:00` into `0500` and parsing the timestamp in the wrong timezone.

* Pad dates before timezone offsets

* Minor test comment and format

---------

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-23 14:06:27 +02:00
9b40f04f32 Fix tagging an already-tagged entry on PostgreSQL (#9136)
* Complete two source TODOs: negative UTC offset date parsing and PostgreSQL tag insert

_noDelimit() stripped '-' and ':' from the whole ISO 8601 string, including a
trailing UTC offset, so a negative offset such as "-05:00" silently lost its
sign and corrupted the parsed date. Extract a trailing offset (or "Z") before
stripping delimiters and re-append it unchanged.

FreshRSS_TagDAOPGSQL::sqlIgnore() returned an empty string as a placeholder,
so re-tagging an already-tagged entry produced a real SQL error on PostgreSQL
instead of being a silent no-op like on MySQL/SQLite. Replace the sqlIgnore()
keyword-injection approach with the sqlIgnoreConflict(string $sql): string
pattern already used by EntryDAO, and implement it for PostgreSQL with
"ON CONFLICT DO NOTHING".

Fixes #7923.

Co-Authored-By: Claude <noreply@anthropic.com>

* Drop the negative-UTC-offset date fix, already covered by #9071

#9071 already fixes this exact bug in lib_date.php, so keeping this
PR focused on the PostgreSQL tag-insert fix only.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 13:52:09 +02:00
TowyTowyandClaude Fable 5 8c5bd58808 Fix SQL errors breaking regex search on MySQL / MariaDB (#9036)
On MySQL and MariaDB (the compressed content_bin code path,
isCompressed() === true), three of the four regex-search constructions
in FreshRSS_EntryDAO::sqlBooleanSearch() generated invalid SQL, so the
query failed and the search silently returned zero articles:

* intext:/regex/ appended a stray closing parenthesis after
  sqlRegex(...) (MySQL error 1248);
* -intext:/regex/ had the same stray parenthesis;
* -/regex/ emitted ' ANT NOT ' instead of ' AND NOT '
  (MySQL error 1064).

sqlRegex() already returns a self-contained, balanced expression
(REGEXP_LIKE(expr, ?, 'flags') or expr REGEXP ?), so no extra
parenthesis is needed, matching the uncompressed (SQLite / PostgreSQL)
branches.

The existing MySQL / MariaDB unit tests for intext:/regex/ had
captured the buggy unbalanced SQL as their expected strings; they are
corrected, and new cases are added for -intext:/regex/ and -/regex/
on both flavours.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 17:50:16 +02:00
Gerard Alvear PorrasandGerard Alvear c15171efb5 Fix actualize mutex collision across instances (#9045)
## What changed

- derive the actualization mutex name from a SHA-256 hash of the canonical `DATA_PATH`
- keep `TMP_PATH` as the configurable location for the mutex file
- add coverage for deterministic per-instance names and independent locks

## Why

Multiple FreshRSS instances that use the default shared system temporary directory currently use the same actualization lock. The data path identifies the instance without exposing or depending on its configured salt.

## Validation

- `vendor/bin/phpunit --bootstrap ./tests/bootstrap.php ./tests/lib/ActualizeMutexTest.php --no-progress`
- `vendor/bin/phpcs app/actualize_script.php lib/lib_rss.php tests/lib/ActualizeMutexTest.php -s`
- `vendor/bin/phpstan analyse --memory-limit 512M --no-progress app/actualize_script.php lib/lib_rss.php tests/lib/ActualizeMutexTest.php`

Fixes #6370

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
2026-07-18 22:32:38 +02:00
124dec5ef1 Check GMP during 32-bit installation (#9046)
* Check GMP during 32-bit installation

- add an installer prerequisite for GMP when PHP uses 32-bit integers
- stop installation before favourite export can call GMP functions that are unavailable
- cover 32-bit and 64-bit GMP requirement states

Google Reader-compatible entry identifiers require GMP on 32-bit PHP. The installer did not check that prerequisite, so a missing extension could instead cause a fatal error during favourite export.

* i18n: fr

---------

Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-18 21:54:36 +02:00
7744f1fed9 WebSub: ignore http/https scheme difference in Self URL comparison (#9005)
* WebSub: ignore http/https scheme difference in Self URL comparison

The PubSubHubbub endpoint logged 'Self URL does not match registered
canonical URL' whenever a feed's self URL differed from the registered
canonical URL only by http vs https (common with http->https redirects),
spamming the log. Compare the URLs ignoring the http/https scheme while
still requiring host/path/query to match exactly.

Closes #3087

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Stylistic preferences

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-09 07:50:42 +02:00
Jam BalayaandAlexandre Alapetite 271257be42 Fix statistics repartition averages (#8996)
* Fix statistics repartition averages

* Minor code preference

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-07-07 10:42:53 +02:00
Jam Balaya 310bcb5e90 Add read status hook for entries (#8995)
- Add a new `EntriesRead` / `entries_read` hook for extensions.
- Trigger the hook when `markRead()` changes entry read/unread state for explicit entry IDs.
- Cover both the default DAO path and the SQLite/PGSQL DAO path.
- Add Minz hook tests for the new hook signature and argument passing.

## Why

Extensions can already react to favorite/bookmark changes through `EntriesFavorite`. This adds the matching read/unread hook requested in #4051 for extensions that need to collect stats or react to manual read state changes.

Closes #4051
2026-07-07 09:04:32 +02:00
Alexandre Alapetite 542f4920e7 Fix lost elements while parsing search query (#8884)
Fix https://github.com/FreshRSS/FreshRSS/issues/8876
2026-05-24 20:23:58 +02:00
Julien HerrandAlexandre Alapetite 5bddc6bed5 feat(cli): add reconfigure-user.php to read/write per-user config attributes (#8873)
* feat(cli): add reconfigure-user.php to read/write per-user config attributes

Closes #8869.

Adds `cli/reconfigure-user.php`, a first-class CLI for per-user configuration attributes — the user-level equivalent of the existing `reconfigure.php` (system config).

### Usage

```sh
# List all attributes (sensitive keys redacted by default)
./cli/reconfigure-user.php --user alice --list
./cli/reconfigure-user.php --user alice --list --show-secrets

# Read a single attribute (exit 2 if key not found)
./cli/reconfigure-user.php --user alice --key language

# Set an attribute (type inferred from existing value: bool, int, string)
./cli/reconfigure-user.php --user alice --key language --set --value fr

# Set from stdin (recommended for secrets — keeps value out of shell history / ps)
./cli/reconfigure-user.php --user alice --key some_token --set --value-stdin < token.txt

# Create a new key, e.g. for an extension (unknown keys rejected by default)
./cli/reconfigure-user.php --user alice --key my_ext_setting --set --value hello --force

# Delete an attribute (exit 2 if key not found)
./cli/reconfigure-user.php --user alice --key some_token --unset
```

### Changes

- `cli/reconfigure-user.php` — new command
- `lib/Minz/Configuration::toArray()` — exposes the full config array (used by `--list`)
- `cli/README.md` — documents the new command
- `tests/cli/UserConfigOptionsParserTest.php` — PHPUnit tests for the options parser, following the existing `CliOptionsParserTest` pattern (shared `cli-parser-test.php` helper)

### Test plan

- `make test-all` passes
- Tested manually against a local FreshRSS instance: `--list`, `--key` (get), `--set` (bool/int/string inference), `--value-stdin`, `--unset`, `--force`, error paths (unknown key without `--force`, wrong type)

* unserialize allowed_classes

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-05-24 13:25:04 +02:00
polybjornandBjørn A. Andersen 3a7431ce04 fix(i18n): validate language directory names against gen.lang.* keys (#8767)
Detect when an `app/i18n/<lang>/` directory has no matching `gen.lang.<lang>`
key in the reference language (or vice versa), and refuse to regenerate the
README from that invalid state.

This catches a class of silent corruption where the README translation
table renders literal i18n keys instead of localised language names. The
trigger is most often a case-folded directory on macOS APFS - git tracks
`zh-TW`, the local FS reads back `zh-tw`, the script's `_t('gen.lang.zh-tw')`
lookup misses, and the README ends up with `gen.lang.zh-tw (zh-tw)` instead
of `正體中文 (zh-TW)`. The same check also flags orphan directories (no
display-name key) and orphan keys (no directory).

The new validateLanguageNames() method on I18nData performs a bidirectional
set comparison and returns human-readable issues. cli/check.translation.php
prints them to STDERR and gates --generate-readme on the result, leaving
routine completeness validation behaviour unchanged. Adds four PHPUnit
tests covering: clean state, case mismatch, orphan directory, orphan key.

Co-authored-by: Bjørn A. Andersen <polybjorn@users.noreply.github.com>
2026-05-02 23:43:19 +02:00
1acc646222 Show time since when a feed has problems + new timeago() method and i18n plurals (#8670)
Closes https://github.com/FreshRSS/FreshRSS/issues/8508

Changes proposed in this pull request:

- Use an integer for `Feed::error` everywhere (follow up to #8646)
- Extract `Entry::machineReadableDate()` into function for use in HTML templates
- Add `timeago()` function that converts a unix timestamp into a "4 weeks ago" string
- Show the last successful feed update, and the last erroneous update

How to test the feature manually:

1. Update a feed
2. Modify the feed URL in the database and set it to a non-existing URL
3. Update the feed again
4. Open the "Manage feed" and see the expanded error message:

>  Blast! This feed has encountered a problem. If this situation persists, please verify that it is still reachable.
> Last successful update 3 hours ago, last erroneous update 1 hour ago. 

You can hover the relative dates to see the timestamp.

* Make Feed::error an int everywhere

Related: https://github.com/FreshRSS/FreshRSS/pull/8646

* Extract timestamptomachinedate()

.. for later usage in the feed error time display.

* Show time since when a feed has problems

We add our own "timeago" function that converts a unix timestamp
into a "4 weeks ago" string.

Resolves: https://github.com/FreshRSS/FreshRSS/issues/8508

* Add new translation keys

* i18n fr, en-US

* Minor XHTML preference

* Slightly shorter rewrite, also hopefully easier to read

* Rewrite to allow (simple) plural
I also moved some functions around for hopefully a more generic and better structure.
I made some changes for the sake of speed (e.g. second-based logic instead of datetime intervals).
Note: I used automatic translation as I was worried it would be too complicated to explain to translators... I proofread the few languages I have some familiarity with.

* Add reference to CLDR

* Slightly more compact syntax

* Always show last update, fix case of unknown error date

* Remove forgotten span

* No need for multi-lines anymore

* Fix error date thresshold

* plurals forms

* Extract gettext formula conversion script to cli

* Simplify a bit

* Escort excess parentheses to the door

* Simplify

* Avoid being too clever in localization

* Fix German

* Fix plural TODO parsing

* Ignore en-US translation

* make fix-all

* git update-index --chmod=+x cli/compile.plurals.php

* Heredoc indent PHP 7.3+

* compileAll: Continue on error

* PHP strict comparisons

* Light logical simplification

* Cache plural_message_families

* Avoid case of empty value

* A bit of documentation

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Co-authored-by: Frans de Jonge <frans@clevercast.com>
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
2026-04-07 22:56:02 +02:00
Alexandre Alapetite 1b90c40fd6 New automatic feed visibility/priority during search (#8609)
When the search query includes some feed IDs or category IDs, adjust feed visibility/priority filter to include at minimum feed or category visibility.
Fix: https://github.com/FreshRSS/FreshRSS/issues/8602
2026-03-21 17:15:07 +01:00
Alexandre Alapetite b68b80e5de Fix user query parsing (#8543)
* Fix user query parsing
Fix https://github.com/FreshRSS/FreshRSS/issues/8531
We used to take some shortcuts in the code, but now that the logic has complexified, we need to parse those user queries more properly.

* More fixes
2026-03-01 21:45:24 +01:00
Alexandre AlapetiteandInverle cf631b6f87 Implement filter on last modified date by server (#8131)
* 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/2566
https://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>
2026-03-01 21:43:03 +01:00
Alexandre Alapetite d64a6c2751 Trim SQL whitespace before parenthesis (#8522) 2026-02-22 23:31:04 +01:00
Alexandre Alapetite 17be48fe1c Fix search expansion with backslash (#8497)
fix https://github.com/FreshRSS/FreshRSS/issues/8495
2026-02-16 23:14:04 +01:00
Alexandre Alapetite dae27ebd5d Fix wrong search toString in case of regex-looking string (#8479)
`author:'/u/Alice'` was missing its quotes during serialisation, transforming it to a regex.
2026-02-01 13:06:18 +01:00
Alexandre Alapetite 7573fee4f0 Fix MySQL transliterator_transliterate fallback (#8427)
The string syntax of `strtr()` cannot handle mutibytes characters, so need to be rewritten using an array map approach.
Extend the fallback replacements to include the Windows/ISO charsets of the latin languages for which we have a translation
2026-01-10 23:38:00 +01:00
Bartłomiej DmitrukandAlexandre Alapetite 26c1102567 Merge commit from fork
* Fix Path Traversal vulnerability in UserDAO methods

* Add tests and changelog for UserDAO path traversal fix

* make fix-all

* Fix PHPStan

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
2026-01-03 18:09:44 +01:00
Alexandre Alapetite 2527033057 Fix unwanted expansion of user queries in some cases (#8395)
fix https://github.com/FreshRSS/FreshRSS/issues/8378
2026-01-03 16:52:33 +01:00
Alexandre Alapetite 00c61cf34c Fix serialisation of dates in searches (#8341)
Dates should not be expanded/resolved in string serialisations of search expressions, as it otherwise break relative user queries such as `P30D`.
Fix of https://github.com/FreshRSS/FreshRSS/pull/8293
2025-12-24 01:19:06 +01:00
Alexandre Alapetite 6d57a9de47 Fix parsing of literal "or" in regex (#8338)
fix https://github.com/FreshRSS/FreshRSS/issues/7879
2025-12-23 11:58:51 +01:00
Alexandre Alapetite 00cd5df294 Use native PHP #[Deprecated] (#8325)
https://php.watch/versions/8.4/Deprecated
And enfore it with PHPUnit + PHPStan.
Especially useful for extensions.
2025-12-17 10:11:18 +01:00
Alexandre Alapetite 4bd5035914 Rework encoding of search filters (#8324)
Rework:
* https://github.com/FreshRSS/FreshRSS/pull/8222

now that we have:
* https://github.com/FreshRSS/FreshRSS/pull/8293

Follow-up of:
* https://github.com/FreshRSS/FreshRSS/pull/8311

* More simplification

* Deprecate getRawInput
2025-12-17 10:07:52 +01:00