Explorar el Código

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
Alexandre Alapetite hace 1 día
padre
commit
fd3f75b3a2

+ 1 - 1
.github/ISSUE_TEMPLATE/bug_report_form.yaml

@@ -44,7 +44,7 @@ body:
     attributes:
       label: System information
       description: Please complete the following information
-      value: "- Database version: [e.g. SQLite, PostgreSQL 17]\n- PHP version: [e.g. PHP 8.2]\n- Installation type: [e.g. Docker, Docker image source, git, Yunohost]\n- Web server type: [e.g. Apache, nginx]\n- Device: [e.g. iPhone13]\n- OS: [e.g. Ubuntu 24.04, Win11, MacOS14]\n- Browser: [e.g. Firefox 134]"
+      value: "- Database version: [e.g. PostgreSQL 18, MySQL 8.4, SQLite]\n- PHP version: [e.g. PHP 8.2]\n- Installation type: [e.g. Docker, Docker image source, git, YunoHost]\n- Web server type: [e.g. Apache, nginx]\n- Device: [e.g. iPhone13]\n- OS: [e.g. Ubuntu 24.04, Win11, macOS 27]\n- Browser: [e.g. Firefox 157]"
     validations:
       required: true
   - type: textarea

+ 1 - 1
.github/ISSUE_TEMPLATE/feed-report-form.yml

@@ -49,7 +49,7 @@ body:
     attributes:
       label: System information
       description: Please complete the following information
-      value: "Database version: [e.g. Mysql 5.7, SQLite]\n- PHP version: [e.g. PHP 8.1]\n- Installation type: [e.g. Docker, Docker image source, git, Yunohost]\n- Web server type: [e.g. Apache, nginx]\n- Device: [e.g. iPhone13]\n- OS: [e.g. Ubuntu 22.04, Win10, MacOS14]\n- Browser: [e.g. Firefox 124]"
+      value: "- Database version: [e.g. PostgreSQL 18, MySQL 8.4, SQLite]\n- PHP version: [e.g. PHP 8.2]\n- Installation type: [e.g. Docker, Docker image source, git, YunoHost]\n- Web server type: [e.g. Apache, nginx]\n- Device: [e.g. iPhone13]\n- OS: [e.g. Ubuntu 24.04, Win11, macOS 27]\n- Browser: [e.g. Firefox 157]"
     validations:
       required: true
   - type: textarea

+ 6 - 1
CHANGELOG.md

