Update website

This commit is contained in:
Guilhem Lavaux 2024-11-19 09:59:00 +01:00
parent 011b183e28
commit 41ce1aa076
23 changed files with 284 additions and 94 deletions

View File

@ -25,7 +25,7 @@ return array(
'Symfony\\Component\\ErrorHandler\\' => array($vendorDir . '/symfony/error-handler'),
'Stevenmaguire\\OAuth2\\Client\\' => array($vendorDir . '/stevenmaguire/oauth2-bitbucket/src'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src', $vendorDir . '/psr/http-factory/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),

View File

@ -159,8 +159,8 @@ class ComposerStaticInitecf73f43d2b62bd3c0232fbab805eceb
),
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-message/src',
1 => __DIR__ . '/..' . '/psr/http-factory/src',
0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-message/src',
),
'Psr\\Http\\Client\\' =>
array (

View File

@ -1536,17 +1536,17 @@
},
{
"name": "symfony/error-handler",
"version": "v5.4.45",
"version_normalized": "5.4.45.0",
"version": "v5.4.46",
"version_normalized": "5.4.46.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
"reference": "55ac2507a8bf97f2d04f8d1e7f104c984ddcaf31"
"reference": "d19ede7a2cafb386be9486c580649d0f9e3d0363"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/55ac2507a8bf97f2d04f8d1e7f104c984ddcaf31",
"reference": "55ac2507a8bf97f2d04f8d1e7f104c984ddcaf31",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/d19ede7a2cafb386be9486c580649d0f9e3d0363",
"reference": "d19ede7a2cafb386be9486c580649d0f9e3d0363",
"shasum": ""
},
"require": {
@ -1559,7 +1559,7 @@
"symfony/http-kernel": "^4.4|^5.0|^6.0",
"symfony/serializer": "^4.4|^5.0|^6.0"
},
"time": "2024-09-25T14:11:13+00:00",
"time": "2024-11-05T14:17:06+00:00",
"bin": [
"Resources/bin/patch-type-declarations"
],
@ -1590,7 +1590,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/error-handler/tree/v5.4.45"
"source": "https://github.com/symfony/error-handler/tree/v5.4.46"
},
"funding": [
{
@ -1746,17 +1746,17 @@
},
{
"name": "symfony/http-client",
"version": "v5.4.45",
"version_normalized": "5.4.45.0",
"version": "v5.4.47",
"version_normalized": "5.4.47.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "54118c6340dc6831a00f10b296ea6e80592ec89d"
"reference": "3b643b83f87e1765d2e9b1e946bb56ee0b4b7bde"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/54118c6340dc6831a00f10b296ea6e80592ec89d",
"reference": "54118c6340dc6831a00f10b296ea6e80592ec89d",
"url": "https://api.github.com/repos/symfony/http-client/zipball/3b643b83f87e1765d2e9b1e946bb56ee0b4b7bde",
"reference": "3b643b83f87e1765d2e9b1e946bb56ee0b4b7bde",
"shasum": ""
},
"require": {
@ -1789,7 +1789,7 @@
"symfony/process": "^4.4|^5.0|^6.0",
"symfony/stopwatch": "^4.4|^5.0|^6.0"
},
"time": "2024-09-25T14:11:13+00:00",
"time": "2024-11-13T12:18:12+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -1820,7 +1820,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v5.4.45"
"source": "https://github.com/symfony/http-client/tree/v5.4.47"
},
"funding": [
{
@ -2578,17 +2578,17 @@
},
{
"name": "symfony/string",
"version": "v5.4.45",
"version_normalized": "5.4.45.0",
"version": "v5.4.47",
"version_normalized": "5.4.47.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "7f6807add88b1e2635f3c6de5e1ace631ed7cad2"
"reference": "136ca7d72f72b599f2631aca474a4f8e26719799"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/7f6807add88b1e2635f3c6de5e1ace631ed7cad2",
"reference": "7f6807add88b1e2635f3c6de5e1ace631ed7cad2",
"url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799",
"reference": "136ca7d72f72b599f2631aca474a4f8e26719799",
"shasum": ""
},
"require": {
@ -2608,7 +2608,7 @@
"symfony/translation-contracts": "^1.1|^2",
"symfony/var-exporter": "^4.4|^5.0|^6.0"
},
"time": "2024-09-25T14:11:13+00:00",
"time": "2024-11-10T20:33:58+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -2647,7 +2647,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v5.4.45"
"source": "https://github.com/symfony/string/tree/v5.4.47"
},
"funding": [
{
@ -2748,17 +2748,17 @@
},
{
"name": "symfony/var-dumper",
"version": "v5.4.45",
"version_normalized": "5.4.45.0",
"version": "v5.4.47",
"version_normalized": "5.4.47.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "c4a5a08fe8d836a1aeec59eeee9697457fd28723"
"reference": "e13e8dfa8eaab2b0536ef365beddc2af723a9ac0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/c4a5a08fe8d836a1aeec59eeee9697457fd28723",
"reference": "c4a5a08fe8d836a1aeec59eeee9697457fd28723",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/e13e8dfa8eaab2b0536ef365beddc2af723a9ac0",
"reference": "e13e8dfa8eaab2b0536ef365beddc2af723a9ac0",
"shasum": ""
},
"require": {
@ -2782,7 +2782,7 @@
"ext-intl": "To show region name in time zone dump",
"symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
},
"time": "2024-09-25T14:11:13+00:00",
"time": "2024-11-08T15:21:10+00:00",
"bin": [
"Resources/bin/var-dump-server"
],
@ -2820,7 +2820,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v5.4.45"
"source": "https://github.com/symfony/var-dumper/tree/v5.4.47"
},
"funding": [
{
@ -2893,17 +2893,17 @@
},
{
"name": "twig/twig",
"version": "v3.11.1",
"version_normalized": "3.11.1.0",
"version": "v3.11.3",
"version_normalized": "3.11.3.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "ff063afc691e1cfda6714f1915ed766cb108d188"
"reference": "3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/ff063afc691e1cfda6714f1915ed766cb108d188",
"reference": "ff063afc691e1cfda6714f1915ed766cb108d188",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e",
"reference": "3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e",
"shasum": ""
},
"require": {
@ -2918,7 +2918,7 @@
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"time": "2024-09-10T10:40:14+00:00",
"time": "2024-11-07T12:34:41+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -2960,7 +2960,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.11.1"
"source": "https://github.com/twigphp/Twig/tree/v3.11.3"
},
"funding": [
{

View File

@ -3,7 +3,7 @@
'name' => 'lavaux/website',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'daa513a17eb44c84c7a93675c67c268afc2a27c0',
'reference' => 'a1314b0ad5d4b3202bc5f3e4c0fb9dd7ddbca62a',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -85,7 +85,7 @@
'lavaux/website' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'daa513a17eb44c84c7a93675c67c268afc2a27c0',
'reference' => 'a1314b0ad5d4b3202bc5f3e4c0fb9dd7ddbca62a',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -239,9 +239,9 @@
'dev_requirement' => false,
),
'symfony/error-handler' => array(
'pretty_version' => 'v5.4.45',
'version' => '5.4.45.0',
'reference' => '55ac2507a8bf97f2d04f8d1e7f104c984ddcaf31',
'pretty_version' => 'v5.4.46',
'version' => '5.4.46.0',
'reference' => 'd19ede7a2cafb386be9486c580649d0f9e3d0363',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/error-handler',
'aliases' => array(),
@ -266,9 +266,9 @@
'dev_requirement' => false,
),
'symfony/http-client' => array(
'pretty_version' => 'v5.4.45',
'version' => '5.4.45.0',
'reference' => '54118c6340dc6831a00f10b296ea6e80592ec89d',
'pretty_version' => 'v5.4.47',
'version' => '5.4.47.0',
'reference' => '3b643b83f87e1765d2e9b1e946bb56ee0b4b7bde',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/http-client',
'aliases' => array(),
@ -362,9 +362,9 @@
'dev_requirement' => false,
),
'symfony/string' => array(
'pretty_version' => 'v5.4.45',
'version' => '5.4.45.0',
'reference' => '7f6807add88b1e2635f3c6de5e1ace631ed7cad2',
'pretty_version' => 'v5.4.47',
'version' => '5.4.47.0',
'reference' => '136ca7d72f72b599f2631aca474a4f8e26719799',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/string',
'aliases' => array(),
@ -380,9 +380,9 @@
'dev_requirement' => false,
),
'symfony/var-dumper' => array(
'pretty_version' => 'v5.4.45',
'version' => '5.4.45.0',
'reference' => 'c4a5a08fe8d836a1aeec59eeee9697457fd28723',
'pretty_version' => 'v5.4.47',
'version' => '5.4.47.0',
'reference' => 'e13e8dfa8eaab2b0536ef365beddc2af723a9ac0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/var-dumper',
'aliases' => array(),
@ -398,9 +398,9 @@
'dev_requirement' => false,
),
'twig/twig' => array(
'pretty_version' => 'v3.11.1',
'version' => '3.11.1.0',
'reference' => 'ff063afc691e1cfda6714f1915ed766cb108d188',
'pretty_version' => 'v3.11.3',
'version' => '3.11.3.0',
'reference' => '3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e',
'type' => 'library',
'install_path' => __DIR__ . '/../twig/twig',
'aliases' => array(),

View File

@ -806,7 +806,7 @@ class ErrorHandler
*/
private function parseAnonymousClass(string $message): string
{
return preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', static function ($m) {
return preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', static function ($m) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
}, $message);
}

View File

@ -226,7 +226,7 @@ class FlattenException
public function setMessage(string $message): self
{
if (false !== strpos($message, "@anonymous\0")) {
$message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
$message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
}, $message);
}

View File

@ -148,6 +148,8 @@ class HttpOptions
}
/**
* @param callable(int, int, array, \Closure|null=):void $callback
*
* @return $this
*/
public function setOnProgress(callable $callback)

View File

@ -138,7 +138,15 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
// Memoize the last progress to ease calling the callback periodically when no network transfer happens
$lastProgress = [0, 0];
$maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : \INF;
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) {
$multi = $this->multi;
$resolve = static function (string $host, ?string $ip = null) use ($multi): ?string {
if (null !== $ip) {
$multi->dnsCache[$host] = $ip;
}
return $multi->dnsCache[$host] ?? null;
};
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration, $resolve) {
if ($info['total_time'] >= $maxDuration) {
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
}
@ -154,7 +162,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
$lastProgress = $progress ?: $lastProgress;
}
$onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
$onProgress($lastProgress[0], $lastProgress[1], $progressInfo, $resolve);
};
} elseif (0 < $options['max_duration']) {
$maxDuration = $options['max_duration'];

View File

@ -77,9 +77,33 @@ final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwa
}
$subnets = $this->subnets;
$lastUrl = '';
$lastPrimaryIp = '';
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use ($onProgress, $subnets, &$lastPrimaryIp): void {
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use ($onProgress, $subnets, &$lastUrl, &$lastPrimaryIp): void {
if ($info['url'] !== $lastUrl) {
$host = trim(parse_url($info['url'], PHP_URL_HOST) ?: '', '[]');
$resolve ??= static fn () => null;
if (($ip = $host)
&& !filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)
&& !filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)
&& !$ip = $resolve($host)
) {
if ($ip = @(dns_get_record($host, \DNS_A)[0]['ip'] ?? null)) {
$resolve($host, $ip);
} elseif ($ip = @(dns_get_record($host, \DNS_AAAA)[0]['ipv6'] ?? null)) {
$resolve($host, '['.$ip.']');
}
}
if ($ip && IpUtils::checkIp($ip, $subnets ?? self::PRIVATE_SUBNETS)) {
throw new TransportException(sprintf('Host "%s" is blocked for "%s".', $host, $info['url']));
}
$lastUrl = $info['url'];
}
if ($info['primary_ip'] !== $lastPrimaryIp) {
if ($info['primary_ip'] && IpUtils::checkIp($info['primary_ip'], $subnets ?? self::PRIVATE_SUBNETS)) {
throw new TransportException(sprintf('IP "%s" is blocked for "%s".', $info['primary_ip'], $info['url']));

View File

@ -89,10 +89,17 @@ final class AmpResponse implements ResponseInterface, StreamableInterface
$info['max_duration'] = $options['max_duration'];
$info['debug'] = '';
$resolve = static function (string $host, ?string $ip = null) use ($multi): ?string {
if (null !== $ip) {
$multi->dnsCache[$host] = $ip;
}
return $multi->dnsCache[$host] ?? null;
};
$onProgress = $options['on_progress'] ?? static function () {};
$onProgress = $this->onProgress = static function () use (&$info, $onProgress) {
$onProgress = $this->onProgress = static function () use (&$info, $onProgress, $resolve) {
$info['total_time'] = microtime(true) - $info['start_time'];
$onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info);
$onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info, $resolve);
};
$pauseDeferred = new Deferred();

View File

@ -156,8 +156,8 @@ final class AsyncContext
$this->info['previous_info'][] = $info = $this->response->getInfo();
if (null !== $onProgress = $options['on_progress'] ?? null) {
$thisInfo = &$this->info;
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info);
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info, $resolve);
};
}
if (0 < ($info['max_duration'] ?? 0) && 0 < ($info['total_time'] ?? 0)) {

View File

@ -51,8 +51,8 @@ final class AsyncResponse implements ResponseInterface, StreamableInterface
if (null !== $onProgress = $options['on_progress'] ?? null) {
$thisInfo = &$this->info;
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info);
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info, $resolve);
};
}
$this->response = $client->request($method, $url, ['buffer' => false] + $options);

View File

@ -115,13 +115,20 @@ final class CurlResponse implements ResponseInterface, StreamableInterface
curl_pause($ch, \CURLPAUSE_CONT);
if ($onProgress = $options['on_progress']) {
$resolve = static function (string $host, ?string $ip = null) use ($multi): ?string {
if (null !== $ip) {
$multi->dnsCache->hostnames[$host] = $ip;
}
return $multi->dnsCache->hostnames[$host] ?? null;
};
$url = isset($info['url']) ? ['url' => $info['url']] : [];
curl_setopt($ch, \CURLOPT_NOPROGRESS, false);
curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer) {
curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer, $resolve) {
try {
rewind($debugBuffer);
$debug = ['debug' => stream_get_contents($debugBuffer)];
$onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug);
$onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug, $resolve);
} catch (\Throwable $e) {
$multi->handlesActivity[(int) $ch][] = null;
$multi->handlesActivity[(int) $ch][] = $e;

View File

@ -58,11 +58,11 @@ final class TraceableHttpClient implements HttpClientInterface, ResetInterface,
$content = false;
}
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use (&$traceInfo, $onProgress) {
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$traceInfo, $onProgress) {
$traceInfo = $info;
if (null !== $onProgress) {
$onProgress($dlNow, $dlSize, $info);
$onProgress($dlNow, $dlSize, $info, $resolve);
}
};

View File

@ -348,14 +348,14 @@ final class EnglishInflector implements InflectorInterface
// indices (index)
['xedni', 5, false, true, ['indicies', 'indexes']],
// fax (faxes, faxxes)
['xaf', 3, true, true, ['faxes', 'faxxes']],
// boxes (box)
['xo', 2, false, true, 'oxes'],
// indexes (index), matrixes (matrix)
['x', 1, true, false, ['cies', 'xes']],
// appendices (appendix)
['xi', 2, false, true, 'ices'],
// indexes (index), matrixes (matrix), appendices (appendix)
['x', 1, true, false, ['ces', 'xes']],
// babies (baby)
['y', 1, false, true, 'ies'],

View File

@ -56,7 +56,7 @@ class ClassStub extends ConstStub
}
if (str_contains($identifier, "@anonymous\0")) {
$this->value = $identifier = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
$this->value = $identifier = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
}, $identifier);
}

View File

@ -288,7 +288,7 @@ class ExceptionCaster
unset($a[$xPrefix.'string'], $a[Caster::PREFIX_DYNAMIC.'xdebug_message'], $a[Caster::PREFIX_DYNAMIC.'__destructorException']);
if (isset($a[Caster::PREFIX_PROTECTED.'message']) && str_contains($a[Caster::PREFIX_PROTECTED.'message'], "@anonymous\0")) {
$a[Caster::PREFIX_PROTECTED.'message'] = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
$a[Caster::PREFIX_PROTECTED.'message'] = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
}, $a[Caster::PREFIX_PROTECTED.'message']);
}

View File

@ -1,3 +1,14 @@
# 3.11.3 (2024-11-07)
* Fix an infinite recursion in the sandbox code
# 3.11.2 (2024-11-06)
* [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects
They are now checked via the property policy
* Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()`
under some circumstances on an object even if the `__toString()` method is not allowed by the security policy
# 3.11.1 (2024-09-10)
* Fix a security issue when an included sandboxed template has been loaded before without the sandbox context

View File

@ -43,11 +43,11 @@ use Twig\TokenParser\TokenParserInterface;
*/
class Environment
{
public const VERSION = '3.11.1';
public const VERSION_ID = 301101;
public const VERSION = '3.11.3';
public const VERSION_ID = 301103;
public const MAJOR_VERSION = 4;
public const MINOR_VERSION = 11;
public const RELEASE_VERSION = 1;
public const RELEASE_VERSION = 3;
public const EXTRA_VERSION = '';
private $charset;

View File

@ -57,6 +57,8 @@ use Twig\Node\Expression\Unary\NegUnary;
use Twig\Node\Expression\Unary\NotUnary;
use Twig\Node\Expression\Unary\PosUnary;
use Twig\NodeVisitor\MacroAutoImportNodeVisitor;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Source;
use Twig\Template;
use Twig\TemplateWrapper;
@ -82,6 +84,20 @@ use Twig\TwigTest;
final class CoreExtension extends AbstractExtension
{
public const ARRAY_LIKE_CLASSES = [
'ArrayIterator',
'ArrayObject',
'CachingIterator',
'RecursiveArrayIterator',
'RecursiveCachingIterator',
'SplDoublyLinkedList',
'SplFixedArray',
'SplObjectStorage',
'SplQueue',
'SplStack',
'WeakMap',
];
private $dateFormats = ['F j, Y H:i', '%d days'];
private $numberFormat = [0, '.', ','];
private $timezone = null;
@ -1549,10 +1565,20 @@ final class CoreExtension extends AbstractExtension
*/
public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1)
{
$propertyNotAllowedError = null;
// array
if (/* Template::METHOD_CALL */ 'method' !== $type) {
$arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item;
if ($sandboxed && $object instanceof \ArrayAccess && !\in_array(get_class($object), self::ARRAY_LIKE_CLASSES, true)) {
try {
$env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source);
} catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) {
goto methodCheck;
}
}
if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object)))
|| ($object instanceof \ArrayAccess && isset($object[$arrayItem]))
) {
@ -1624,19 +1650,25 @@ final class CoreExtension extends AbstractExtension
// object property
if (/* Template::METHOD_CALL */ 'method' !== $type) {
if ($sandboxed) {
try {
$env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source);
} catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) {
goto methodCheck;
}
}
if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) {
if ($isDefinedTest) {
return true;
}
if ($sandboxed) {
$env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source);
}
return $object->$item;
}
}
methodCheck:
static $cache = [];
$class = \get_class($object);
@ -1695,6 +1727,10 @@ final class CoreExtension extends AbstractExtension
return false;
}
if ($propertyNotAllowedError) {
throw $propertyNotAllowedError;
}
if ($ignoreStrictCheck || !$env->isStrictVariables()) {
return;
}
@ -1702,12 +1738,24 @@ final class CoreExtension extends AbstractExtension
throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source);
}
if ($isDefinedTest) {
return true;
if ($sandboxed) {
try {
$env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source);
} catch (SecurityNotAllowedMethodError $e) {
if ($isDefinedTest) {
return false;
}
if ($propertyNotAllowedError) {
throw $propertyNotAllowedError;
}
throw $e;
}
}
if ($sandboxed) {
$env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source);
if ($isDefinedTest) {
return true;
}
// Some objects throw exceptions when they have __call, and the method we try

View File

@ -119,6 +119,12 @@ final class SandboxExtension extends AbstractExtension
public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null)
{
if (\is_array($obj)) {
$this->ensureToStringAllowedForArray($obj, $lineno, $source);
return $obj;
}
if ($this->isSandboxed($source) && \is_object($obj) && method_exists($obj, '__toString')) {
try {
$this->policy->checkMethodAllowed($obj, '__toString');
@ -132,4 +138,45 @@ final class SandboxExtension extends AbstractExtension
return $obj;
}
private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, array &$stack = []): void
{
foreach ($obj as $k => $v) {
if (!$v) {
continue;
}
if (!\is_array($v)) {
$this->ensureToStringAllowed($v, $lineno, $source);
continue;
}
if (\PHP_VERSION_ID < 70400) {
static $cookie;
if ($v === $cookie ?? $cookie = new \stdClass()) {
continue;
}
$obj[$k] = $cookie;
try {
$this->ensureToStringAllowedForArray($v, $lineno, $source, $stack);
} finally {
$obj[$k] = $v;
}
continue;
}
if ($r = \ReflectionReference::fromArrayElement($obj, $k)) {
if (isset($stack[$r->getId()])) {
continue;
}
$stack[$r->getId()] = true;
}
$this->ensureToStringAllowedForArray($v, $lineno, $source, $stack);
}
}
}

View File

@ -31,6 +31,7 @@ class GetAttrExpression extends AbstractExpression
public function compile(Compiler $compiler): void
{
$env = $compiler->getEnvironment();
$arrayAccessSandbox = false;
// optimize array calls
if (
@ -44,17 +45,35 @@ class GetAttrExpression extends AbstractExpression
->raw('(('.$var.' = ')
->subcompile($this->getNode('node'))
->raw(') && is_array(')
->raw($var)
->raw($var);
if (!$env->hasExtension(SandboxExtension::class)) {
$compiler
->raw(') || ')
->raw($var)
->raw(' instanceof ArrayAccess ? (')
->raw($var)
->raw('[')
->subcompile($this->getNode('attribute'))
->raw('] ?? null) : null)')
;
return;
}
$arrayAccessSandbox = true;
$compiler
->raw(') || ')
->raw($var)
->raw(' instanceof ArrayAccess ? (')
->raw(' instanceof ArrayAccess && in_array(')
->raw('get_class('.$var.')')
->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (')
->raw($var)
->raw('[')
->subcompile($this->getNode('attribute'))
->raw('] ?? null) : null)')
->raw('] ?? null) : ')
;
return;
}
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
@ -83,5 +102,9 @@ class GetAttrExpression extends AbstractExpression
->raw(', ')->repr($this->getNode('node')->getTemplateLine())
->raw(')')
;
if ($arrayAccessSandbox) {
$compiler->raw(')');
}
}
}

View File

@ -15,12 +15,14 @@ use Twig\Environment;
use Twig\Node\CheckSecurityCallNode;
use Twig\Node\CheckSecurityNode;
use Twig\Node\CheckToStringNode;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\Binary\RangeBinary;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\PrintNode;
@ -120,7 +122,18 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
{
$expr = $node->getNode($name);
if (($expr instanceof NameExpression || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) {
$node->setNode($name, new CheckToStringNode($expr));
// Simplify in 4.0 as the spread attribute has been removed there
$new = new CheckToStringNode($expr);
if ($expr->hasAttribute('spread')) {
$new->setAttribute('spread', $expr->getAttribute('spread'));
}
$node->setNode($name, $new);
} elseif ($expr instanceof SpreadUnary) {
$this->wrapNode($expr, 'node');
} elseif ($expr instanceof ArrayExpression) {
foreach ($expr as $name => $_) {
$this->wrapNode($expr, $name);
}
}
}