* 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>
* Log and block invalid challenge formats during login
Fixes some unexpected behaviors during a user login, including strange errors in the log related to JSON.
* Ignore JSON decode errors for URL unserializing
* Document the format a bit
* SimplePie: Harden cache deserialization calls
To reduce impact if a maliciously provided cache file is somehow loaded, e.g. from a manipulated backup or some kind of external arbitrary file write vulnerability.
* Sync our SimplePie
https://github.com/FreshRSS/simplepie/pull/90
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* 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>
* Dev updates: composer, hadolint, typos...
Composer compatibility PHP 8.6.
Manual updates not supported by Dependabot
* Fix hadolint
* Update lib/Minz/Url.php
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
* Add sha256sum for composer
---------
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
* 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>
* 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>
* Show only one representation per media:group
A <media:group> gathers <media:content> elements that are effectively
the same content, yet different representations (e.g. several
resolutions of the same video), but every representation was stored and
rendered as its own enclosure. For instance, articles from PeerTube
feeds showed the same video five times.
When loading entries, keep only one <media:content> per <media:group>:
the one marked with isDefault="true", or the first one otherwise.
https://www.rssboard.org/media-rss#media-groupFixes#8569
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update SimplePie to include FreshRSS/simplepie#86
Regenerate the vendored SimplePie from FreshRSS/simplepie@freshrss via
`composer update --no-autoloader`, replacing the hand-patched Item.php
with the merged upstream version.
Includes <https://github.com/FreshRSS/simplepie/pull/86>.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Remove redundant test
Already covered by https://github.com/FreshRSS/simplepie/pull/86
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
## 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>
* 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>
* Warn during install if document root does not point to ./p
If the Web server's document root is misconfigured to serve the
FreshRSS repository root instead of the ./p public folder, other
folders such as ./data (which store credentials, tokens, and the
database) can become publicly accessible.
Add a non-blocking check to installation step 1 that compares the
detected DOCUMENT_ROOT against the ./p folder and warns when they
do not match. The check is skipped when DOCUMENT_ROOT is unknown
(e.g. CLI/cron context), including when it is an empty string,
since realpath('') would otherwise misleadingly resolve to the
current working directory.
Fixes#3679
* i18n: fr, en-US
* Path semantics preference
* Minor syntax preference
* Warn when document root is unavailable
---------
Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
PHPMailer enables SMTPAutoTLS by default, meaning it opportunistically
upgrades a plain SMTP connection to STARTTLS whenever the server
advertises support for it, regardless of the 'secure' => '' setting
documented in config.default.php. Minz_Mailer never read or set
$mail->SMTPAutoTLS, so this behaviour could not be turned off, making
it impossible to talk to SMTP servers with a self-signed or otherwise
untrusted certificate (e.g. a mail server on an internal Docker
network) without importing a custom CA.
Add a new 'auto_tls' key to the 'smtp' config array, defaulting to
true to preserve current behaviour, and wire it through to
$mail->SMTPAutoTLS in Minz_Mailer. Document the new option.
Fixes#2997
Co-authored-by: Gerard Alvear <gerard.alvear@logiqd.me>
Saving custom CSS/JS rendered the response from the pre-save state (no
Post/Redirect/Get), so the stylesheet cache-busting URL was stale and
the change only appeared after a manual reload, which reads as erratic
behavior. Redirect back to the extension config after save, matching
the pattern used elsewhere in the codebase.
Fixes#8795
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixes `Uncaught SyntaxError: redeclaration of const freshrssSliderLoadEvent` error shown in console on e.g. `/i/?c=subscription&id=1` due to https://github.com/FreshRSS/FreshRSS/pull/8973
- 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
"Follow-up" of https://github.com/FreshRSS/FreshRSS/pull/8612 (not fixing a regression, just a bug)
Some features like URL observers and custom favicon JS (e.g. *Reset to default* button) wouldn't work, due to `extra.js` not being loaded properly on pages where the slider content was already included in the HTML.
* Add SSRF mitigations using `filter_var` and `CURLOPT_RESOLVE`
The idea is to prevent FreshRSS from sending any HTTP requests to internal services, except for the ones that are explicitly allowed in the config.
Based on https://github.com/moodle/moodle/blob/6e82b46a480826d1a85394d9e5087f7d82d1dd52/lib/filelib.php#L3818 and https://github.com/symfony/symfony/blob/8.1/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.phphttps://github.com/FreshRSS/simplepie/pull/76https://github.com/FreshRSS/simplepie/pull/78
* Add allowlist setting in Web UI
* make readme
* Update app/i18n/fr/admin.php
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* make readme again
* make readme
* Further work
Still WIP and needs testing etc.
* Readd previous if check for domain combination allowlist
* Turn POST to GET after redirect
* Improve
* Update config.default.php
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* make readme
* Skip SSRF check if `CURLOPT_PROXY` is set
* make readme
* Fix `!empty()` mistake
* Respect max redirects feed option when fetching with `httpGet()`
* Respect max redirects during SimplePie fetching + fix bypass
bypass fix: `CURLOPT_FOLLOWLOCATION` was moved below so that emulated redirects are enforced.
* Avoid FreshRSS and Minz code in SimplePie
https://github.com/FreshRSS/FreshRSS/pull/8400#discussion_r2935375980
* Corrected hook code
* phpdoc wrong return type
* Add CIDR support in allowlist
* Implement simple DNS caching
* Suppress `dns_get_record()` warnings
* A bit of proof-reading
* Minor typo
* Fix proxy logic
* Fix HTTP POST redirect logic
* Proofread checkCIDR
Add fixes for several situations
* Remove credentials from URL in logs
* Ensure `CURLOPT_FOLLOWLOCATION` is `false` by setting it at the end
* Fix codesniffer long line
* Fix potential bypass due to wrong return value
If there were no records returned by `dns_get_record()`, no overrides to `CURLOPT_RESOLVE` would get passed,
and a potential bypass could occur, when cURL would try to resolve the domain by itself.
* Put the URL at the end in logs
* Add documentation and environment variable support
* make readme
* Fix wrong behavior in case of IP
* Fix duplicate selector in CSS
* Minor type check change
* i18n fr, en
* Minor type check change
* Fix whitespace i18n fr
* make fix-all
* Fix `$ips_ok` not being returned after domain records were cached
* make readme
* PHPStan fix
* make readme
* Minor syntax in SimplePie
* Only return `null` if no allowed IPs were found
* Add wildcard *, help message
* Consistent docs with help message
* i18n: pl
* SimplePie compatibility PHP 7.2
* make fix-all
* Sync SimplePie
* https://github.com/FreshRSS/simplepie/pull/76
* 💥 Breaking change in the Changelog
* Document `INTERNAL_HOST_ALLOWLIST` in Docker docs
* Remove `Cookie` and `Authorization` headers in `httpGet()` during cross-origin redirect
* Minor whitespace
And same comment convention than below
* Remove authentication headers and change POST to GET on redirect in SimplePie
* Remove .local in Docker example
* Fill in default ports when comparing URL origins
* Remove .local from other places than the Docker example
* Rewrite WebSub subscribe to use `httpGet()`
* make fix-all
* Also unset `CURLOPT_USERPWD` during redirects
* phpcs fix
* Always unset `CURLOPT_FOLLOWLOCATION`
* Bump SimplePie
https://github.com/FreshRSS/simplepie/pull/78
* Update logic for CURLOPT_FOLLOWLOCATION
* Fix PHPStan
* Changelog fix security section
* Update most common RSS Bridge case
https://hub.docker.com/r/rssbridge/rss-bridge
* Replace misleading 127.0.0.1:8080 example for Docker
This does not make sense for a Docker container
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* 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>
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>
* Add FeedsListBeforeActualize hook.
Closes#8650
Implement new hook to allow extensions to modify the list of feeds to actualize.
How to test the feature manually:
1. Add several feeds from a single site (e.g. reddit and YT).
2. Add feeds to other sites.
3. Add and enable the extension https://github.com/pe1uca/xExtension-Declumping
4. Call `php app/actualize_script.php` to update feeds with a different order.
A log like this one is needed to properly see the behavior.
```php
foreach ($feeds as $key => $value) {
syslog(LOG_INFO, "$key: {$value->name()} ({$value->url()})");
}
```
Sort in alphabetical order.
* Implement call to hook
* Remove duplicate return
* Fix PHPStan error
* Update documentation
* Sanitize hook response
* Markdown cleaning
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* 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>
* 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