@@ -4,14 +4,19 @@ See also [the FreshRSS releases](https://github.com/FreshRSS/FreshRSS/releases).
 
 ## 2026-XX-XX FreshRSS 1.30.5-dev
 
+* Compatibility
+	* 💥 Require PHP 8.2+ (drop PHP 8.1) [#9470](https://github.com/FreshRSS/FreshRSS/pull/9470)
 * Bug fixes
 	* Fix 32-bit systems provide a string for SQL BIGINT [#9486](https://github.com/FreshRSS/FreshRSS/pull/9486)
 * Security
 	* Fix escape labels during JSON import and render tag dropdown as text [#9504](https://github.com/FreshRSS/FreshRSS/pull/9504)
+* Deployment
+	* Docker: dev image `freshrss/freshrss:oldest` updated to Alpine 3.18 with PHP 8.2.16 and Apache 2.4.62 [#9470](https://github.com/FreshRSS/FreshRSS/pull/9470)
 * I18n
 	* Add Arabic [#9502](https://github.com/FreshRSS/FreshRSS/pull/9502)
 * CI/CD
 	* Add a link checker workflow using lychee [#9209](https://github.com/FreshRSS/FreshRSS/pull/9209), [#9495](https://github.com/FreshRSS/FreshRSS/pull/9495), [#9507](https://github.com/FreshRSS/FreshRSS/pull/9507)
+	* Update to PHPUnit 11.x (PHP 8.2+) [#9470](https://github.com/FreshRSS/FreshRSS/pull/9470)
 	* Group auto dependency updates into one PR per ecosystem [#9496](https://github.com/FreshRSS/FreshRSS/pull/9496)
 	* Update dev dependencies [#9494](https://github.com/FreshRSS/FreshRSS/pull/9494), [#9497](https://github.com/FreshRSS/FreshRSS/pull/9497), [#9498](https://github.com/FreshRSS/FreshRSS/pull/9498),
 		[#9499](https://github.com/FreshRSS/FreshRSS/pull/9499),[#9500](https://github.com/FreshRSS/FreshRSS/pull/9500),[#9501](https://github.com/FreshRSS/FreshRSS/pull/9501),
@@ -79,7 +84,7 @@ See also [the FreshRSS releases](https://github.com/FreshRSS/FreshRSS/releases).
 	* Accept a trusted proxy address given without a subnet [#9301](https://github.com/FreshRSS/FreshRSS/pull/9301)
 	* Warn during session regenerate fail [#9311](https://github.com/FreshRSS/FreshRSS/pull/9311), [#9376](https://github.com/FreshRSS/FreshRSS/pull/9376)
 	* CI/CD add zizmor workflow for action security checks [#9228](https://github.com/FreshRSS/FreshRSS/pull/9228), [#9331](https://github.com/FreshRSS/FreshRSS/pull/9331)
-	* Use PHP `#[\SensitiveParameter]` [#9322](https://github.com/FreshRSS/FreshRSS/pull/9322)
+	* Use PHP 8.2+ `#[\SensitiveParameter]` [#9322](https://github.com/FreshRSS/FreshRSS/pull/9322)
 	* Reject token access (RSS/OPML export, feed refresh) for disabled accounts [#9336](https://github.com/FreshRSS/FreshRSS/pull/9336)
 	* Require POST+CSRF for self-update mutations [#9335](https://github.com/FreshRSS/FreshRSS/pull/9335)
 	* Make the login challenge nonce one-time [#9334](https://github.com/FreshRSS/FreshRSS/pull/9334)

+ 7 - 7
Docker/Dockerfile-Oldest

@@ -1,15 +1,15 @@
 # Test image for development
-FROM alpine:3.16
+FROM alpine:3.18
 
 ENV TZ=UTC
 SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
 
 RUN apk add --no-cache \
 	tzdata \
-	apache2 php81-apache2 \
-	php81 php81-curl php81-gmp php81-intl php81-mbstring php81-xml php81-zip \
-	php81-ctype php81-dom php81-fileinfo php81-iconv php81-json php81-opcache php81-openssl php81-phar php81-session php81-simplexml php81-xmlreader php81-xmlwriter php81-xml php81-tokenizer php81-zlib \
-	php81-pdo_sqlite php81-pdo_mysql php81-pdo_pgsql
+	apache2 php82-apache2 \
+	php82 php82-curl php82-gmp php82-intl php82-mbstring php82-xml php82-zip \
+	php82-ctype php82-dom php82-fileinfo php82-iconv php82-json php82-opcache php82-openssl php82-phar php82-session php82-simplexml php82-xmlreader php82-xmlwriter php82-xml php82-tokenizer php82-zlib \
+	php82-pdo_sqlite php82-pdo_mysql php82-pdo_pgsql
 
 RUN mkdir -p /var/www/FreshRSS /run/apache2/
 WORKDIR /var/www/FreshRSS
@@ -43,8 +43,8 @@ RUN \
 	sed -r -i "/^\s*#\s*LoadModule .*mod_(deflate|expires|filter|headers|mime|remoteip|setenvif).so$/s/^\s*#//" \
 		/etc/apache2/httpd.conf && \
 	# PHP configuration
-	if [ ! -f /usr/bin/php ]; then ln -s /usr/bin/php81 /usr/bin/php; else true; fi && \
-	echo 'memory_limit = 256M' > /etc/php81/conf.d/10_memory.ini && \
+	if [ ! -f /usr/bin/php ]; then ln -s /usr/bin/php82 /usr/bin/php; else true; fi && \
+	echo 'memory_limit = 256M' > /etc/php82/conf.d/10_memory.ini && \
 	# Disable built-in FreshRSS updates when using Docker, as the full image is supposed to be updated instead.
 	sed -r -i "\\#disable_update#s#^.*#\t'disable_update' => true,#" ./config.default.php && \
 	# Configure cron job

+ 1 - 1
README.fr.md

@@ -63,7 +63,7 @@ FreshRSS n’est fourni avec aucune garantie.
 * Serveur modeste, par exemple sous Linux ou Windows
 	* Fonctionne même sur un Raspberry Pi 1 avec des temps de réponse < 1s (testé sur 150 flux, 22k articles)
 * Serveur Web Apache2.4+ (recommandé), ou nginx, lighttpd (non testé sur les autres)
-* PHP 8.1+
+* PHP 8.2+
 	* Extensions requises : [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/features.sessions), [ctype](https://www.php.net/ctype)
 	* Extensions recommandées : [PDO_SQLite](https://www.php.net/pdo-sqlite) (pour l’export/import), [GMP](https://www.php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](https://www.php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](https://www.php.net/mbstring) (pour le texte Unicode), [iconv](https://www.php.net/iconv) (pour conversion d’encodages), [ZIP](https://www.php.net/zip) (pour import/export), [zlib](https://www.php.net/zlib) (pour les flux compressés)
 	* Extension pour base de données : [PDO_PGSQL](https://www.php.net/pdo-pgsql) ou [PDO_SQLite](https://www.php.net/pdo-sqlite) ou [PDO_MySQL](https://www.php.net/pdo-mysql)

+ 1 - 1
README.md

@@ -63,7 +63,7 @@ FreshRSS comes with absolutely no warranty.
 * Light server running Linux or Windows
 	* It even works on Raspberry Pi 1 with response time under a second (tested with 150 feeds, 22k articles)
 * A Web server: Apache2.4+ (recommended), nginx, lighttpd (not tested on others)
-* PHP 8.1+
+* PHP 8.2+
 	* Required extensions: [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/features.sessions), [ctype](https://www.php.net/ctype)
 	* Recommended extensions: [PDO_SQLite](https://www.php.net/pdo-sqlite) (for export/import), [GMP](https://www.php.net/gmp) (for API access on 32-bit platforms), [IDN](https://www.php.net/intl.idn) (for Internationalized Domain Names), [mbstring](https://www.php.net/mbstring) (for Unicode strings), [iconv](https://www.php.net/iconv) (for charset conversion), [ZIP](https://www.php.net/zip) (for import/export), [zlib](https://www.php.net/zlib) (for compressed feeds)
 	* Extension for database: [PDO_PGSQL](https://www.php.net/pdo-pgsql) or [PDO_SQLite](https://www.php.net/pdo-sqlite) or [PDO_MySQL](https://www.php.net/pdo-mysql)

+ 3 - 11
app/Controllers/importExportController.php

@@ -80,19 +80,11 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 		$this->listSqliteArchives();
 	}
 
-	private static function megabytes(string $size_str): float|int|string {
-		return match (substr($size_str, -1)) {
-			'M', 'm' => (int)$size_str,
-			'K', 'k' => (int)$size_str / 1024,
-			'G', 'g' => (int)$size_str * 1024,
-			default => $size_str,
-		};
-	}
-
 	private static function minimumMemory(int|string $mb): void {
 		$mb = (int)$mb;
-		$ini = self::megabytes(ini_get('memory_limit') ?: '0');
-		if ($ini < $mb) {
+		// `ini_get('memory_limit')` returns e.g. `128M`, or `-1` for unlimited
+		$ini = (int)(ini_parse_quantity(ini_get('memory_limit') ?: '0') / 1048576);
+		if ($ini >= 0 && $ini < $mb) {
 			ini_set('memory_limit', $mb . 'M');
 		}
 	}

+ 4 - 4
app/Models/SimplePieFetch.php

@@ -25,10 +25,10 @@ final class FreshRSS_SimplePieFetch extends \SimplePie\File
 			}
 		}
 
-		$redirects = $curl_options[CURLOPT_MAXREDIRS] ?? $redirects;
-		if (!is_int($redirects)) {
-			$redirects = 4;
-		} elseif ($redirects < 0) {
+		if (is_int($curl_options[CURLOPT_MAXREDIRS] ?? null)) {
+			$redirects = $curl_options[CURLOPT_MAXREDIRS];
+		}
+		if ($redirects < 0) {
 			$redirects = -1;	// infinite redirects
 		}
 		if (isset($curl_options[CURLOPT_FOLLOWLOCATION])) {

+ 1 - 1
app/Models/ViewMode.php

@@ -4,7 +4,7 @@ declare(strict_types=1);
 /**
  * Represents a view mode option for the reading configuration
  */
-final class FreshRSS_ViewMode {
+final readonly class FreshRSS_ViewMode {
 	private string $id;
 	private string $name;
 	private string $controller;

+ 7 - 34
app/Utils/httpUtil.php

@@ -4,23 +4,8 @@ declare(strict_types=1);
 final class FreshRSS_http_Util {
 
 	private const RETRY_AFTER_PATH = DATA_PATH . '/Retry-After/';
-	private const PRIVATE_SUBNETS = [
-		'127.0.0.0/8',    // RFC1700 (Loopback)
-		'10.0.0.0/8',     // RFC1918
-		'192.168.0.0/16', // RFC1918
-		'172.16.0.0/12',  // RFC1918
-		'169.254.0.0/16', // RFC3927
-		'0.0.0.0/8',      // RFC5735
-		'240.0.0.0/4',    // RFC1112
-		'100.64.0.0/10',  // RFC6598 (Shared Address Space of Carrier-Grade NAT)
-		'::1/128',        // Loopback
-		'fc00::/7',       // Unique Local Address
-		'fe80::/10',      // Link Local Address
-		'::ffff:0:0/96',  // IPv4 translations
-		'64:ff9b::/96',   // RFC6052 (IPv6 Addressing of IPv4/IPv6 Translators, NAT64)
-		'64:ff9b:1::/48', // RFC8215 (Local-Use IPv4/IPv6 Translation Prefix)
-		'::/128',         // Unspecified address
-	];
+	private const NAT64_PREFIX_RFC6052 = '64:ff9b::/96';
+	private const NAT64_PREFIX_RFC8215 = '64:ff9b:1::/48';
 	/** @var array<string, string[]> $resolve_ok */
 	private static array $resolve_ok = [];
 	/** @var array<string, bool> $retry_after_domain_wide */
@@ -398,7 +383,7 @@ final class FreshRSS_http_Util {
 			// NAT64 addresses use the RFC 6052 well-known prefix.
 			// Validate the embedded IPv4 address because the NAT64 IPv6 address itself
 			// is globally routable even when it maps to a private/reserved IPv4 address.
-			if (self::checkCIDR($ip, '64:ff9b::/96')) {
+			if (self::checkCIDR($ip, self::NAT64_PREFIX_RFC6052)) {
 				$packed = @inet_pton($ip);
 				if ($packed === false || strlen($packed) !== 16) {
 					continue;
@@ -409,31 +394,19 @@ final class FreshRSS_http_Util {
 					continue;
 				}
 
-				if (filter_var($embedded_ipv4, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
+				if (filter_var($embedded_ipv4, FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE) === false) {
 					continue;
 				}
 
-				// Extra check because the above one might not be enough: https://github.com/php/php-src/issues/16944
-				foreach (self::PRIVATE_SUBNETS as $cidr) {
-					if (self::checkCIDR($embedded_ipv4, $cidr)) {
-						continue 2;
-					}
-				}
-
 				$ips_ok[] = $add_ip;
 				continue;
 			}
 
-			if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
+			// `FILTER_FLAG_GLOBAL_RANGE` does not reject the NAT64 prefixes
+			if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE) === false ||
+				self::checkCIDR($ip, self::NAT64_PREFIX_RFC8215)) {
 				continue;
 			}
-			// Extra check because the above one might not be enough: https://github.com/php/php-src/issues/16944
-			// Workaround is available by using `FILTER_FLAG_GLOBAL_RANGE` instead, but that was only added in PHP 8.2, and we need to support PHP 8.1+
-			foreach (self::PRIVATE_SUBNETS as $cidr) {
-				if (self::checkCIDR($ip, $cidr)) {
-					continue 2;
-				}
-			}
 
 			$ips_ok[] = $add_ip;
 		}

+ 4 - 4
composer.json

@@ -17,7 +17,7 @@
 		"WebSub"
 	],
 	"require": {
-		"php": ">=8.1",
+		"php": ">=8.2",
 		"ext-ctype": "*",
 		"ext-curl": "*",
 		"ext-dom": "*",
@@ -49,11 +49,11 @@
 			"phpstan/extension-installer": false
 		},
 		"platform": {
-			"php": "8.1"
+			"php": "8.2"
 		}
 	},
 	"require-dev": {
-		"php": ">=8.1",
+		"php": ">=8.2",
 		"ext-phar": "*",
 		"ext-tokenizer": "*",
 		"ext-xmlwriter": "*",
@@ -61,7 +61,7 @@
 		"phpstan/phpstan-deprecation-rules": "^2.0",
 		"phpstan/phpstan-phpunit": "^2.0",
 		"phpstan/phpstan-strict-rules": "^2.0",
-		"phpunit/phpunit": "^10",
+		"phpunit/phpunit": "^11",
 		"squizlabs/php_codesniffer": "^4.0"
 	},
 	"scripts": {

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 240 - 195
composer.lock


+ 1 - 1
constants.php

@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 //<Not customisable>
 /** @var string */
-const FRESHRSS_MIN_PHP_VERSION = '8.1.0';
+const FRESHRSS_MIN_PHP_VERSION = '8.2.0';
 /** @var string */
 const FRESHRSS_VERSION = '1.30.5-dev';
 /** @var string */

+ 1 - 1
docs/en/admins/02_Prerequisites.md

@@ -7,7 +7,7 @@ You need to verify that your server can run FreshRSS before installing it. If yo
 | Software      | Recommended             | Also Works With         |
 | ------------- | ----------------------- | ----------------------- |
 | Web server    | **Apache 2.4**          | nginx, lighttpd |
-| PHP           | **PHP 8.1+**            | FreshRSS 1.24.3: PHP 7.4+<br />FreshRSS 1.22.1: PHP 7.2+ |
+| PHP           | **PHP 8.2+**            | FreshRSS 1.30.4: PHP 8.1+ |
 | PHP modules   | Required: libxml, cURL, JSON, PDO_MySQL, PCRE and ctype.<br />Required (32-bit only): GMP <br />Recommended: Zlib, mbstring, iconv, ZipArchive<br />*For the whole modules list see [Dockerfile](https://github.com/FreshRSS/FreshRSS/blob/edge/Docker/Dockerfile-Alpine#L9-L11)* | |
 | Database      | **PostgreSQL 10+**      | SQLite, MariaDB 10.6+, MySQL 8.0+ |
 | Browser       | **Firefox**             | Chrome, Opera, Safari, or Edge       |

+ 1 - 1
docs/en/admins/10_ServerConfig.md

@@ -95,7 +95,7 @@ server {
 	# php files handling
 	# this regex is mandatory because of the API
 	location ~ ^.+?\.php(/.*)?$ {
-		fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
+		fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
 		fastcgi_split_path_info ^(.+\.php)(/.*)$;
 		# By default, the variable PATH_INFO is not set under PHP-FPM
 		# But FreshRSS APIs greader.php and misc.php need it. If you have a “Bad Request” error, double check this var!

+ 2 - 2
docs/fr/users/01_Installation.md

@@ -7,7 +7,7 @@ Il est toutefois de votre responsabilité de vérifier que votre hébergement pe
 | Logiciel         | Recommandé         | Fonctionne aussi avec          |
 | --------         | -----------        | ---------------------          |
 | Serveur web      | **Apache 2.4+**    | nginx, lighttpd |
-| PHP              | **PHP 8.1+**       | FreshRSS 1.24.3 : PHP 7.4+<br />FreshRSS 1.22.1 : PHP 7.2+ |
+| PHP              | **PHP 8.2+**       | FreshRSS 1.30.4 : PHP 8.1+ |
 | Modules PHP      | Requis : libxml, cURL, JSON, PDO_MySQL, PCRE et ctype<br />Requis (32 bits seulement) : GMP<br />Recommandé : Zlib, mbstring et iconv, ZipArchive<br />*Pour une liste complète des modules nécessaires voir le [Dockerfile](https://github.com/FreshRSS/FreshRSS/blob/edge/Docker/Dockerfile-Alpine#L9-L11)* |                                |
 | Base de données  | **PostgreSQL 10+** | SQLite, MariaDB 10.6+, MySQL 8.0+ |
 | Navigateur       | **Firefox**        | Chrome, Opera, Safari, or Edge   |
@@ -118,7 +118,7 @@ server {
 	# gestion des fichiers php
 	# il est nécessaire d’utiliser cette expression régulière pour le bon fonctionnement de l’API
 	location ~ ^.+?\.php(/.*)?$ {
-		fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
+		fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
 		fastcgi_split_path_info ^(.+\.php)(/.*)$;
 		# Par défaut la variable PATH_INFO n’est pas définie sous PHP-FPM
 		# mais les APIs FreshRSS greader.php et misc.php en ont besoin. Si vous avez un “Bad Request”, vérifiez bien cette dernière !

+ 1 - 1
lib/Minz/Hook.php

@@ -1,7 +1,7 @@
 <?php
 declare(strict_types=1);
 
-class Minz_Hook {
+readonly class Minz_Hook {
 	public const DEFAULT_PRIORITY = 0;
 
 	public function __construct(

+ 1 - 1
phpstan.dist.neon

@@ -1,6 +1,6 @@
 parameters:
 	phpVersion:
-		min: 80100	# PHP 8.1
+		min: 80200	# PHP 8.2
 		max: 80599	# PHP 8.5
 	level: 10	# https://phpstan.org/user-guide/rule-levels
 	fileExtensions:

+ 5 - 2
tests/app/Models/BooleanSearchTest.php

@@ -5,8 +5,8 @@ use PHPUnit\Framework\Attributes\DataProvider;
 
 final class BooleanSearchTest extends \PHPUnit\Framework\TestCase {
 
-	public function __construct(string $name) {
-		parent::__construct($name);
+	#[\Override]
+	protected function setUp(): void {
 		if (!FreshRSS_Context::hasSystemConf()) {
 			FreshRSS_Context::initSystem();
 		}
@@ -48,6 +48,9 @@ final class BooleanSearchTest extends \PHPUnit\Framework\TestCase {
 
 	/** @return list<list{string}> */
 	public static function provideTooLongOrTooDeepSearches(): array {
+		if (!FreshRSS_Context::hasSystemConf()) {
+			FreshRSS_Context::initSystem();
+		}
 		$tooLong = str_repeat('a', FreshRSS_Context::systemConf()->limits['max_search_length'] + 1);
 		$tooDeep = str_repeat('(', FreshRSS_Context::systemConf()->limits['max_search_parentheses_depth'] + 1) . 'ab' .
 			str_repeat(')', FreshRSS_Context::systemConf()->limits['max_search_parentheses_depth'] + 1);

+ 2 - 2
tests/app/Models/EntryTest.php

@@ -3,8 +3,8 @@ declare(strict_types=1);
 
 final class EntryTest extends \PHPUnit\Framework\TestCase {
 
-	public function __construct(string $name) {
-		parent::__construct($name);
+	#[\Override]
+	protected function setUp(): void {
 		if (!FreshRSS_Context::hasSystemConf()) {
 			FreshRSS_Context::initSystem();
 		}

+ 10 - 18
tests/app/Models/SearchTest.php

@@ -7,8 +7,8 @@ require_once LIB_PATH . '/lib_date.php';
 
 final class SearchTest extends \PHPUnit\Framework\TestCase {
 
-	public function __construct(string $name) {
-		parent::__construct($name);
+	#[\Override]
+	protected function setUp(): void {
 		if (!FreshRSS_Context::hasSystemConf()) {
 			FreshRSS_Context::initSystem();
 		}
@@ -866,10 +866,8 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 		];
 	}
 
-	/**
-	 * @dataProvider provideRegexPostreSQL
-	 * @param array<string> $values
-	 */
+	/** @param array<string> $values */
+	#[DataProvider('provideRegexPostreSQL')]
 	public function test__regex_postgresql(string $input, string $sql, array $values): void {
 		[$filterValues, $filterSearch] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
 		self::assertSame(trim($sql), trim($filterSearch));
@@ -977,10 +975,8 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 		];
 	}
 
-	/**
-	 * @dataProvider provideRegexMariaDB
-	 * @param array<string> $values
-	 */
+	/** @param array<string> $values */
+	#[DataProvider('provideRegexMariaDB')]
 	public function test__regex_mariadb(string $input, string $sql, array $values): void {
 		FreshRSS_DatabaseDAO::$dummyConnection = true;
 		FreshRSS_DatabaseDAO::setStaticVersion('11.4.3-MariaDB-ubu2404');
@@ -1030,10 +1026,8 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 		];
 	}
 
-	/**
-	 * @dataProvider provideRegexMySQL
-	 * @param array<string> $values
-	 */
+	/** @param array<string> $values */
+	#[DataProvider('provideRegexMySQL')]
 	public function test__regex_mysql(string $input, string $sql, array $values): void {
 		FreshRSS_DatabaseDAO::$dummyConnection = true;
 		FreshRSS_DatabaseDAO::setStaticVersion('9.0.1');
@@ -1078,10 +1072,8 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 		];
 	}
 
-	/**
-	 * @dataProvider provideRegexSQLite
-	 * @param array<string> $values
-	 */
+	/** @param array<string> $values */
+	#[DataProvider('provideRegexSQLite')]
 	public function test__regex_sqlite(string $input, string $sql, array $values): void {
 		[$filterValues, $filterSearch] = FreshRSS_EntryDAOSQLite::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
 		self::assertSame(trim($sql), trim($filterSearch));

+ 2 - 2
tests/app/Models/SimplePieCustomTest.php

@@ -9,8 +9,8 @@ use PHPUnit\Framework\Attributes\DataProvider;
  */
 final class SimplePieCustomTest extends \PHPUnit\Framework\TestCase {
 
-	public function __construct(string $name) {
-		parent::__construct($name);
+	#[\Override]
+	protected function setUp(): void {
 		if (!FreshRSS_Context::hasSystemConf()) {
 			FreshRSS_Context::initSystem();
 		}

+ 2 - 2
tests/app/Models/UserQueryTest.php

@@ -8,8 +8,8 @@ use PHPUnit\Framework\TestCase;
  */
 class UserQueryTest extends TestCase {
 
-	public function __construct(string $name) {
-		parent::__construct($name);
+	#[\Override]
+	protected function setUp(): void {
 		if (!FreshRSS_Context::hasSystemConf()) {
 			FreshRSS_Context::initSystem();
 		}

+ 32 - 26
tests/app/Utils/httpUtilTest.php

@@ -114,33 +114,34 @@ final class httpUtilTest extends \PHPUnit\Framework\TestCase {
 		);
 	}
 
-	public function test_getCurlResolveInfoRejectsPrivateNat64Address(): void {
+	#[DataProvider('providePrivateSubnets')]
+	public function test_getCurlResolveInfoRejectsPrivateSubnets(string $ip): void {
 		FreshRSS_Context::initSystem();
 		$resolveOk = new ReflectionProperty(FreshRSS_http_Util::class, 'resolve_ok');
-		$resolveOk->setValue(null, [
-			'example.test' => [
-				'64:ff9b::a9fe:a9fe',
-			],
-		]);
-
-		self::assertNull(
-			FreshRSS_http_Util::getCurlResolveInfo('https://example.test/feed')
-		);
+		$resolveOk->setValue(null, ['example.test' => [$ip]]);
+		self::assertNull(FreshRSS_http_Util::getCurlResolveInfo('https://example.test/feed'));
 	}
 
-	public function test_getCurlResolveInfoRejectsLocalUseNat64Address(): void {
-		FreshRSS_Context::initSystem();
-		$resolveOk = new ReflectionProperty(FreshRSS_http_Util::class, 'resolve_ok');
-		$resolveOk->setValue(null, [
-			'example.test' => [
-				'64:ff9b:1::c000:4260',
-				'64:ff9b:1::a9fe:a9fe',
-			],
-		]);
-
-		self::assertNull(
-			FreshRSS_http_Util::getCurlResolveInfo('https://example.test/feed')
-		);
+	/** @return array<string,array{string}> */
+	public static function providePrivateSubnets(): array {
+		return [
+			// One example per range, ensuring FILTER_FLAG_GLOBAL_RANGE and custom checks are sufficient
+			'127.0.0.0/8' => ['127.0.0.1'],	// RFC1700 (Loopback)
+			'10.0.0.0/8' => ['10.0.0.5'],	// RFC1918
+			'192.168.0.0/16' => ['192.168.1.5'],	// RFC1918
+			'172.16.0.0/12' => ['172.16.5.5'],	// RFC1918
+			'169.254.0.0/16' => ['169.254.5.5'],	// RFC3927
+			'0.0.0.0/8' => ['0.0.0.5'],	// RFC5735
+			'240.0.0.0/4' => ['240.0.0.5'],	// RFC1112
+			'100.64.0.0/10' => ['100.64.5.5'],	// RFC6598 (Shared Address Space of Carrier-Grade NAT)
+			'::1/128' => ['::1'],	// Loopback
+			'fc00::/7' => ['fc00::5'],	// Unique Local Address
+			'fe80::/10' => ['fe80::5'],	// Link Local Address
+			'::ffff:0:0/96' => ['::ffff:10.0.0.5'],	// IPv4 translations
+			'64:ff9b::/96' => ['64:ff9b::a9fe:a9fe'],	// RFC6052 (IPv4/IPv6 Translator, NAT64)
+			'64:ff9b:1::/48' => ['64:ff9b:1::c000:4260'],	// RFC8215 (Local-Use IPv4/IPv6 Translation Prefix)
+			'::/128' => ['::'],	// Unspecified address
+		];
 	}
 
 	/** Socks schemes default to port 1080, and an IP address proxy produces no resolve entry */
@@ -171,11 +172,16 @@ final class httpUtilTest extends \PHPUnit\Framework\TestCase {
 			FreshRSS_http_Util::getCurlResolveInfo('socks5h://proxy.example.test:1080')
 		);
 
-		// An entry is still produced without allowlisting, because the address is public
+		// Applications must not emit resolve entries for an address that is not guaranteed public.
+		// TEST-NET-2 is a reserved range, so without an allowlist entry, no entry is produced.
 		FreshRSS_Context::systemConf()->internal_host_allowlist = [];
+		self::assertNull(FreshRSS_http_Util::getCurlResolveInfo('socks5h://proxy.example.test:1080'));
+
+		// A genuinely public address produces an entry without allowlisting
+		$resolveOk->setValue(null, ['public-proxy.example.test' => ['8.8.8.8']]);
 		self::assertSame(
-			['proxy.example.test:1080:198.51.100.7'],
-			FreshRSS_http_Util::getCurlResolveInfo('socks5h://proxy.example.test:1080')
+			['public-proxy.example.test:1080:8.8.8.8'],
+			FreshRSS_http_Util::getCurlResolveInfo('socks5h://public-proxy.example.test:1080')
 		);
 
 		// A proxy resolving to a private address is rejected unless allowlisted

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio