diff --git a/app/composer.json b/app/composer.json index 9aa96ddbf..a77a69354 100644 --- a/app/composer.json +++ b/app/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=8.0", - "cakephp/cakephp": "4.5.*", + "cakephp/cakephp": "4.6.*", "cakephp/migrations": "^3.2", "cakephp/plugin-installer": "^1.3", "doctrine/dbal": "^3.3", diff --git a/app/composer.lock b/app/composer.lock index f383754fd..325a4aae3 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0db20fb981b4b8c8d1f6b017df5c777f", + "content-hash": "0903212b8cb5b43bd6eaa080bf0d70a4", "packages": [ { "name": "cakephp/cakephp", - "version": "4.5.4", + "version": "4.6.0", "source": { "type": "git", "url": "https://github.com/cakephp/cakephp.git", - "reference": "a28d65864a67d52482dba82207a4a22c15a59ef4" + "reference": "b8585672346c0654311c77500ce613cdf37687cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/cakephp/zipball/a28d65864a67d52482dba82207a4a22c15a59ef4", - "reference": "a28d65864a67d52482dba82207a4a22c15a59ef4", + "url": "https://api.github.com/repos/cakephp/cakephp/zipball/b8585672346c0654311c77500ce613cdf37687cc", + "reference": "b8585672346c0654311c77500ce613cdf37687cc", "shasum": "" }, "require": { @@ -117,20 +117,20 @@ "issues": "https://github.com/cakephp/cakephp/issues", "source": "https://github.com/cakephp/cakephp" }, - "time": "2024-03-02T03:23:45+00:00" + "time": "2025-03-23T02:36:48+00:00" }, { "name": "cakephp/chronos", - "version": "2.4.4", + "version": "2.4.5", "source": { "type": "git", "url": "https://github.com/cakephp/chronos.git", - "reference": "03208c18eb3267490662e68671bb9c22aa804492" + "reference": "b0321ab7658af9e7abcb3dd876f226e6f3dbb81f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/chronos/zipball/03208c18eb3267490662e68671bb9c22aa804492", - "reference": "03208c18eb3267490662e68671bb9c22aa804492", + "url": "https://api.github.com/repos/cakephp/chronos/zipball/b0321ab7658af9e7abcb3dd876f226e6f3dbb81f", + "reference": "b0321ab7658af9e7abcb3dd876f226e6f3dbb81f", "shasum": "" }, "require": { @@ -175,7 +175,7 @@ "issues": "https://github.com/cakephp/chronos/issues", "source": "https://github.com/cakephp/chronos" }, - "time": "2023-11-03T05:26:08+00:00" + "time": "2024-07-30T22:26:11+00:00" }, { "name": "cakephp/migrations", @@ -288,28 +288,28 @@ }, { "name": "composer/ca-bundle", - "version": "1.4.1", + "version": "1.5.6", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd" + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", - "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f65c239c970e7f072f067ab78646e9f0b2935175", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "psr/log": "^1.0", - "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { @@ -344,7 +344,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.4.1" + "source": "https://github.com/composer/ca-bundle/tree/1.5.6" }, "funding": [ { @@ -360,7 +360,7 @@ "type": "tidelift" } ], - "time": "2024-02-23T10:16:52+00:00" + "time": "2025-03-06T14:30:56+00:00" }, { "name": "doctrine/cache", @@ -747,8 +747,8 @@ "type": "library", "extra": { "laminas": { - "config-provider": "Laminas\\Diactoros\\ConfigProvider", - "module": "Laminas\\Diactoros" + "module": "Laminas\\Diactoros", + "config-provider": "Laminas\\Diactoros\\ConfigProvider" } }, "autoload": { @@ -805,30 +805,30 @@ }, { "name": "laminas/laminas-httphandlerrunner", - "version": "2.10.0", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-httphandlerrunner.git", - "reference": "35a0ba92e940a2f9533754f5a56187fa321f7693" + "reference": "c428d9f67f280d155637cbe2b7245b5188c8cdae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-httphandlerrunner/zipball/35a0ba92e940a2f9533754f5a56187fa321f7693", - "reference": "35a0ba92e940a2f9533754f5a56187fa321f7693", + "url": "https://api.github.com/repos/laminas/laminas-httphandlerrunner/zipball/c428d9f67f280d155637cbe2b7245b5188c8cdae", + "reference": "c428d9f67f280d155637cbe2b7245b5188c8cdae", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "psr/http-message": "^1.0 || ^2.0", "psr/http-message-implementation": "^1.0 || ^2.0", "psr/http-server-handler": "^1.0" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.5.0", - "laminas/laminas-diactoros": "^3.3.0", - "phpunit/phpunit": "^10.5.5", - "psalm/plugin-phpunit": "^0.18.4", - "vimeo/psalm": "^5.18" + "laminas/laminas-coding-standard": "~3.0.0", + "laminas/laminas-diactoros": "^3.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^5.26.1" }, "type": "library", "extra": { @@ -868,7 +868,7 @@ "type": "community_bridge" } ], - "time": "2024-01-04T10:50:34+00:00" + "time": "2024-10-17T20:37:17+00:00" }, { "name": "league/container", @@ -1411,20 +1411,20 @@ }, { "name": "psr/http-factory", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "type": "library", @@ -1448,7 +1448,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -1460,9 +1460,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-04-10T20:10:41+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", @@ -3036,16 +3036,16 @@ }, { "name": "composer/composer", - "version": "2.7.1", + "version": "2.7.6", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc" + "reference": "fabd995783b633829fd4280e272284b39b6ae702" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", - "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", + "url": "https://api.github.com/repos/composer/composer/zipball/fabd995783b633829fd4280e272284b39b6ae702", + "reference": "fabd995783b633829fd4280e272284b39b6ae702", "shasum": "" }, "require": { @@ -3089,13 +3089,13 @@ ], "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.7-dev" - }, "phpstan": { "includes": [ "phpstan/rules.neon" ] + }, + "branch-alias": { + "dev-main": "2.7-dev" } }, "autoload": { @@ -3130,7 +3130,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.7.1" + "source": "https://github.com/composer/composer/tree/2.7.6" }, "funding": [ { @@ -3146,7 +3146,7 @@ "type": "tidelift" } ], - "time": "2024-02-09T14:26:28+00:00" + "time": "2024-05-04T21:03:15+00:00" }, { "name": "composer/metadata-minifier", @@ -6402,16 +6402,16 @@ }, { "name": "symfony/process", - "version": "v7.0.4", + "version": "v7.2.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9" + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/0e7727191c3b71ebec6d529fa0e50a01ca5679e9", - "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9", + "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", "shasum": "" }, "require": { @@ -6443,7 +6443,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.0.4" + "source": "https://github.com/symfony/process/tree/v7.2.5" }, "funding": [ { @@ -6459,7 +6459,7 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:27:20+00:00" + "time": "2025-03-13T12:21:46+00:00" }, { "name": "symfony/var-dumper", @@ -6664,30 +6664,37 @@ }, { "name": "twig/twig", - "version": "v3.8.0", + "version": "v3.20.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d" + "reference": "3468920399451a384bef53cf7996965f7cd40183" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183", + "reference": "3468920399451a384bef53cf7996965f7cd40183", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php80": "^1.22" + "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { + "phpstan/phpstan": "^2.0", "psr/container": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0" + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, "type": "library", "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -6720,7 +6727,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.8.0" + "source": "https://github.com/twigphp/Twig/tree/v3.20.0" }, "funding": [ { @@ -6732,7 +6739,7 @@ "type": "tidelift" } ], - "time": "2023-11-21T18:54:41+00:00" + "time": "2025-02-13T08:34:43+00:00" } ], "aliases": [], @@ -6745,6 +6752,6 @@ "platform": { "php": ">=8.0" }, - "platform-dev": {}, - "plugin-api-version": "2.6.0" + "platform-dev": [], + "plugin-api-version": "2.3.0" } diff --git a/app/vendor/cakephp-plugins.php b/app/vendor/cakephp-plugins.php index 9b3ecad34..8497f738b 100644 --- a/app/vendor/cakephp-plugins.php +++ b/app/vendor/cakephp-plugins.php @@ -12,5 +12,6 @@ 'DebugKit' => $baseDir . '/vendor/cakephp/debug_kit/', 'EnvSource' => $baseDir . '/plugins/EnvSource/', 'Migrations' => $baseDir . '/vendor/cakephp/migrations/', + 'TestWidget' => $baseDir . '/plugins/TestWidget/', ], ]; diff --git a/app/vendor/cakephp/cakephp/VERSION.txt b/app/vendor/cakephp/cakephp/VERSION.txt index 4a9ed9ac7..4ee60801c 100644 --- a/app/vendor/cakephp/cakephp/VERSION.txt +++ b/app/vendor/cakephp/cakephp/VERSION.txt @@ -16,4 +16,4 @@ // @license https://opensource.org/licenses/mit-license.php MIT License // +--------------------------------------------------------------------------------------------+ // //////////////////////////////////////////////////////////////////////////////////////////////////// -4.5.4 +4.6.0 diff --git a/app/vendor/cakephp/cakephp/src/Cache/Engine/MemcachedEngine.php b/app/vendor/cakephp/cakephp/src/Cache/Engine/MemcachedEngine.php index f490b363e..29c56954a 100644 --- a/app/vendor/cakephp/cakephp/src/Cache/Engine/MemcachedEngine.php +++ b/app/vendor/cakephp/cakephp/src/Cache/Engine/MemcachedEngine.php @@ -437,7 +437,7 @@ public function clear(): bool } foreach ($keys as $key) { - if (strpos($key, $this->_config['prefix']) === 0) { + if ($this->_config['prefix'] === '' || strpos($key, $this->_config['prefix']) === 0) { $this->_Memcached->delete($key); } } diff --git a/app/vendor/cakephp/cakephp/src/Cache/Engine/RedisEngine.php b/app/vendor/cakephp/cakephp/src/Cache/Engine/RedisEngine.php index dc90ef72f..cdd479fb2 100644 --- a/app/vendor/cakephp/cakephp/src/Cache/Engine/RedisEngine.php +++ b/app/vendor/cakephp/cakephp/src/Cache/Engine/RedisEngine.php @@ -45,6 +45,7 @@ class RedisEngine extends CacheEngine * - `password` Redis server password. * - `persistent` Connect to the Redis server with a persistent connection * - `port` port number to the Redis server. + * - `tls` connect to the Redis server using TLS. * - `prefix` Prefix appended to all entries. Good for when you need to share a keyspace * with either another cache config or another application. * - `scanCount` Number of keys to ask for each scan (default: 10) @@ -61,6 +62,7 @@ class RedisEngine extends CacheEngine 'password' => false, 'persistent' => true, 'port' => 6379, + 'tls' => false, 'prefix' => 'cake_', 'host' => null, 'server' => '127.0.0.1', @@ -99,24 +101,29 @@ public function init(array $config = []): bool */ protected function _connect(): bool { + $tls = $this->_config['tls'] === true ? 'tls://' : ''; + + $map = [ + 'ssl_ca' => 'cafile', + 'ssl_key' => 'local_pk', + 'ssl_cert' => 'local_cert', + ]; + + $ssl = []; + foreach ($map as $key => $context) { + if (!empty($this->_config[$key])) { + $ssl[$context] = $this->_config[$key]; + } + } + try { - $this->_Redis = new Redis(); + $this->_Redis = $this->_createRedisInstance(); if (!empty($this->_config['unix_socket'])) { $return = $this->_Redis->connect($this->_config['unix_socket']); } elseif (empty($this->_config['persistent'])) { - $return = $this->_Redis->connect( - $this->_config['server'], - (int)$this->_config['port'], - (int)$this->_config['timeout'] - ); + $return = $this->_connectTransient($tls . $this->_config['server'], $ssl); } else { - $persistentId = $this->_config['port'] . $this->_config['timeout'] . $this->_config['database']; - $return = $this->_Redis->pconnect( - $this->_config['server'], - (int)$this->_config['port'], - (int)$this->_config['timeout'], - $persistentId - ); + $return = $this->_connectPersistent($tls . $this->_config['server'], $ssl); } } catch (RedisException $e) { if (class_exists(Log::class)) { @@ -135,6 +142,67 @@ protected function _connect(): bool return $return; } + /** + * Connects to a Redis server using a new connection. + * + * @param string $server Server to connect to. + * @param array $ssl SSL context options. + * @throws \RedisException + * @return bool True if Redis server was connected + */ + protected function _connectTransient($server, array $ssl): bool + { + if (empty($ssl)) { + return $this->_Redis->connect( + $server, + (int)$this->_config['port'], + (int)$this->_config['timeout'] + ); + } + + return $this->_Redis->connect( + $server, + (int)$this->_config['port'], + (int)$this->_config['timeout'], + null, + 0, + 0.0, + ['ssl' => $ssl] + ); + } + + /** + * Connects to a Redis server using a persistent connection. + * + * @param string $server Server to connect to. + * @param array $ssl SSL context options. + * @throws \RedisException + * @return bool True if Redis server was connected + */ + protected function _connectPersistent($server, array $ssl): bool + { + $persistentId = $this->_config['port'] . $this->_config['timeout'] . $this->_config['database']; + + if (empty($ssl)) { + return $this->_Redis->pconnect( + $server, + (int)$this->_config['port'], + (int)$this->_config['timeout'], + $persistentId + ); + } + + return $this->_Redis->pconnect( + $server, + (int)$this->_config['port'], + (int)$this->_config['timeout'], + $persistentId, + 0, + 0.0, + ['ssl' => $ssl] + ); + } + /** * Write data for key into cache. * @@ -394,6 +462,16 @@ protected function unserialize(string $value) return unserialize($value); } + /** + * Create new Redis instance. + * + * @return \Redis + */ + protected function _createRedisInstance(): Redis + { + return new Redis(); + } + /** * Disconnects from the redis server */ diff --git a/app/vendor/cakephp/cakephp/src/Command/CacheClearGroupCommand.php b/app/vendor/cakephp/cakephp/src/Command/CacheClearGroupCommand.php index 52e083585..96b97247c 100644 --- a/app/vendor/cakephp/cakephp/src/Command/CacheClearGroupCommand.php +++ b/app/vendor/cakephp/cakephp/src/Command/CacheClearGroupCommand.php @@ -98,7 +98,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int )); $this->abort(); } else { - $io->success(sprintf('Group "%s" was cleared.', $group)); + $io->success(sprintf('Cache "%s" was cleared.', $groupConfig)); } } diff --git a/app/vendor/cakephp/cakephp/src/Console/ConsoleInputArgument.php b/app/vendor/cakephp/cakephp/src/Console/ConsoleInputArgument.php index 7070cb9bf..329189657 100644 --- a/app/vendor/cakephp/cakephp/src/Console/ConsoleInputArgument.php +++ b/app/vendor/cakephp/cakephp/src/Console/ConsoleInputArgument.php @@ -55,6 +55,13 @@ class ConsoleInputArgument */ protected $_choices; + /** + * Default value for the argument. + * + * @var string|null + */ + protected $_default; + /** * Make a new Input Argument * @@ -62,8 +69,9 @@ class ConsoleInputArgument * @param string $help The help text for this option * @param bool $required Whether this argument is required. Missing required args will trigger exceptions * @param array $choices Valid choices for this option. + * @param string|null $default The default value for this argument. */ - public function __construct($name, $help = '', $required = false, $choices = []) + public function __construct($name, $help = '', $required = false, $choices = [], $default = null) { if (is_array($name) && isset($name['name'])) { foreach ($name as $key => $value) { @@ -75,6 +83,7 @@ public function __construct($name, $help = '', $required = false, $choices = []) $this->_help = $help; $this->_required = $required; $this->_choices = $choices; + $this->_default = $default; } } @@ -119,6 +128,9 @@ public function help(int $width = 0): string if ($this->_choices) { $optional .= sprintf(' (choices: %s)', implode('|', $this->_choices)); } + if ($this->_default !== null) { + $optional .= sprintf(' default: "%s"', $this->_default); + } return sprintf('%s%s%s', $name, $this->_help, $optional); } @@ -142,6 +154,16 @@ public function usage(): string return $name; } + /** + * Get the default value for this argument + * + * @return string|null + */ + public function defaultValue() + { + return $this->_default; + } + /** * Check if this argument is a required argument * @@ -194,6 +216,9 @@ public function xml(SimpleXMLElement $parent): SimpleXMLElement foreach ($this->_choices as $valid) { $choices->addChild('choice', $valid); } + if ($this->_default !== null) { + $option->addAttribute('default', $this->_default); + } return $parent; } diff --git a/app/vendor/cakephp/cakephp/src/Console/ConsoleOptionParser.php b/app/vendor/cakephp/cakephp/src/Console/ConsoleOptionParser.php index 846f94fd1..eee1c8b4f 100644 --- a/app/vendor/cakephp/cakephp/src/Console/ConsoleOptionParser.php +++ b/app/vendor/cakephp/cakephp/src/Console/ConsoleOptionParser.php @@ -725,10 +725,15 @@ public function parse(array $argv, ?ConsoleIo $io = null): array } foreach ($this->_args as $i => $arg) { - if ($arg->isRequired() && !isset($args[$i])) { - throw new ConsoleException( - sprintf('Missing required argument. The `%s` argument is required.', $arg->name()) - ); + if (!isset($args[$i])) { + if ($arg->isRequired()) { + throw new ConsoleException( + sprintf('Missing required argument. The `%s` argument is required.', $arg->name()) + ); + } + if ($arg->defaultValue() !== null) { + $args[$i] = $arg->defaultValue(); + } } } foreach ($this->_options as $option) { diff --git a/app/vendor/cakephp/cakephp/src/Datasource/QueryInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/QueryInterface.php index fc7023a55..47fede090 100644 --- a/app/vendor/cakephp/cakephp/src/Datasource/QueryInterface.php +++ b/app/vendor/cakephp/cakephp/src/Datasource/QueryInterface.php @@ -279,6 +279,7 @@ public function toArray(): array; * * @param \Cake\Datasource\RepositoryInterface $repository The default repository object to use * @return $this + * @deprecated */ public function repository(RepositoryInterface $repository); diff --git a/app/vendor/cakephp/cakephp/src/Http/ServerRequestFactory.php b/app/vendor/cakephp/cakephp/src/Http/ServerRequestFactory.php index a4e48d07d..836c3c5b0 100644 --- a/app/vendor/cakephp/cakephp/src/Http/ServerRequestFactory.php +++ b/app/vendor/cakephp/cakephp/src/Http/ServerRequestFactory.php @@ -247,14 +247,7 @@ protected static function marshalUriFromSapi(array $server, array $headers): Uri $uri = marshalUriFromSapi($server, $headers); [$base, $webroot] = static::getBase($uri, $server); - // Look in PATH_INFO first, as this is the exact value we need prepared - // by PHP. - $pathInfo = Hash::get($server, 'PATH_INFO'); - if ($pathInfo) { - $uri = $uri->withPath($pathInfo); - } else { - $uri = static::updatePath($base, $uri); - } + $uri = static::updatePath($base, $uri); if (!$uri->getHost()) { $uri = $uri->withHost('localhost'); @@ -282,12 +275,18 @@ protected static function updatePath(string $base, UriInterface $uri): UriInterf if (empty($path) || $path === '/' || $path === '//' || $path === '/index.php') { $path = '/'; } - $endsWithIndex = '/' . (Configure::read('App.webroot') ?: 'webroot') . '/index.php'; - $endsWithLength = strlen($endsWithIndex); - if ( - strlen($path) >= $endsWithLength && - substr($path, -$endsWithLength) === $endsWithIndex - ) { + // Check for $webroot/index.php at the start and end of the path. + $search = ''; + if ($path[0] === '/') { + $search .= '/'; + } + $search .= (Configure::read('App.webroot') ?: 'webroot') . '/index.php'; + if (strpos($path, $search) === 0) { + $path = substr($path, strlen($search)); + } elseif (substr($path, -strlen($search)) === $search) { + $path = '/'; + } + if (!$path) { $path = '/'; } @@ -321,9 +320,9 @@ protected static function getBase(UriInterface $uri, array $server): array // Clean up additional / which cause following code to fail.. $base = preg_replace('#/+#', '/', $base); - $indexPos = strpos($base, '/' . $webroot . '/index.php'); + $indexPos = strpos($base, '/index.php'); if ($indexPos !== false) { - $base = substr($base, 0, $indexPos) . '/' . $webroot; + $base = substr($base, 0, $indexPos); } if ($webroot === basename($base)) { $base = dirname($base); diff --git a/app/vendor/cakephp/cakephp/src/I18n/DateFormatTrait.php b/app/vendor/cakephp/cakephp/src/I18n/DateFormatTrait.php index 942ce1a30..dca02329c 100644 --- a/app/vendor/cakephp/cakephp/src/I18n/DateFormatTrait.php +++ b/app/vendor/cakephp/cakephp/src/I18n/DateFormatTrait.php @@ -262,7 +262,7 @@ protected function _formatObject($date, $format, ?string $locale): string static::$_formatters[$key] = $formatter; } - return static::$_formatters[$key]->format($date->format('U')); + return static::$_formatters[$key]->format($date); } /** diff --git a/app/vendor/cakephp/cakephp/src/I18n/MessagesFileLoader.php b/app/vendor/cakephp/cakephp/src/I18n/MessagesFileLoader.php index 2d02ab18c..d47e86923 100644 --- a/app/vendor/cakephp/cakephp/src/I18n/MessagesFileLoader.php +++ b/app/vendor/cakephp/cakephp/src/I18n/MessagesFileLoader.php @@ -181,6 +181,8 @@ public function translationsFolders(): array foreach ($localePaths as $path) { foreach ($folders as $folder) { $searchPaths[] = $path . $folder . DIRECTORY_SEPARATOR; + // gettext compatible paths, see https://www.php.net/manual/en/function.gettext.php + $searchPaths[] = $path . $folder . DIRECTORY_SEPARATOR . 'LC_MESSAGES' . DIRECTORY_SEPARATOR; } } @@ -188,6 +190,8 @@ public function translationsFolders(): array $basePath = App::path('locales', $this->_plugin)[0]; foreach ($folders as $folder) { $searchPaths[] = $basePath . $folder . DIRECTORY_SEPARATOR; + // gettext compatible paths, see https://www.php.net/manual/en/function.gettext.php + $searchPaths[] = $basePath . $folder . DIRECTORY_SEPARATOR . 'LC_MESSAGES' . DIRECTORY_SEPARATOR; } } diff --git a/app/vendor/cakephp/cakephp/src/I18n/Number.php b/app/vendor/cakephp/cakephp/src/I18n/Number.php index ccc224f33..6c12f715a 100644 --- a/app/vendor/cakephp/cakephp/src/I18n/Number.php +++ b/app/vendor/cakephp/cakephp/src/I18n/Number.php @@ -231,6 +231,8 @@ public static function formatDelta($value, array $options = []): string * - `zero` - The text to use for zero values, can be a string or a number. e.g. 0, 'Free!' * - `places` - Number of decimal places to use. e.g. 2 * - `precision` - Maximum Number of decimal places to use, e.g. 2 + * - `roundingMode` - Rounding mode to use. e.g. NumberFormatter::ROUND_HALF_UP. + * When not set locale default will be used * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00 * - `useIntlCode` - Whether to replace the currency symbol with the international * currency code. @@ -365,6 +367,8 @@ public static function setDefaultCurrencyFormat($currencyFormat = null): void * numbers representing money or a NumberFormatter constant. * - `places` - Number of decimal places to use. e.g. 2 * - `precision` - Maximum Number of decimal places to use, e.g. 2 + * - `roundingMode` - Rounding mode to use. e.g. NumberFormatter::ROUND_HALF_UP. + * When not set locale default will be used * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00 * - `useIntlCode` - Whether to replace the currency symbol with the international * currency code. @@ -406,6 +410,7 @@ public static function formatter(array $options = []): NumberFormatter $options = array_intersect_key($options, [ 'places' => null, 'precision' => null, + 'roundingMode' => null, 'pattern' => null, 'useIntlCode' => null, ]); @@ -452,6 +457,10 @@ protected static function _setAttributes(NumberFormatter $formatter, array $opti $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $options['precision']); } + if (isset($options['roundingMode'])) { + $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, $options['roundingMode']); + } + if (!empty($options['pattern'])) { $formatter->setPattern($options['pattern']); } diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsTo.php b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsTo.php index ad2294482..3072073a5 100644 --- a/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsTo.php +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsTo.php @@ -31,6 +31,9 @@ * related to only one record in the target table. * * An example of a BelongsTo association would be Article belongs to Author. + * + * @template T of \Cake\ORM\Table + * @mixin T */ class BelongsTo extends Association { diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php index 8c95cbc75..71b26e4a0 100644 --- a/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php @@ -37,6 +37,9 @@ * * An example of a BelongsToMany association would be Article belongs to many Tags. * In this example 'Article' is the source table and 'Tags' is the target table. + * + * @template T of \Cake\ORM\Table + * @mixin T */ class BelongsToMany extends Association { diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/HasMany.php b/app/vendor/cakephp/cakephp/src/ORM/Association/HasMany.php index 9dbeac72b..5d95621da 100644 --- a/app/vendor/cakephp/cakephp/src/ORM/Association/HasMany.php +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/HasMany.php @@ -33,6 +33,9 @@ * will have one or multiple records per each one in the source side. * * An example of a HasMany association would be Author has many Articles. + * + * @template T of \Cake\ORM\Table + * @mixin T */ class HasMany extends Association { diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/HasOne.php b/app/vendor/cakephp/cakephp/src/ORM/Association/HasOne.php index 746caddfc..ff239477d 100644 --- a/app/vendor/cakephp/cakephp/src/ORM/Association/HasOne.php +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/HasOne.php @@ -29,6 +29,9 @@ * related to only one record in the target table and vice versa. * * An example of a HasOne association would be User has one Profile. + * + * @template T of \Cake\ORM\Table + * @mixin T */ class HasOne extends Association { diff --git a/app/vendor/cakephp/cakephp/src/ORM/BehaviorRegistry.php b/app/vendor/cakephp/cakephp/src/ORM/BehaviorRegistry.php index e4b76e1c3..9f006a1bd 100644 --- a/app/vendor/cakephp/cakephp/src/ORM/BehaviorRegistry.php +++ b/app/vendor/cakephp/cakephp/src/ORM/BehaviorRegistry.php @@ -203,6 +203,31 @@ protected function _getMethods(Behavior $instance, string $class, string $alias) return compact('methods', 'finders'); } + /** + * Remove an object from the registry. + * + * If this registry has an event manager, the object will be detached from any events as well. + * + * @param string $name The name of the object to remove from the registry. + * @return $this + */ + public function unload(string $name) + { + $instance = $this->get($name); + $result = parent::unload($name); + + $methods = array_change_key_case($instance->implementedMethods()); + foreach (array_keys($methods) as $method) { + unset($this->_methodMap[$method]); + } + $finders = array_change_key_case($instance->implementedFinders()); + foreach (array_keys($finders) as $finder) { + unset($this->_finderMap[$finder]); + } + + return $result; + } + /** * Check if any loaded behavior implements a method. * diff --git a/app/vendor/cakephp/cakephp/src/ORM/Query/SelectQuery.php b/app/vendor/cakephp/cakephp/src/ORM/Query/SelectQuery.php index 06942fabb..368b6d7b7 100644 --- a/app/vendor/cakephp/cakephp/src/ORM/Query/SelectQuery.php +++ b/app/vendor/cakephp/cakephp/src/ORM/Query/SelectQuery.php @@ -16,6 +16,7 @@ */ namespace Cake\ORM\Query; +use Cake\Database\Connection; use Cake\ORM\Query; /** @@ -89,4 +90,38 @@ public function set($key, $value = null, $types = []) return parent::set($key, $value, $types); } + + /** + * Sets the connection role. + * + * @param string $role Connection role ('read' or 'write') + * @return $this + */ + public function setConnectionRole(string $role) + { + assert($role === Connection::ROLE_READ || $role === Connection::ROLE_WRITE); + $this->connectionRole = $role; + + return $this; + } + + /** + * Sets the connection role to read. + * + * @return $this + */ + public function useReadRole() + { + return $this->setConnectionRole(Connection::ROLE_READ); + } + + /** + * Sets the connection role to write. + * + * @return $this + */ + public function useWriteRole() + { + return $this->setConnectionRole(Connection::ROLE_WRITE); + } } diff --git a/app/vendor/cakephp/cakephp/src/Routing/RouteCollection.php b/app/vendor/cakephp/cakephp/src/Routing/RouteCollection.php index 73a34178e..c91e5ba9e 100644 --- a/app/vendor/cakephp/cakephp/src/Routing/RouteCollection.php +++ b/app/vendor/cakephp/cakephp/src/Routing/RouteCollection.php @@ -204,11 +204,19 @@ public function parse(string $url, string $method = ''): array public function parseRequest(ServerRequestInterface $request): array { $uri = $request->getUri(); - $urlPath = urldecode($uri->getPath()); + $urlPath = $uri->getPath(); + if (strpos($urlPath, '%') !== false) { + // decode urlencoded segments, but don't decode %2f aka / + $parts = explode('/', $urlPath); + $parts = array_map( + fn (string $part) => str_replace('/', '%2f', urldecode($part)), + $parts + ); + $urlPath = implode('/', $parts); + } if ($urlPath !== '/') { $urlPath = rtrim($urlPath, '/'); } - if (isset($this->staticPaths[$urlPath])) { foreach ($this->staticPaths[$urlPath] as $route) { $r = $route->parseRequest($request); @@ -217,7 +225,7 @@ public function parseRequest(ServerRequestInterface $request): array } if ($uri->getQuery()) { parse_str($uri->getQuery(), $queryParameters); - $r['?'] = $queryParameters; + $r['?'] = array_merge($r['?'] ?? [], $queryParameters); } return $r; diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/StringCompareTrait.php b/app/vendor/cakephp/cakephp/src/TestSuite/StringCompareTrait.php index 0b388fbe8..b37298b9e 100644 --- a/app/vendor/cakephp/cakephp/src/TestSuite/StringCompareTrait.php +++ b/app/vendor/cakephp/cakephp/src/TestSuite/StringCompareTrait.php @@ -47,6 +47,11 @@ trait StringCompareTrait /** * Compare the result to the contents of the file * + * Set UPDATE_TEST_COMPARISON_FILES=1 in your environment + * to have this assertion *overwrite* comparison files. This + * is useful when you intentionally make a behavior change and + * want a quick way to capture the baseline output. + * * @param string $path partial path to test comparison file * @param string $result test result as a string * @return void diff --git a/app/vendor/cakephp/cakephp/src/Utility/Inflector.php b/app/vendor/cakephp/cakephp/src/Utility/Inflector.php index 9cb8c1f44..8abcd079d 100644 --- a/app/vendor/cakephp/cakephp/src/Utility/Inflector.php +++ b/app/vendor/cakephp/cakephp/src/Utility/Inflector.php @@ -464,7 +464,7 @@ public static function delimit(string $string, string $delimiter = '_'): string } /** - * Returns corresponding table name for given model $className. ("people" for the model class "Person"). + * Returns corresponding table name for given model $className. ("people" for the class name "Person"). * * @param string $className Name of class to get database table name for * @return string Name of the database table for given class @@ -483,7 +483,7 @@ public static function tableize(string $className): string } /** - * Returns Cake model class name ("Person" for the database table "people".) for given database table. + * Returns a singular, CamelCase inflection for given database table. ("Person" for the table name "people") * * @param string $tableName Name of database table to get class name for * @return string Class name diff --git a/app/vendor/cakephp/cakephp/src/View/Helper/FormHelper.php b/app/vendor/cakephp/cakephp/src/View/Helper/FormHelper.php index f67ee04ec..77bc7451c 100644 --- a/app/vendor/cakephp/cakephp/src/View/Helper/FormHelper.php +++ b/app/vendor/cakephp/cakephp/src/View/Helper/FormHelper.php @@ -1396,9 +1396,13 @@ protected function setRequiredAndCustomValidity(string $fieldName, array $option $options['templateVars']['customValidityMessage'] = $message; if ($this->getConfig('autoSetCustomValidity')) { + $condition = 'this.value'; + if ($options['type'] === 'checkbox') { + $condition = 'this.checked'; + } $options['data-validity-message'] = $message; $options['oninvalid'] = "this.setCustomValidity(''); " - . 'if (!this.value) this.setCustomValidity(this.dataset.validityMessage)'; + . "if (!{$condition}) this.setCustomValidity(this.dataset.validityMessage)"; $options['oninput'] = "this.setCustomValidity('')"; } } diff --git a/app/vendor/cakephp/cakephp/src/View/Helper/NumberHelper.php b/app/vendor/cakephp/cakephp/src/View/Helper/NumberHelper.php index 77a13233a..c20d19b4c 100644 --- a/app/vendor/cakephp/cakephp/src/View/Helper/NumberHelper.php +++ b/app/vendor/cakephp/cakephp/src/View/Helper/NumberHelper.php @@ -182,6 +182,8 @@ public function format($number, array $options = []): string * - `zero` - The text to use for zero values, can be a string or a number. e.g. 0, 'Free!' * - `places` - Number of decimal places to use. e.g. 2 * - `precision` - Maximum Number of decimal places to use, e.g. 2 + * - `roundingMode` - Rounding mode to use. e.g. NumberFormatter::ROUND_HALF_UP. + * When not set locale default will be used * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00 * - `useIntlCode` - Whether to replace the currency symbol with the international * currency code. diff --git a/app/vendor/cakephp/cakephp/tests/PHPStan/PhpDoc/TableAssociationTypeNodeResolverExtension.php b/app/vendor/cakephp/cakephp/tests/PHPStan/PhpDoc/TableAssociationTypeNodeResolverExtension.php new file mode 100644 index 000000000..82e41bd3a --- /dev/null +++ b/app/vendor/cakephp/cakephp/tests/PHPStan/PhpDoc/TableAssociationTypeNodeResolverExtension.php @@ -0,0 +1,87 @@ +` + * + * The type `\Cake\ORM\Association\BelongsTo&\App\Model\Table\UsersTable` is considered invalid (NeverType) by PHPStan + */ +class TableAssociationTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension +{ + private TypeNodeResolver $typeNodeResolver; + + /** + * @var array + */ + protected array $associationTypes = [ + BelongsTo::class, + BelongsToMany::class, + HasMany::class, + HasOne::class, + Association::class, + ]; + + /** + * @param \PHPStan\PhpDoc\TypeNodeResolver $typeNodeResolver + * @return void + */ + public function setTypeNodeResolver(TypeNodeResolver $typeNodeResolver): void + { + $this->typeNodeResolver = $typeNodeResolver; + } + + /** + * @param \PHPStan\PhpDocParser\Ast\Type\TypeNode $typeNode + * @param \PHPStan\Analyser\NameScope $nameScope + * @return \PHPStan\Type\Type|null + */ + public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type + { + if (!$typeNode instanceof IntersectionTypeNode) { + return null; + } + $types = $this->typeNodeResolver->resolveMultiple($typeNode->types, $nameScope); + $config = [ + 'association' => null, + 'table' => null, + ]; + foreach ($types as $type) { + if (!$type instanceof ObjectType) { + continue; + } + $className = $type->getClassName(); + if ($config['association'] === null && in_array($className, $this->associationTypes)) { + $config['association'] = $type; + } elseif ($config['table'] === null && str_ends_with($className, 'Table')) { + $config['table'] = $type; + } + } + if ($config['table'] && $config['association']) { + return new GenericObjectType( + $config['association']->getClassName(), + [$config['table']] + ); + } + + return null; + } +} diff --git a/app/vendor/cakephp/chronos/src/ChronosInterface.php b/app/vendor/cakephp/chronos/src/ChronosInterface.php index 923831689..c5f329db4 100644 --- a/app/vendor/cakephp/chronos/src/ChronosInterface.php +++ b/app/vendor/cakephp/chronos/src/ChronosInterface.php @@ -1245,7 +1245,7 @@ public function endOfWeek(): self; * to indicate the desired dayOfWeek, ex. static::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function next(?int $dayOfWeek = null); @@ -1256,7 +1256,7 @@ public function next(?int $dayOfWeek = null); * to indicate the desired dayOfWeek, ex. static::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function previous(?int $dayOfWeek = null); @@ -1267,7 +1267,7 @@ public function previous(?int $dayOfWeek = null); * to indicate the desired dayOfWeek, ex. static::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function firstOfMonth(?int $dayOfWeek = null); @@ -1278,7 +1278,7 @@ public function firstOfMonth(?int $dayOfWeek = null); * to indicate the desired dayOfWeek, ex. static::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function lastOfMonth(?int $dayOfWeek = null); @@ -1290,7 +1290,7 @@ public function lastOfMonth(?int $dayOfWeek = null); * * @param int $nth The offset to use. * @param int $dayOfWeek The day of the week to move to. - * @return mixed + * @return static|false */ public function nthOfMonth(int $nth, int $dayOfWeek); @@ -1301,7 +1301,7 @@ public function nthOfMonth(int $nth, int $dayOfWeek); * to indicate the desired dayOfWeek, ex. static::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function firstOfQuarter(?int $dayOfWeek = null); @@ -1312,7 +1312,7 @@ public function firstOfQuarter(?int $dayOfWeek = null); * to indicate the desired dayOfWeek, ex. static::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function lastOfQuarter(?int $dayOfWeek = null); @@ -1324,7 +1324,7 @@ public function lastOfQuarter(?int $dayOfWeek = null); * * @param int $nth The offset to use. * @param int $dayOfWeek The day of the week to move to. - * @return mixed + * @return static|false */ public function nthOfQuarter(int $nth, int $dayOfWeek); @@ -1335,7 +1335,7 @@ public function nthOfQuarter(int $nth, int $dayOfWeek); * to indicate the desired dayOfWeek, ex. static::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function firstOfYear(?int $dayOfWeek = null); @@ -1346,7 +1346,7 @@ public function firstOfYear(?int $dayOfWeek = null); * to indicate the desired dayOfWeek, ex. static::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function lastOfYear(?int $dayOfWeek = null); @@ -1358,7 +1358,7 @@ public function lastOfYear(?int $dayOfWeek = null); * * @param int $nth The offset to use. * @param int $dayOfWeek The day of the week to move to. - * @return mixed + * @return static|false */ public function nthOfYear(int $nth, int $dayOfWeek); diff --git a/app/vendor/cakephp/chronos/src/Traits/ComparisonTrait.php b/app/vendor/cakephp/chronos/src/Traits/ComparisonTrait.php index 677663a7c..35a7ce613 100644 --- a/app/vendor/cakephp/chronos/src/Traits/ComparisonTrait.php +++ b/app/vendor/cakephp/chronos/src/Traits/ComparisonTrait.php @@ -196,7 +196,7 @@ public function lessThan(ChronosInterface $dt) */ public function lte(ChronosInterface $dt): bool { - trigger_error('2.5 lte() is deprecated. Use lessthanOrEquals() instead.', E_USER_DEPRECATED); + trigger_error('2.5 lte() is deprecated. Use lessThanOrEquals() instead.', E_USER_DEPRECATED); return $this->lessThanOrEquals($dt); } diff --git a/app/vendor/cakephp/chronos/src/Traits/ModifierTrait.php b/app/vendor/cakephp/chronos/src/Traits/ModifierTrait.php index 13a3c8acb..7fa95e469 100644 --- a/app/vendor/cakephp/chronos/src/Traits/ModifierTrait.php +++ b/app/vendor/cakephp/chronos/src/Traits/ModifierTrait.php @@ -935,7 +935,7 @@ public function endOfDay(bool $microseconds = false): ChronosInterface public function startOfMonth(): ChronosInterface { if (static::class === ChronosDate::class) { - trigger_error('2.5 startOfMonth() will be removed in 3.x.', E_USER_DEPRECATED); + trigger_error('2.5 startOfMonth() will be removed in 3.x. Use firstOfMonth() instead.', E_USER_DEPRECATED); } return $this->modify('first day of this month midnight'); @@ -949,7 +949,7 @@ public function startOfMonth(): ChronosInterface public function endOfMonth(): ChronosInterface { if (static::class === ChronosDate::class) { - trigger_error('2.5 endOfMonth() will be removed in 3.x.', E_USER_DEPRECATED); + trigger_error('2.5 endOfMonth() will be removed in 3.x. Use lastOfMonth() instead.', E_USER_DEPRECATED); } return $this->modify('last day of this month, 23:59:59'); @@ -963,7 +963,7 @@ public function endOfMonth(): ChronosInterface public function startOfYear(): ChronosInterface { if (static::class === ChronosDate::class) { - trigger_error('2.5 startOfYear() will be removed in 3.x.', E_USER_DEPRECATED); + trigger_error('2.5 startOfYear() will be removed in 3.x. Use firstOfYear() instead.', E_USER_DEPRECATED); } return $this->modify('first day of january midnight'); @@ -977,7 +977,7 @@ public function startOfYear(): ChronosInterface public function endOfYear(): ChronosInterface { if (static::class === ChronosDate::class) { - trigger_error('2.5 endOfYear() will be removed in 3.x.', E_USER_DEPRECATED); + trigger_error('2.5 endOfYear() will be removed in 3.x. Use lastOfYear() instead.', E_USER_DEPRECATED); } return $this->modify('last day of december, 23:59:59'); @@ -1051,6 +1051,9 @@ public function startOfWeek(): ChronosInterface if ($dt->dayOfWeek !== static::$weekStartsAt) { $dt = $dt->previous(static::$weekStartsAt); } + if ($dt instanceof ChronosDate) { + return $dt; + } return $dt->startOfDay(); } @@ -1066,6 +1069,9 @@ public function endOfWeek(): ChronosInterface if ($dt->dayOfWeek !== static::$weekEndsAt) { $dt = $dt->next(static::$weekEndsAt); } + if ($dt instanceof ChronosDate) { + return $dt; + } return $dt->endOfDay(); } @@ -1077,7 +1083,7 @@ public function endOfWeek(): ChronosInterface * to indicate the desired dayOfWeek, ex. ChronosInterface::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function next(?int $dayOfWeek = null) { @@ -1097,7 +1103,7 @@ public function next(?int $dayOfWeek = null) * to indicate the desired dayOfWeek, ex. ChronosInterface::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function previous(?int $dayOfWeek = null) { @@ -1117,7 +1123,7 @@ public function previous(?int $dayOfWeek = null) * to indicate the desired dayOfWeek, ex. ChronosInterface::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function firstOfMonth(?int $dayOfWeek = null) { @@ -1133,7 +1139,7 @@ public function firstOfMonth(?int $dayOfWeek = null) * to indicate the desired dayOfWeek, ex. ChronosInterface::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function lastOfMonth(?int $dayOfWeek = null) { @@ -1150,7 +1156,7 @@ public function lastOfMonth(?int $dayOfWeek = null) * * @param int $nth The offset to use. * @param int $dayOfWeek The day of the week to move to. - * @return mixed + * @return static|false */ public function nthOfMonth(int $nth, int $dayOfWeek) { @@ -1168,7 +1174,7 @@ public function nthOfMonth(int $nth, int $dayOfWeek) * to indicate the desired dayOfWeek, ex. ChronosInterface::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function firstOfQuarter(?int $dayOfWeek = null) { @@ -1185,7 +1191,7 @@ public function firstOfQuarter(?int $dayOfWeek = null) * to indicate the desired dayOfWeek, ex. ChronosInterface::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function lastOfQuarter(?int $dayOfWeek = null) { @@ -1203,7 +1209,7 @@ public function lastOfQuarter(?int $dayOfWeek = null) * * @param int $nth The offset to use. * @param int $dayOfWeek The day of the week to move to. - * @return mixed + * @return static|false */ public function nthOfQuarter(int $nth, int $dayOfWeek) { @@ -1222,7 +1228,7 @@ public function nthOfQuarter(int $nth, int $dayOfWeek) * to indicate the desired dayOfWeek, ex. ChronosInterface::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function firstOfYear(?int $dayOfWeek = null) { @@ -1238,7 +1244,7 @@ public function firstOfYear(?int $dayOfWeek = null) * to indicate the desired dayOfWeek, ex. ChronosInterface::MONDAY. * * @param int|null $dayOfWeek The day of the week to move to. - * @return mixed + * @return static */ public function lastOfYear(?int $dayOfWeek = null) { @@ -1255,7 +1261,7 @@ public function lastOfYear(?int $dayOfWeek = null) * * @param int $nth The offset to use. * @param int $dayOfWeek The day of the week to move to. - * @return mixed + * @return static|false */ public function nthOfYear(int $nth, int $dayOfWeek) { diff --git a/app/vendor/composer/ClassLoader.php b/app/vendor/composer/ClassLoader.php index 7824d8f7e..a72151c77 100644 --- a/app/vendor/composer/ClassLoader.php +++ b/app/vendor/composer/ClassLoader.php @@ -45,34 +45,35 @@ class ClassLoader /** @var \Closure(string):void */ private static $includeFile; - /** @var string|null */ + /** @var ?string */ private $vendorDir; // PSR-4 /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixLengthsPsr4 = array(); /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixDirsPsr4 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr4 = array(); // PSR-0 /** - * List of PSR-0 prefixes - * - * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) - * - * @var array>> + * @var array[] + * @psalm-var array> */ private $prefixesPsr0 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr0 = array(); @@ -80,7 +81,8 @@ class ClassLoader private $useIncludePath = false; /** - * @var array + * @var string[] + * @psalm-var array */ private $classMap = array(); @@ -88,20 +90,21 @@ class ClassLoader private $classMapAuthoritative = false; /** - * @var array + * @var bool[] + * @psalm-var array */ private $missingClasses = array(); - /** @var string|null */ + /** @var ?string */ private $apcuPrefix; /** - * @var array + * @var self[] */ private static $registeredLoaders = array(); /** - * @param string|null $vendorDir + * @param ?string $vendorDir */ public function __construct($vendorDir = null) { @@ -110,7 +113,7 @@ public function __construct($vendorDir = null) } /** - * @return array> + * @return string[] */ public function getPrefixes() { @@ -122,7 +125,8 @@ public function getPrefixes() } /** - * @return array> + * @return array[] + * @psalm-return array> */ public function getPrefixesPsr4() { @@ -130,7 +134,8 @@ public function getPrefixesPsr4() } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirs() { @@ -138,7 +143,8 @@ public function getFallbackDirs() } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirsPsr4() { @@ -146,7 +152,8 @@ public function getFallbackDirsPsr4() } /** - * @return array Array of classname => path + * @return string[] Array of classname => path + * @psalm-return array */ public function getClassMap() { @@ -154,7 +161,8 @@ public function getClassMap() } /** - * @param array $classMap Class to filename map + * @param string[] $classMap Class to filename map + * @psalm-param array $classMap * * @return void */ @@ -171,25 +179,24 @@ public function addClassMap(array $classMap) * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, - $paths + (array) $paths ); } @@ -198,19 +205,19 @@ public function add($prefix, $paths, $prepend = false) $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = $paths; + $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], - $paths + (array) $paths ); } } @@ -219,9 +226,9 @@ public function add($prefix, $paths, $prepend = false) * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * @@ -229,18 +236,17 @@ public function add($prefix, $paths, $prepend = false) */ public function addPsr4($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, - $paths + (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { @@ -250,18 +256,18 @@ public function addPsr4($prefix, $paths, $prepend = false) throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = $paths; + $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], - $paths + (array) $paths ); } } @@ -270,8 +276,8 @@ public function addPsr4($prefix, $paths, $prepend = false) * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 base directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories * * @return void */ @@ -288,8 +294,8 @@ public function set($prefix, $paths) * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * @@ -475,9 +481,9 @@ public function findFile($class) } /** - * Returns the currently registered loaders keyed by their corresponding vendor directories. + * Returns the currently registered loaders indexed by their corresponding vendor directories. * - * @return array + * @return self[] */ public static function getRegisteredLoaders() { diff --git a/app/vendor/composer/InstalledVersions.php b/app/vendor/composer/InstalledVersions.php index 51e734a77..c6b54af7b 100644 --- a/app/vendor/composer/InstalledVersions.php +++ b/app/vendor/composer/InstalledVersions.php @@ -98,7 +98,7 @@ public static function isInstalled($packageName, $includeDevRequirements = true) { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { - return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); } } @@ -119,7 +119,7 @@ public static function isInstalled($packageName, $includeDevRequirements = true) */ public static function satisfies(VersionParser $parser, $packageName, $constraint) { - $constraint = $parser->parseConstraints((string) $constraint); + $constraint = $parser->parseConstraints($constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); @@ -328,9 +328,7 @@ private static function getInstalled() if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { - /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ - $required = require $vendorDir.'/composer/installed.php'; - $installed[] = self::$installedByVendor[$vendorDir] = $required; + $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { self::$installed = $installed[count($installed) - 1]; } @@ -342,17 +340,12 @@ private static function getInstalled() // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { - /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ - $required = require __DIR__ . '/installed.php'; - self::$installed = $required; + self::$installed = require __DIR__ . '/installed.php'; } else { self::$installed = array(); } } - - if (self::$installed !== array()) { - $installed[] = self::$installed; - } + $installed[] = self::$installed; return $installed; } diff --git a/app/vendor/composer/autoload_files.php b/app/vendor/composer/autoload_files.php index 557f97f35..a3f9ae503 100644 --- a/app/vendor/composer/autoload_files.php +++ b/app/vendor/composer/autoload_files.php @@ -8,8 +8,11 @@ return array( '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', - 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', + '89efb1254ef2d1c5d80096acd12c4098' => $vendorDir . '/twig/twig/src/Resources/core.php', + 'ffecb95d45175fd40f75be8a23b34f90' => $vendorDir . '/twig/twig/src/Resources/debug.php', + 'c7baa00073ee9c61edf148c51917cfb4' => $vendorDir . '/twig/twig/src/Resources/escaper.php', + 'f844ccf1d25df8663951193c3fc307c8' => $vendorDir . '/twig/twig/src/Resources/string_loader.php', '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', '34122c0574b76bf21c9a8db62b5b9cf3' => $vendorDir . '/cakephp/chronos/src/carbon_compat.php', @@ -38,6 +41,7 @@ 'b1fc73705e1bec51cd2b20a32cf1c60a' => $vendorDir . '/cakephp/cakephp/src/Utility/bootstrap.php', 'ad155f8f1cf0d418fe49e248db8c661b' => $vendorDir . '/react/promise/src/functions_include.php', '0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php', + 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php', '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', diff --git a/app/vendor/composer/autoload_psr4.php b/app/vendor/composer/autoload_psr4.php index 53fe05ada..db3323307 100644 --- a/app/vendor/composer/autoload_psr4.php +++ b/app/vendor/composer/autoload_psr4.php @@ -8,6 +8,8 @@ return array( 'Twig\\Extra\\Markdown\\' => array($vendorDir . '/twig/markdown-extra'), 'Twig\\' => array($vendorDir . '/twig/twig/src'), + 'TestPlugin\\Test\\' => array($baseDir . '/availableplugins/TestPlugin/tests'), + 'TestPlugin\\' => array($baseDir . '/availableplugins/TestPlugin/src'), 'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'), 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), 'Symfony\\Polyfill\\Php73\\' => array($vendorDir . '/symfony/polyfill-php73'), @@ -69,6 +71,8 @@ 'DebugKit\\' => array($vendorDir . '/cakephp/debug_kit/src'), 'CoreServer\\Test\\' => array($baseDir . '/plugins/CoreServer/tests'), 'CoreServer\\' => array($baseDir . '/plugins/CoreServer/src'), + 'CoreReport\\Test\\' => array($baseDir . '/plugins/CoreReport/tests'), + 'CoreReport\\' => array($baseDir . '/plugins/CoreReport/src'), 'CoreJob\\Test\\' => array($baseDir . '/plugins/CoreJob/tests'), 'CoreJob\\' => array($baseDir . '/plugins/CoreJob/src'), 'CoreEnroller\\Test\\' => array($baseDir . '/plugins/CoreEnroller/tests'), diff --git a/app/vendor/composer/autoload_static.php b/app/vendor/composer/autoload_static.php index 3fed33eae..b639386fc 100644 --- a/app/vendor/composer/autoload_static.php +++ b/app/vendor/composer/autoload_static.php @@ -9,8 +9,11 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e public static $files = array ( '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', - 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', + '89efb1254ef2d1c5d80096acd12c4098' => __DIR__ . '/..' . '/twig/twig/src/Resources/core.php', + 'ffecb95d45175fd40f75be8a23b34f90' => __DIR__ . '/..' . '/twig/twig/src/Resources/debug.php', + 'c7baa00073ee9c61edf148c51917cfb4' => __DIR__ . '/..' . '/twig/twig/src/Resources/escaper.php', + 'f844ccf1d25df8663951193c3fc307c8' => __DIR__ . '/..' . '/twig/twig/src/Resources/string_loader.php', '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', '34122c0574b76bf21c9a8db62b5b9cf3' => __DIR__ . '/..' . '/cakephp/chronos/src/carbon_compat.php', @@ -39,6 +42,7 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e 'b1fc73705e1bec51cd2b20a32cf1c60a' => __DIR__ . '/..' . '/cakephp/cakephp/src/Utility/bootstrap.php', 'ad155f8f1cf0d418fe49e248db8c661b' => __DIR__ . '/..' . '/react/promise/src/functions_include.php', '0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php', + 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php', '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', @@ -51,6 +55,8 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e array ( 'Twig\\Extra\\Markdown\\' => 20, 'Twig\\' => 5, + 'TestPlugin\\Test\\' => 16, + 'TestPlugin\\' => 11, ), 'S' => array ( @@ -142,6 +148,8 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e array ( 'CoreServer\\Test\\' => 16, 'CoreServer\\' => 11, + 'CoreReport\\Test\\' => 16, + 'CoreReport\\' => 11, 'CoreJob\\Test\\' => 13, 'CoreJob\\' => 8, 'CoreEnroller\\Test\\' => 18, @@ -186,6 +194,14 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e array ( 0 => __DIR__ . '/..' . '/twig/twig/src', ), + 'TestPlugin\\Test\\' => + array ( + 0 => __DIR__ . '/../..' . '/availableplugins/TestPlugin/tests', + ), + 'TestPlugin\\' => + array ( + 0 => __DIR__ . '/../..' . '/availableplugins/TestPlugin/src', + ), 'Symfony\\Polyfill\\Php81\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-php81', @@ -433,6 +449,14 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e array ( 0 => __DIR__ . '/../..' . '/plugins/CoreServer/src', ), + 'CoreReport\\Test\\' => + array ( + 0 => __DIR__ . '/../..' . '/plugins/CoreReport/tests', + ), + 'CoreReport\\' => + array ( + 0 => __DIR__ . '/../..' . '/plugins/CoreReport/src', + ), 'CoreJob\\Test\\' => array ( 0 => __DIR__ . '/../..' . '/plugins/CoreJob/tests', diff --git a/app/vendor/composer/ca-bundle/composer.json b/app/vendor/composer/ca-bundle/composer.json index c85480adf..c2ce2bb8e 100644 --- a/app/vendor/composer/ca-bundle/composer.json +++ b/app/vendor/composer/ca-bundle/composer.json @@ -24,13 +24,13 @@ "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.2 || ^5", - "phpstan/phpstan": "^0.12.55", - "psr/log": "^1.0", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^8 || ^9", + "phpstan/phpstan": "^1.10", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "autoload": { "psr-4": { @@ -48,7 +48,7 @@ } }, "scripts": { - "test": "SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1 vendor/bin/simple-phpunit", - "phpstan": "vendor/bin/phpstan analyse" + "test": "@php phpunit", + "phpstan": "@php phpstan analyse" } } diff --git a/app/vendor/composer/ca-bundle/res/cacert.pem b/app/vendor/composer/ca-bundle/res/cacert.pem index d8fda7d1a..584af3c0b 100644 --- a/app/vendor/composer/ca-bundle/res/cacert.pem +++ b/app/vendor/composer/ca-bundle/res/cacert.pem @@ -1,7 +1,9 @@ ## ## Bundle of CA Root Certificates ## -## Certificate data from Mozilla as of: Tue Dec 12 04:12:04 2023 GMT +## Certificate data from Mozilla as of: Tue Feb 25 04:12:03 2025 GMT +## +## Find updated versions here: https://curl.se/docs/caextract.html ## ## This is a bundle of X.509 certificates of public Certificate Authorities ## (CA). These were automatically extracted from Mozilla's root certificates @@ -14,7 +16,7 @@ ## Just configure this file as the SSLCACertificateFile. ## ## Conversion done with mk-ca-bundle.pl version 1.29. -## SHA256: 1970dd65858925d68498d2356aea6d03f764422523c5887deca8ce3ba9e1f845 +## SHA256: 620fd89c02acb0019f1899dab7907db5d20735904f5a9a0d3a8771a5857ac482 ## @@ -369,37 +371,6 @@ NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ -----END CERTIFICATE----- -SwissSign Silver CA - G2 -======================== ------BEGIN CERTIFICATE----- -MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT -BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X -DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3 -aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG -9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644 -N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm -+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH -6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu -MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h -qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5 -FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs -ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc -celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X -CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB -tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 -cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P -4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F -kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L -3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx -/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa -DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP -e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu -WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ -DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub -DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u ------END CERTIFICATE----- - SecureTrust CA ============== -----BEGIN CERTIFICATE----- @@ -582,27 +553,6 @@ NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= -----END CERTIFICATE----- -SecureSign RootCA11 -=================== ------BEGIN CERTIFICATE----- -MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi -SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS -b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw -KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1 -cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL -TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO -wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq -g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP -O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA -bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX -t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh -OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r -bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ -Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01 -y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061 -lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I= ------END CERTIFICATE----- - Microsec e-Szigno Root CA 2009 ============================== -----BEGIN CERTIFICATE----- @@ -2317,40 +2267,6 @@ hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB dBb9HxEGmpv0 -----END CERTIFICATE----- -Entrust Root Certification Authority - G4 -========================================= ------BEGIN CERTIFICATE----- -MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAwgb4xCzAJBgNV -BAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3Qu -bmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1 -dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1 -dGhvcml0eSAtIEc0MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYT -AlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 -L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhv -cml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhv -cml0eSAtIEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3D -umSXbcr3DbVZwbPLqGgZ2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV -3imz/f3ET+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j5pds -8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAMC1rlLAHGVK/XqsEQ -e9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73TDtTUXm6Hnmo9RR3RXRv06QqsYJn7 -ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNXwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5X -xNMhIWNlUpEbsZmOeX7m640A2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV -7rtNOzK+mndmnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 -dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwlN4y6mACXi0mW -Hv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNjc0kCAwEAAaNCMEAwDwYDVR0T -AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9n -MA0GCSqGSIb3DQEBCwUAA4ICAQAS5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4Q -jbRaZIxowLByQzTSGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht -7LGrhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/B7NTeLUK -YvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uIAeV8KEsD+UmDfLJ/fOPt -jqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbwH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+ -m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKW -RGhXxNUzzxkvFMSUHHuk2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjA -JOgc47OlIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk5F6G -+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuYn/PIjhs4ViFqUZPT -kcpG2om3PVODLAgfi49T3f+sHw== ------END CERTIFICATE----- - Microsoft ECC Root Certificate Authority 2017 ============================================= -----BEGIN CERTIFICATE----- @@ -3168,36 +3084,6 @@ AwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozmut6Dacpps6kFtZaSF4fC0urQe87YQVt8 rgIwRt7qy12a7DLCZRawTDBcMPPaTnOGBtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR -----END CERTIFICATE----- -Security Communication RootCA3 -============================== ------BEGIN CERTIFICATE----- -MIIFfzCCA2egAwIBAgIJAOF8N0D9G/5nMA0GCSqGSIb3DQEBDAUAMF0xCzAJBgNVBAYTAkpQMSUw -IwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMScwJQYDVQQDEx5TZWN1cml0eSBD -b21tdW5pY2F0aW9uIFJvb3RDQTMwHhcNMTYwNjE2MDYxNzE2WhcNMzgwMTE4MDYxNzE2WjBdMQsw -CQYDVQQGEwJKUDElMCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UE -AxMeU2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A -MIICCgKCAgEA48lySfcw3gl8qUCBWNO0Ot26YQ+TUG5pPDXC7ltzkBtnTCHsXzW7OT4rCmDvu20r -hvtxosis5FaU+cmvsXLUIKx00rgVrVH+hXShuRD+BYD5UpOzQD11EKzAlrenfna84xtSGc4RHwsE -NPXY9Wk8d/Nk9A2qhd7gCVAEF5aEt8iKvE1y/By7z/MGTfmfZPd+pmaGNXHIEYBMwXFAWB6+oHP2 -/D5Q4eAvJj1+XCO1eXDe+uDRpdYMQXF79+qMHIjH7Iv10S9VlkZ8WjtYO/u62C21Jdp6Ts9EriGm -npjKIG58u4iFW/vAEGK78vknR+/RiTlDxN/e4UG/VHMgly1s2vPUB6PmudhvrvyMGS7TZ2crldtY -XLVqAvO4g160a75BflcJdURQVc1aEWEhCmHCqYj9E7wtiS/NYeCVvsq1e+F7NGcLH7YMx3weGVPK -p7FKFSBWFHA9K4IsD50VHUeAR/94mQ4xr28+j+2GaR57GIgUssL8gjMunEst+3A7caoreyYn8xrC -3PsXuKHqy6C0rtOUfnrQq8PsOC0RLoi/1D+tEjtCrI8Cbn3M0V9hvqG8OmpI6iZVIhZdXw3/JzOf -GAN0iltSIEdrRU0id4xVJ/CvHozJgyJUt5rQT9nO/NkuHJYosQLTA70lUhw0Zk8jq/R3gpYd0Vcw -CBEF/VfR2ccCAwEAAaNCMEAwHQYDVR0OBBYEFGQUfPxYchamCik0FW8qy7z8r6irMA4GA1UdDwEB -/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQDcAiMI4u8hOscNtybS -YpOnpSNyByCCYN8Y11StaSWSntkUz5m5UoHPrmyKO1o5yGwBQ8IibQLwYs1OY0PAFNr0Y/Dq9HHu -Tofjcan0yVflLl8cebsjqodEV+m9NU1Bu0soo5iyG9kLFwfl9+qd9XbXv8S2gVj/yP9kaWJ5rW4O -H3/uHWnlt3Jxs/6lATWUVCvAUm2PVcTJ0rjLyjQIUYWg9by0F1jqClx6vWPGOi//lkkZhOpn2ASx -YfQAW0q3nHE3GYV5v4GwxxMOdnE+OoAGrgYWp421wsTL/0ClXI2lyTrtcoHKXJg80jQDdwj98ClZ -XSEIx2C/pHF7uNkegr4Jr2VvKKu/S7XuPghHJ6APbw+LP6yVGPO5DtxnVW5inkYO0QR4ynKudtml -+LLfiAlhi+8kTtFZP1rUPcmTPCtk9YENFpb3ksP+MW/oKjJ0DvRMmEoYDjBU1cXrvMUVnuiZIesn -KwkK2/HmcBhWuwzkvvnoEKQTkrgc4NtnHVMDpCKn3F2SEDzq//wbEBrD2NCcnWXL0CsnMQMeNuE9 -dnUM/0Umud1RvCPHX9jYhxBAEg09ODfnRDwYwFMJZI//1ZqmfHAuc1Uh6N//g7kdPjIe1qZ9LPFm -6Vwdp6POXiUyK+OVrCoHzrQoeIY8LaadTdJ0MN1kURXbg4NR16/9M51NZg== ------END CERTIFICATE----- - Security Communication ECC RootCA1 ================================== -----BEGIN CERTIFICATE----- @@ -3532,3 +3418,225 @@ dVwPaFsdZcJfMw8eD/A7hvWwTruc9+olBdytoptLFwG+Qt81IR2tq670v64fG9PiO/yzcnMcmyiQ iRM9HcEARwmWmjgb3bHPDcK0RPOWlc4yOo80nOAXx17Org3bhzjlP1v9mxnhMUF6cKojawHhRUzN lM47ni3niAIi9G7oyOzWPPO5std3eqx7 -----END CERTIFICATE----- + +Telekom Security TLS ECC Root 2020 +================================== +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQswCQYDVQQGEwJE +RTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMSswKQYDVQQDDCJUZWxl +a29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIwMB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIz +NTk1OVowYzELMAkGA1UEBhMCREUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkg +R21iSDErMCkGA1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqG +SM49AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/OtdKPD/M1 +2kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDPf8iAC8GXs7s1J8nCG6NC +MEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6fMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMAoGCCqGSM49BAMDA2cAMGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZ +Mo7k+5Dck2TOrbRBR2Diz6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdU +ga/sf+Rn27iQ7t0l +-----END CERTIFICATE----- + +Telekom Security TLS RSA Root 2023 +================================== +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBjMQswCQYDVQQG +EwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMSswKQYDVQQDDCJU +ZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAyMDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMy +NzIzNTk1OVowYzELMAkGA1UEBhMCREUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJp +dHkgR21iSDErMCkGA1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9cUD/h3VC +KSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHVcp6R+SPWcHu79ZvB7JPP +GeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMAU6DksquDOFczJZSfvkgdmOGjup5czQRx +UX11eKvzWarE4GC+j4NSuHUaQTXtvPM6Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWo +l8hHD/BeEIvnHRz+sTugBTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9 +FIS3R/qy8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73Jco4v +zLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg8qKrBC7m8kwOFjQg +rIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8rFEz0ciD0cmfHdRHNCk+y7AO+oML +KFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7S +WWO/gLCMk3PLNaaZlSJhZQNg+y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUtqeXgj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQpGv7qHBFfLp+ +sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm9S3ul0A8Yute1hTWjOKWi0Fp +kzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErwM807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy +/SKE8YXJN3nptT+/XOR0so8RYgDdGGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4 +mZqTuXNnQkYRIer+CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtz +aL1txKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+w6jv/naa +oqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aKL4x35bcF7DvB7L6Gs4a8 +wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+ljX273CXE2whJdV/LItM3z7gLfEdxquVeE +HVlNjM7IDiPCtyaaEBRx/pOyiriA8A4QntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0 +o82bNSQ3+pCTE4FCxpgmdTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +FIRMAPROFESIONAL CA ROOT-A WEB +============================== +-----BEGIN CERTIFICATE----- +MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQswCQYDVQQGEwJF +UzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UEYQwPVkFURVMtQTYyNjM0MDY4 +MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENBIFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2 +WhcNNDcwMzMxMDkwMTM2WjBuMQswCQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25h +bCBTQTEYMBYGA1UEYQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFM +IENBIFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zfe9MEkVz6 +iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6CcyvHZpsKjECcfIr28jlg +st7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FD +Y1w8ndYn81LsF7Kpryz3dvgwHQYDVR0OBBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB +/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgL +cFBTApFwhVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dGXSaQ +pYXFuXqUPoeovQA= +-----END CERTIFICATE----- + +TWCA CYBER Root CA +================== +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQMQswCQYDVQQG +EwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NB +IENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQG +EwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NB +IENZQkVSIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1s +Ts6P40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxFavcokPFh +V8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/34bKS1PE2Y2yHer43CdT +o0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684iJkXXYJndzk834H/nY62wuFm40AZoNWDT +Nq5xQwTxaWV4fPMf88oon1oglWa0zbfuj3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK +/c/WMw+f+5eesRycnupfXtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkH +IuNZW0CP2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDAS9TM +fAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDAoS/xUgXJP+92ZuJF +2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzCkHDXShi8fgGwsOsVHkQGzaRP6AzR +wyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83 +QOGt4A1WNzAdBgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0ttGlTITVX1olN +c79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn68xDiBaiA9a5F/gZbG0jAn/x +X9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNnTKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDR +IG4kqIQnoVesqlVYL9zZyvpoBJ7tRCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq +/p1hvIbZv97Tujqxf36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0R +FxbIQh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz8ppy6rBe +Pm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4NxKfKjLji7gh7MMrZQzv +It6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzXxeSDwWrruoBa3lwtcHb4yOWHh8qgnaHl +IhInD0Q9HWzq1MKLL295q39QpsQZp6F6t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +SecureSign Root CA12 +==================== +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBMdGQuMR0wGwYDVQQDExRT +ZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgwNTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJ +BgNVBAYTAkpQMSMwIQYDVQQKExpDeWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMU +U2VjdXJlU2lnbiBSb290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3 +emhFKxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mtp7JIKwcc +J/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zdJ1M3s6oYwlkm7Fsf0uZl +fO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gurFzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBF +EaCeVESE99g2zvVQR9wsMJvuwPWW0v4JhscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1Uef +NzFJM3IFTQy2VYzxV4+Kh9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsFAAOC +AQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6LdmmQOmFxv3Y67ilQi +LUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJmBClnW8Zt7vPemVV2zfrPIpyMpce +mik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPS +vWKErI4cqc1avTc7bgoitPQV55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhga +aaI5gdka9at/yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== +-----END CERTIFICATE----- + +SecureSign Root CA14 +==================== +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEMBQAwUTELMAkG +A1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBMdGQuMR0wGwYDVQQDExRT +ZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgwNzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJ +BgNVBAYTAkpQMSMwIQYDVQQKExpDeWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMU +U2VjdXJlU2lnbiBSb290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh +1oq/FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOgvlIfX8xn +bacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy6pJxaeQp8E+BgQQ8sqVb +1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa +/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9JkdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOE +kJTRX45zGRBdAuVwpcAQ0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSx +jVIHvXiby8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac18iz +ju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs0Wq2XSqypWa9a4X0 +dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIABSMbHdPTGrMNASRZhdCyvjG817XsY +AFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVLApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeq +YR3r6/wtbyPk86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ibed87hwriZLoA +ymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopTzfFP7ELyk+OZpDc8h7hi2/Ds +Hzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHSDCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPG +FrojutzdfhrGe0K22VoF3Jpf1d+42kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6q +nsb58Nn4DSEC5MUoFlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/ +OfVyK4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6dB7h7sxa +OgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtlLor6CZpO2oYofaphNdgO +pygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB365jJ6UeTo3cKXhZ+PmhIIynJkBugnLN +eLLIjzwec+fBH7/PzqUqm9tEZDKgu39cJRNItX+S +-----END CERTIFICATE----- + +SecureSign Root CA15 +==================== +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMwUTELMAkGA1UE +BhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBMdGQuMR0wGwYDVQQDExRTZWN1 +cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMyNTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNV +BAYTAkpQMSMwIQYDVQQKExpDeWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2Vj +dXJlU2lnbiBSb290IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5G +dCx4wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSRZHX+AezB +2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT9DAKBggqhkjOPQQDAwNoADBlAjEA2S6J +fl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJ +SwdLZrWeqrqgHkHZAXQ6bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- + +D-TRUST BR Root CA 2 2023 +========================= +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBIMQswCQYDVQQG +EwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEJSIFJvb3QgQ0Eg +MiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUwOTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTAT +BgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCT +cfKri3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNEgXtRr90z +sWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8k12b9py0i4a6Ibn08OhZ +WiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCTRphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6 +++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LUL +QyReS2tNZ9/WtT5PeB+UcSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIv +x9gvdhFP/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bSuREV +MweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+0bpwHJwh5Q8xaRfX +/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4NDfTisl01gLmB1IRpkQLLddCNxbU9 +CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUZ5Dw1t61GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRC +MEAwPqA8oDqGOGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tIFoE9c+CeJyrr +d6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67nriv6uvw8l5VAk1/DLQOj7aRv +U9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTRVFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4 +nj8+AybmTNudX0KEPUUDAxxZiMrcLmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdij +YQ6qgYF/6FKC0ULn4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff +/vtDhQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsGkoHU6XCP +pz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46ls/pdu4D58JDUjxqgejB +WoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aSEcr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/ +5usWDiJFAbzdNpQ0qTUmiteXue4Icr80knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jt +n/mtd+ArY0+ew+43u3gJhJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +D-TRUST EV Root CA 2 2023 +========================= +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBIMQswCQYDVQQG +EwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEVWIFJvb3QgQ0Eg +MiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUwOTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTAT +BgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1 +sJkKF8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE7CUXFId/ +MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFeEMbsh2aJgWi6zCudR3Mf +vc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6lHPTGGkKSv/BAQP/eX+1SH977ugpbzZM +lWGG2Pmic4ruri+W7mjNPU0oQvlFKzIbRlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3 +YG14C8qKXO0elg6DpkiVjTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq910 +7PncjLgcjmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZxTnXo +nMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ARZZaBhDM7DS3LAa +QzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nkhbDhezGdpn9yo7nELC7MmVcOIQxF +AZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knFNXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUqvyREBuHkV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRC +MEAwPqA8oDqGOGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14QvBukEdHjqOS +Mo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4pZt+UPJ26oUFKidBK7GB0aL2 +QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xD +UmPBEcrCRbH0O1P1aa4846XerOhUt7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V +4U/M5d40VxDJI3IXcI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuo +dNv8ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT2vFp4LJi +TZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs7dpn1mKmS00PaaLJvOwi +S5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNPgofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/ +HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAstNl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L ++KIkBI3Y4WNeApI02phhXBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- diff --git a/app/vendor/composer/ca-bundle/src/CaBundle.php b/app/vendor/composer/ca-bundle/src/CaBundle.php index ecd4d5f42..2d6b48c56 100644 --- a/app/vendor/composer/ca-bundle/src/CaBundle.php +++ b/app/vendor/composer/ca-bundle/src/CaBundle.php @@ -24,8 +24,6 @@ class CaBundle private static $caPath; /** @var array */ private static $caFileValidity = array(); - /** @var bool|null */ - private static $useOpensslParse; /** * Returns the system CA bundle path, or a path to the bundled one @@ -64,7 +62,7 @@ class CaBundle * @param LoggerInterface $logger optional logger for information about which CA files were loaded * @return string path to a CA bundle file or directory */ - public static function getSystemCaRootBundlePath(LoggerInterface $logger = null) + public static function getSystemCaRootBundlePath(?LoggerInterface $logger = null) { if (self::$caPath !== null) { return self::$caPath; @@ -86,23 +84,19 @@ public static function getSystemCaRootBundlePath(LoggerInterface $logger = null) '/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package) '/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package) '/etc/ssl/ca-bundle.pem', // SUSE, openSUSE (ca-certificates package) - '/usr/local/share/certs/ca-root-nss.crt', // FreeBSD (ca_root_nss_package) '/usr/ssl/certs/ca-bundle.crt', // Cygwin '/opt/local/share/curl/curl-ca-bundle.crt', // OS X macports, curl-ca-bundle package '/usr/local/share/curl/curl-ca-bundle.crt', // Default cURL CA bunde path (without --with-ca-bundle option) '/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat? '/etc/ssl/cert.pem', // OpenBSD - '/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x '/usr/local/etc/openssl/cert.pem', // OS X homebrew, openssl package '/usr/local/etc/openssl@1.1/cert.pem', // OS X homebrew, openssl@1.1 package '/opt/homebrew/etc/openssl@3/cert.pem', // macOS silicon homebrew, openssl@3 package '/opt/homebrew/etc/openssl@1.1/cert.pem', // macOS silicon homebrew, openssl@1.1 package + '/etc/pki/tls/certs', + '/etc/ssl/certs', // FreeBSD ); - foreach($otherLocations as $location) { - $otherLocations[] = dirname($location); - } - $caBundlePaths = array_merge($caBundlePaths, $otherLocations); foreach ($caBundlePaths as $caBundle) { @@ -160,7 +154,7 @@ public static function getBundledCaBundlePath() * * @return bool */ - public static function validateCaFile($filename, LoggerInterface $logger = null) + public static function validateCaFile($filename, ?LoggerInterface $logger = null) { static $warned = false; @@ -170,19 +164,7 @@ public static function validateCaFile($filename, LoggerInterface $logger = null) $contents = file_get_contents($filename); - // assume the CA is valid if php is vulnerable to - // https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html - if (!static::isOpensslParseSafe()) { - if (!$warned && $logger) { - $logger->warning(sprintf( - 'Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.', - PHP_VERSION - )); - $warned = true; - } - - $isValid = !empty($contents); - } elseif (is_string($contents) && strlen($contents) > 0) { + if (is_string($contents) && strlen($contents) > 0) { $contents = preg_replace("/^(\\-+(?:BEGIN|END))\\s+TRUSTED\\s+(CERTIFICATE\\-+)\$/m", '$1 $2', $contents); if (null === $contents) { // regex extraction failed @@ -211,100 +193,7 @@ public static function validateCaFile($filename, LoggerInterface $logger = null) */ public static function isOpensslParseSafe() { - if (null !== self::$useOpensslParse) { - return self::$useOpensslParse; - } - - if (PHP_VERSION_ID >= 50600) { - return self::$useOpensslParse = true; - } - - // Vulnerable: - // PHP 5.3.0 - PHP 5.3.27 - // PHP 5.4.0 - PHP 5.4.22 - // PHP 5.5.0 - PHP 5.5.6 - if ( - (PHP_VERSION_ID < 50400 && PHP_VERSION_ID >= 50328) - || (PHP_VERSION_ID < 50500 && PHP_VERSION_ID >= 50423) - || PHP_VERSION_ID >= 50507 - ) { - // This version of PHP has the fix for CVE-2013-6420 applied. - return self::$useOpensslParse = true; - } - - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - // Windows is probably insecure in this case. - return self::$useOpensslParse = false; - } - - $compareDistroVersionPrefix = function ($prefix, $fixedVersion) { - $regex = '{^'.preg_quote($prefix).'([0-9]+)$}'; - - if (preg_match($regex, PHP_VERSION, $m)) { - return ((int) $m[1]) >= $fixedVersion; - } - - return false; - }; - - // Hard coded list of PHP distributions with the fix backported. - if ( - $compareDistroVersionPrefix('5.3.3-7+squeeze', 18) // Debian 6 (Squeeze) - || $compareDistroVersionPrefix('5.4.4-14+deb7u', 7) // Debian 7 (Wheezy) - || $compareDistroVersionPrefix('5.3.10-1ubuntu3.', 9) // Ubuntu 12.04 (Precise) - ) { - return self::$useOpensslParse = true; - } - - // Symfony Process component is missing so we assume it is unsafe at this point - if (!class_exists('Symfony\Component\Process\PhpProcess')) { - return self::$useOpensslParse = false; - } - - // This is where things get crazy, because distros backport security - // fixes the chances are on NIX systems the fix has been applied but - // it's not possible to verify that from the PHP version. - // - // To verify exec a new PHP process and run the issue testcase with - // known safe input that replicates the bug. - - // Based on testcase in https://github.com/php/php-src/commit/c1224573c773b6845e83505f717fbf820fc18415 - // changes in https://github.com/php/php-src/commit/76a7fd893b7d6101300cc656058704a73254d593 - $cert = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVwRENDQTR5Z0F3SUJBZ0lKQUp6dThyNnU2ZUJjTUEwR0NTcUdTSWIzRFFFQkJRVUFNSUhETVFzd0NRWUQKVlFRR0V3SkVSVEVjTUJvR0ExVUVDQXdUVG05eVpISm9aV2x1TFZkbGMzUm1ZV3hsYmpFUU1BNEdBMVVFQnd3SApTOE9Ed3Jac2JqRVVNQklHQTFVRUNnd0xVMlZyZEdsdmJrVnBibk14SHpBZEJnTlZCQXNNRmsxaGJHbGphVzkxCmN5QkRaWEowSUZObFkzUnBiMjR4SVRBZkJnTlZCQU1NR0cxaGJHbGphVzkxY3k1elpXdDBhVzl1WldsdWN5NWsKWlRFcU1DZ0dDU3FHU0liM0RRRUpBUlliYzNSbFptRnVMbVZ6YzJWeVFITmxhM1JwYjI1bGFXNXpMbVJsTUhVWQpaREU1TnpBd01UQXhNREF3TURBd1dnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBCkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEKQUFBQUFBQVhEVEUwTVRFeU9ERXhNemt6TlZvd2djTXhDekFKQmdOVkJBWVRBa1JGTVJ3d0dnWURWUVFJREJOTwpiM0prY21obGFXNHRWMlZ6ZEdaaGJHVnVNUkF3RGdZRFZRUUhEQWRMdzRQQ3RteHVNUlF3RWdZRFZRUUtEQXRUClpXdDBhVzl1UldsdWN6RWZNQjBHQTFVRUN3d1dUV0ZzYVdOcGIzVnpJRU5sY25RZ1UyVmpkR2x2YmpFaE1COEcKQTFVRUF3d1liV0ZzYVdOcGIzVnpMbk5sYTNScGIyNWxhVzV6TG1SbE1Tb3dLQVlKS29aSWh2Y05BUWtCRmh0egpkR1ZtWVc0dVpYTnpaWEpBYzJWcmRHbHZibVZwYm5NdVpHVXdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCCkR3QXdnZ0VLQW9JQkFRRERBZjNobDdKWTBYY0ZuaXlFSnBTU0RxbjBPcUJyNlFQNjV1c0pQUnQvOFBhRG9xQnUKd0VZVC9OYSs2ZnNnUGpDMHVLOURaZ1dnMnRIV1dvYW5TYmxBTW96NVBINlorUzRTSFJaN2UyZERJalBqZGhqaAowbUxnMlVNTzV5cDBWNzk3R2dzOWxOdDZKUmZIODFNTjJvYlhXczROdHp0TE11RDZlZ3FwcjhkRGJyMzRhT3M4CnBrZHVpNVVhd1Raa3N5NXBMUEhxNWNNaEZHbTA2djY1Q0xvMFYyUGQ5K0tBb2tQclBjTjVLTEtlYno3bUxwazYKU01lRVhPS1A0aWRFcXh5UTdPN2ZCdUhNZWRzUWh1K3ByWTNzaTNCVXlLZlF0UDVDWm5YMmJwMHdLSHhYMTJEWAoxbmZGSXQ5RGJHdkhUY3lPdU4rblpMUEJtM3ZXeG50eUlJdlZBZ01CQUFHalFqQkFNQWtHQTFVZEV3UUNNQUF3CkVRWUpZSVpJQVliNFFnRUJCQVFEQWdlQU1Bc0dBMVVkRHdRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFHMGZaWVlDVGJkajFYWWMrMVNub2FQUit2SThDOENhRAo4KzBVWWhkbnlVNGdnYTBCQWNEclk5ZTk0ZUVBdTZacXljRjZGakxxWFhkQWJvcHBXb2NyNlQ2R0QxeDMzQ2tsClZBcnpHL0t4UW9oR0QySmVxa2hJTWxEb214SE83a2EzOStPYThpMnZXTFZ5alU4QVp2V01BcnVIYTRFRU55RzcKbFcyQWFnYUZLRkNyOVRuWFRmcmR4R1ZFYnY3S1ZRNmJkaGc1cDVTanBXSDErTXEwM3VSM1pYUEJZZHlWODMxOQpvMGxWajFLRkkyRENML2xpV2lzSlJvb2YrMWNSMzVDdGQwd1lCY3BCNlRac2xNY09QbDc2ZHdLd0pnZUpvMlFnClpzZm1jMnZDMS9xT2xOdU5xLzBUenprVkd2OEVUVDNDZ2FVK1VYZTRYT1Z2a2NjZWJKbjJkZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'; - $script = <<<'EOT' - -error_reporting(-1); -$info = openssl_x509_parse(base64_decode('%s')); -var_dump(PHP_VERSION, $info['issuer']['emailAddress'], $info['validFrom_time_t']); - -EOT; - $script = '<'."?php\n".sprintf($script, $cert); - - try { - $process = new PhpProcess($script); - $process->mustRun(); - } catch (\Exception $e) { - // In the case of any exceptions just accept it is not possible to - // determine the safety of openssl_x509_parse and bail out. - return self::$useOpensslParse = false; - } - - $output = preg_split('{\r?\n}', trim($process->getOutput())); - $errorOutput = trim($process->getErrorOutput()); - - if ( - is_array($output) - && count($output) === 3 - && $output[0] === sprintf('string(%d) "%s"', strlen(PHP_VERSION), PHP_VERSION) - && $output[1] === 'string(27) "stefan.esser@sektioneins.de"' - && $output[2] === 'int(-1)' - && preg_match('{openssl_x509_parse\(\): illegal (?:ASN1 data type for|length in) timestamp in - on line \d+}', $errorOutput) - ) { - // This PHP has the fix backported probably by a distro security team. - return self::$useOpensslParse = true; - } - - return self::$useOpensslParse = false; + return true; } /** @@ -315,7 +204,6 @@ public static function reset() { self::$caFileValidity = array(); self::$caPath = null; - self::$useOpensslParse = null; } /** @@ -340,12 +228,12 @@ private static function getEnvVariable($name) * @param LoggerInterface|null $logger * @return bool */ - private static function caFileUsable($certFile, LoggerInterface $logger = null) + private static function caFileUsable($certFile, ?LoggerInterface $logger = null) { return $certFile - && static::isFile($certFile, $logger) - && static::isReadable($certFile, $logger) - && static::validateCaFile($certFile, $logger); + && self::isFile($certFile, $logger) + && self::isReadable($certFile, $logger) + && self::validateCaFile($certFile, $logger); } /** @@ -353,12 +241,12 @@ private static function caFileUsable($certFile, LoggerInterface $logger = null) * @param LoggerInterface|null $logger * @return bool */ - private static function caDirUsable($certDir, LoggerInterface $logger = null) + private static function caDirUsable($certDir, ?LoggerInterface $logger = null) { return $certDir - && static::isDir($certDir, $logger) - && static::isReadable($certDir, $logger) - && static::glob($certDir . '/*', $logger); + && self::isDir($certDir, $logger) + && self::isReadable($certDir, $logger) + && self::glob($certDir . '/*', $logger); } /** @@ -366,7 +254,7 @@ private static function caDirUsable($certDir, LoggerInterface $logger = null) * @param LoggerInterface|null $logger * @return bool */ - private static function isFile($certFile, LoggerInterface $logger = null) + private static function isFile($certFile, ?LoggerInterface $logger = null) { $isFile = @is_file($certFile); if (!$isFile && $logger) { @@ -381,7 +269,7 @@ private static function isFile($certFile, LoggerInterface $logger = null) * @param LoggerInterface|null $logger * @return bool */ - private static function isDir($certDir, LoggerInterface $logger = null) + private static function isDir($certDir, ?LoggerInterface $logger = null) { $isDir = @is_dir($certDir); if (!$isDir && $logger) { @@ -396,7 +284,7 @@ private static function isDir($certDir, LoggerInterface $logger = null) * @param LoggerInterface|null $logger * @return bool */ - private static function isReadable($certFileOrDir, LoggerInterface $logger = null) + private static function isReadable($certFileOrDir, ?LoggerInterface $logger = null) { $isReadable = @is_readable($certFileOrDir); if (!$isReadable && $logger) { @@ -411,7 +299,7 @@ private static function isReadable($certFileOrDir, LoggerInterface $logger = nul * @param LoggerInterface|null $logger * @return bool */ - private static function glob($pattern, LoggerInterface $logger = null) + private static function glob($pattern, ?LoggerInterface $logger = null) { $certs = glob($pattern); if ($certs === false) { diff --git a/app/vendor/composer/composer/composer.lock b/app/vendor/composer/composer/composer.lock index 04c822b48..2f1cd7693 100644 --- a/app/vendor/composer/composer/composer.lock +++ b/app/vendor/composer/composer/composer.lock @@ -8,28 +8,28 @@ "packages": [ { "name": "composer/ca-bundle", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "b66d11b7479109ab547f9405b97205640b17d385" + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/b66d11b7479109ab547f9405b97205640b17d385", - "reference": "b66d11b7479109ab547f9405b97205640b17d385", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", + "phpstan/phpstan": "^1.10", "psr/log": "^1.0", "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { @@ -64,7 +64,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.4.0" + "source": "https://github.com/composer/ca-bundle/tree/1.5.0" }, "funding": [ { @@ -80,20 +80,20 @@ "type": "tidelift" } ], - "time": "2023-12-18T12:05:55+00:00" + "time": "2024-03-15T14:00:32+00:00" }, { "name": "composer/class-map-generator", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9" + "reference": "8286a62d243312ed99b3eee20d5005c961adb311" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/953cc4ea32e0c31f2185549c7d216d7921f03da9", - "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8286a62d243312ed99b3eee20d5005c961adb311", + "reference": "8286a62d243312ed99b3eee20d5005c961adb311", "shasum": "" }, "require": { @@ -137,7 +137,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.1.0" + "source": "https://github.com/composer/class-map-generator/tree/1.1.1" }, "funding": [ { @@ -153,7 +153,7 @@ "type": "tidelift" } ], - "time": "2023-06-30T13:58:57+00:00" + "time": "2024-03-15T12:53:41+00:00" }, { "name": "composer/metadata-minifier", @@ -226,16 +226,16 @@ }, { "name": "composer/pcre", - "version": "2.1.1", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b439557066cd445732fa57cbc8d905394b4db8a0" + "reference": "540af382c97b83c628227d5f87cf56466d476191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b439557066cd445732fa57cbc8d905394b4db8a0", - "reference": "b439557066cd445732fa57cbc8d905394b4db8a0", + "url": "https://api.github.com/repos/composer/pcre/zipball/540af382c97b83c628227d5f87cf56466d476191", + "reference": "540af382c97b83c628227d5f87cf56466d476191", "shasum": "" }, "require": { @@ -277,7 +277,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/2.1.1" + "source": "https://github.com/composer/pcre/tree/2.1.3" }, "funding": [ { @@ -293,7 +293,7 @@ "type": "tidelift" } ], - "time": "2023-10-11T07:10:55+00:00" + "time": "2024-03-19T09:03:05+00:00" }, { "name": "composer/semver", @@ -458,16 +458,16 @@ }, { "name": "composer/xdebug-handler", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ced299686f41dce890debac69273b47ffe98a40c" + "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", - "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255", "shasum": "" }, "require": { @@ -478,7 +478,7 @@ "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^6.0" + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -502,9 +502,9 @@ "performance" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.4" }, "funding": [ { @@ -520,7 +520,7 @@ "type": "tidelift" } ], - "time": "2022-02-25T21:32:43+00:00" + "time": "2024-03-26T18:29:49+00:00" }, { "name": "justinrainbow/json-schema", @@ -938,16 +938,16 @@ }, { "name": "symfony/console", - "version": "v5.4.35", + "version": "v5.4.36", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931" + "reference": "39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/dbdf6adcb88d5f83790e1efb57ef4074309d3931", - "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931", + "url": "https://api.github.com/repos/symfony/console/zipball/39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e", + "reference": "39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e", "shasum": "" }, "require": { @@ -1017,7 +1017,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.35" + "source": "https://github.com/symfony/console/tree/v5.4.36" }, "funding": [ { @@ -1033,20 +1033,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:28:09+00:00" + "time": "2024-02-20T16:33:57+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.2", + "version": "v2.5.3", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + "reference": "80d075412b557d41002320b96a096ca65aa2c98d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", + "reference": "80d075412b557d41002320b96a096ca65aa2c98d", "shasum": "" }, "require": { @@ -1084,7 +1084,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" }, "funding": [ { @@ -1100,20 +1100,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2023-01-24T14:02:46+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.35", + "version": "v5.4.38", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "5a553607d4ffbfa9c0ab62facadea296c9db7086" + "reference": "899330a01056077271e2f614c7b28b0379a671eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/5a553607d4ffbfa9c0ab62facadea296c9db7086", - "reference": "5a553607d4ffbfa9c0ab62facadea296c9db7086", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/899330a01056077271e2f614c7b28b0379a671eb", + "reference": "899330a01056077271e2f614c7b28b0379a671eb", "shasum": "" }, "require": { @@ -1148,7 +1148,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.35" + "source": "https://github.com/symfony/filesystem/tree/v5.4.38" }, "funding": [ { @@ -1164,7 +1164,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-03-21T08:05:07+00:00" }, { "name": "symfony/finder", @@ -1781,16 +1781,16 @@ }, { "name": "symfony/process", - "version": "v5.4.35", + "version": "v5.4.36", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "cbc28e34015ad50166fc2f9c8962d28d0fe861eb" + "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/cbc28e34015ad50166fc2f9c8962d28d0fe861eb", - "reference": "cbc28e34015ad50166fc2f9c8962d28d0fe861eb", + "url": "https://api.github.com/repos/symfony/process/zipball/4fdf34004f149cc20b2f51d7d119aa500caad975", + "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975", "shasum": "" }, "require": { @@ -1823,7 +1823,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.35" + "source": "https://github.com/symfony/process/tree/v5.4.36" }, "funding": [ { @@ -1839,20 +1839,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-02-12T15:49:53+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.2", + "version": "v2.5.3", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" + "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a2329596ddc8fd568900e3fc76cba42489ecc7f3", + "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3", "shasum": "" }, "require": { @@ -1906,7 +1906,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.3" }, "funding": [ { @@ -1922,20 +1922,20 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:17:29+00:00" + "time": "2023-04-21T15:04:16+00:00" }, { "name": "symfony/string", - "version": "v5.4.35", + "version": "v5.4.36", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2" + "reference": "4e232c83622bd8cd32b794216aa29d0d266d353b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/c209c4d0559acce1c9a2067612cfb5d35756edc2", - "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2", + "url": "https://api.github.com/repos/symfony/string/zipball/4e232c83622bd8cd32b794216aa29d0d266d353b", + "reference": "4e232c83622bd8cd32b794216aa29d0d266d353b", "shasum": "" }, "require": { @@ -1992,7 +1992,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.35" + "source": "https://github.com/symfony/string/tree/v5.4.36" }, "funding": [ { @@ -2008,22 +2008,22 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-02-01T08:49:30+00:00" } ], "packages-dev": [ { "name": "phpstan/phpstan", - "version": "1.10.57", + "version": "1.10.67", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e" + "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1627b1d03446904aaa77593f370c5201d2ecc34e", - "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493", + "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493", "shasum": "" }, "require": { @@ -2066,13 +2066,9 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2024-01-24T11:51:34+00:00" + "time": "2024-04-16T07:22:02+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -2124,16 +2120,16 @@ }, { "name": "phpstan/phpstan-phpunit", - "version": "1.3.15", + "version": "1.3.16", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a" + "reference": "d5242a59d035e46774f2e634b374bc39ff62cb95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", - "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/d5242a59d035e46774f2e634b374bc39ff62cb95", + "reference": "d5242a59d035e46774f2e634b374bc39ff62cb95", "shasum": "" }, "require": { @@ -2170,27 +2166,27 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.15" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.16" }, - "time": "2023-10-09T18:58:39+00:00" + "time": "2024-02-23T09:51:20+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.5.2", + "version": "1.5.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542" + "reference": "2e193a07651a6f4be3baa44ddb21d822681f5918" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/7a50e9662ee9f3942e4aaaf3d603653f60282542", - "reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/2e193a07651a6f4be3baa44ddb21d822681f5918", + "reference": "2e193a07651a6f4be3baa44ddb21d822681f5918", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.34" + "phpstan/phpstan": "^1.10.60" }, "require-dev": { "nikic/php-parser": "^4.13.0", @@ -2219,28 +2215,28 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.2" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.5" }, - "time": "2023-10-30T14:35:06+00:00" + "time": "2024-04-19T15:12:26+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "1.3.7", + "version": "1.3.12", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "ef7db637be9b85fa00278fc3477ac66abe8eb7d1" + "reference": "f4b9407fa3203aebafd422ae8f0eb1ef94659a80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/ef7db637be9b85fa00278fc3477ac66abe8eb7d1", - "reference": "ef7db637be9b85fa00278fc3477ac66abe8eb7d1", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/f4b9407fa3203aebafd422ae8f0eb1ef94659a80", + "reference": "f4b9407fa3203aebafd422ae8f0eb1ef94659a80", "shasum": "" }, "require": { "ext-simplexml": "*", "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.36" + "phpstan/phpstan": "^1.10.62" }, "conflict": { "symfony/framework-bundle": "<3.0" @@ -2291,22 +2287,22 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.7" + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.12" }, - "time": "2024-01-10T21:54:42+00:00" + "time": "2024-04-14T13:30:23+00:00" }, { "name": "symfony/phpunit-bridge", - "version": "v7.0.3", + "version": "v7.0.6", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "0a2eeb0d9e68bf01660e3e903f8113508bb46132" + "reference": "a014167aa1f66cb9990675840da65609d3e61612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/0a2eeb0d9e68bf01660e3e903f8113508bb46132", - "reference": "0a2eeb0d9e68bf01660e3e903f8113508bb46132", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/a014167aa1f66cb9990675840da65609d3e61612", + "reference": "a014167aa1f66cb9990675840da65609d3e61612", "shasum": "" }, "require": { @@ -2358,7 +2354,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v7.0.3" + "source": "https://github.com/symfony/phpunit-bridge/tree/v7.0.6" }, "funding": [ { @@ -2374,7 +2370,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T15:02:46+00:00" + "time": "2024-03-19T11:57:22+00:00" } ], "aliases": [], diff --git a/app/vendor/composer/composer/res/composer-schema.json b/app/vendor/composer/composer/res/composer-schema.json index a77becb4b..90714875c 100644 --- a/app/vendor/composer/composer/res/composer-schema.json +++ b/app/vendor/composer/composer/res/composer-schema.json @@ -288,6 +288,52 @@ } } }, + "php-ext": { + "type": "object", + "description": "Settings for PHP extension packages.", + "properties": { + "extension-name": { + "type": "string", + "description": "If specified, this will be used as the name of the extension, where needed by tooling. If this is not specified, the extension name will be derived from the Composer package name (e.g. `vendor/name` would become `ext-name`). The extension name may be specified with or without the `ext-` prefix, and tools that use this must normalise this appropriately.", + "example": "ext-xdebug" + }, + "priority": { + "type": "integer", + "description": "This is used to add a prefix to the INI file, e.g. `90-xdebug.ini` which affects the loading order. The priority is a number in the range 10-99 inclusive, with 10 being the highest priority (i.e. will be processed first), and 99 being the lowest priority (i.e. will be processed last). There are two digits so that the files sort correctly on any platform, whether the sorting is natural or not.", + "minimum": 10, + "maximum": 99, + "example": 80, + "default": 80 + }, + "support-zts": { + "type": "boolean", + "description": "Does this package support Zend Thread Safety", + "example": false, + "default": true + }, + "configure-options": { + "type": "array", + "description": "These configure options make up the flags that can be passed to ./configure when installing the extension.", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "The name of the flag, this would typically be prefixed with `--`, for example, the value 'the-flag' would be passed as `./configure --the-flag`.", + "example": "without-xdebug-compression", + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9-_]*$" + }, + "description": { + "type": "string", + "description": "The description of what the flag does or means.", + "example": "Disable compression through zlib" + } + } + } + } + } + }, "config": { "type": "object", "description": "Composer options.", diff --git a/app/vendor/composer/composer/src/Composer/Advisory/Auditor.php b/app/vendor/composer/composer/src/Composer/Advisory/Auditor.php index f0dc76ae5..38d827dfe 100644 --- a/app/vendor/composer/composer/src/Composer/Advisory/Auditor.php +++ b/app/vendor/composer/composer/src/Composer/Advisory/Auditor.php @@ -264,6 +264,10 @@ private function outputAdvisoriesTable(ConsoleIO $io, array $advisories): void $advisory->affectedVersions->getPrettyString(), $advisory->reportedAt->format(DATE_ATOM), ]; + if ($advisory->cve === null) { + $headers[] = 'Advisory ID'; + $row[] = $advisory->advisoryId; + } if ($advisory instanceof IgnoredSecurityAdvisory) { $headers[] = 'Ignore reason'; $row[] = $advisory->ignoreReason ?? 'None specified'; @@ -294,6 +298,9 @@ private function outputAdvisoriesPlain(IOInterface $io, array $advisories): void $error[] = "Package: ".$advisory->packageName; $error[] = "Severity: ".$this->getSeverity($advisory); $error[] = "CVE: ".$this->getCVE($advisory); + if ($advisory->cve === null) { + $error[] = "Advisory ID: ".$advisory->advisoryId; + } $error[] = "Title: ".OutputFormatter::escape($advisory->title); $error[] = "URL: ".$this->getURL($advisory); $error[] = "Affected versions: ".OutputFormatter::escape($advisory->affectedVersions->getPrettyString()); diff --git a/app/vendor/composer/composer/src/Composer/Command/BaseCommand.php b/app/vendor/composer/composer/src/Composer/Command/BaseCommand.php index b6b1439de..bdfbb0d05 100644 --- a/app/vendor/composer/composer/src/Composer/Command/BaseCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/BaseCommand.php @@ -289,6 +289,29 @@ protected function initialize(InputInterface $input, OutputInterface $output) parent::initialize($input, $output); } + /** + * Calls {@see Factory::create()} with the given arguments, taking into account flags and default states for disabling scripts and plugins + * + * @param mixed $config either a configuration array or a filename to read from, if null it will read from + * the default filename + * @return Composer + */ + protected function createComposerInstance(InputInterface $input, IOInterface $io, $config = null, ?bool $disablePlugins = null, ?bool $disableScripts = null): Composer + { + $disablePlugins = $disablePlugins === true || $input->hasParameterOption('--no-plugins'); + $disableScripts = $disableScripts === true || $input->hasParameterOption('--no-scripts'); + + $application = parent::getApplication(); + if ($application instanceof Application && $application->getDisablePluginsByDefault()) { + $disablePlugins = true; + } + if ($application instanceof Application && $application->getDisableScriptsByDefault()) { + $disableScripts = true; + } + + return Factory::create($io, $config, $disablePlugins, $disableScripts); + } + /** * Returns preferSource and preferDist values based on the configuration. * diff --git a/app/vendor/composer/composer/src/Composer/Command/ClearCacheCommand.php b/app/vendor/composer/composer/src/Composer/Command/ClearCacheCommand.php index 028aa4212..77ed517a8 100644 --- a/app/vendor/composer/composer/src/Composer/Command/ClearCacheCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/ClearCacheCommand.php @@ -45,7 +45,13 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $config = Factory::createConfig(); + $composer = $this->tryComposer(); + if ($composer !== null) { + $config = $composer->getConfig(); + } else { + $config = Factory::createConfig(); + } + $io = $this->getIO(); $cachePaths = [ diff --git a/app/vendor/composer/composer/src/Composer/Command/ConfigCommand.php b/app/vendor/composer/composer/src/Composer/Command/ConfigCommand.php index cbdc174bc..b625cbf1a 100644 --- a/app/vendor/composer/composer/src/Composer/Command/ConfigCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/ConfigCommand.php @@ -771,8 +771,12 @@ static function ($vals) { foreach ($bits as $bit) { $currentValue = $currentValue[$bit] ?? null; } - if (is_array($currentValue)) { - $value = array_merge($currentValue, $value); + if (is_array($currentValue) && is_array($value)) { + if (array_is_list($currentValue) && array_is_list($value)) { + $value = array_merge($currentValue, $value); + } else { + $value = $value + $currentValue; + } } } } diff --git a/app/vendor/composer/composer/src/Composer/Command/CreateProjectCommand.php b/app/vendor/composer/composer/src/Composer/Command/CreateProjectCommand.php index f0bb7dbb5..b7e873684 100644 --- a/app/vendor/composer/composer/src/Composer/Command/CreateProjectCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/CreateProjectCommand.php @@ -193,7 +193,7 @@ public function installProject(IOInterface $io, Config $config, InputInterface $ $this->suggestedPackagesReporter = new SuggestedPackagesReporter($io); if ($packageName !== null) { - $installedFromVcs = $this->installRootPackage($io, $config, $packageName, $platformRequirementFilter, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repositories, $disablePlugins, $disableScripts, $noProgress, $secureHttp); + $installedFromVcs = $this->installRootPackage($input, $io, $config, $packageName, $platformRequirementFilter, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repositories, $disablePlugins, $disableScripts, $noProgress, $secureHttp); } else { $installedFromVcs = false; } @@ -202,7 +202,7 @@ public function installProject(IOInterface $io, Config $config, InputInterface $ unlink('composer.lock'); } - $composer = Factory::create($io, null, $disablePlugins, $disableScripts); + $composer = $this->createComposerInstance($input, $io, null, $disablePlugins, $disableScripts); // add the repository to the composer.json and use it for the install run later if ($repositories !== null && $addRepository) { @@ -221,7 +221,7 @@ public function installProject(IOInterface $io, Config $config, InputInterface $ $configSource->addRepository($name, $repoConfig, false); } - $composer = Factory::create($io, null, $disablePlugins); + $composer = $this->createComposerInstance($input, $io, null, $disablePlugins); } } @@ -336,7 +336,7 @@ public function installProject(IOInterface $io, Config $config, InputInterface $ * * @throws \Exception */ - protected function installRootPackage(IOInterface $io, Config $config, string $packageName, PlatformRequirementFilterInterface $platformRequirementFilter, ?string $directory = null, ?string $packageVersion = null, ?string $stability = 'stable', bool $preferSource = false, bool $preferDist = false, bool $installDevPackages = false, ?array $repositories = null, bool $disablePlugins = false, bool $disableScripts = false, bool $noProgress = false, bool $secureHttp = true): bool + protected function installRootPackage(InputInterface $input, IOInterface $io, Config $config, string $packageName, PlatformRequirementFilterInterface $platformRequirementFilter, ?string $directory = null, ?string $packageVersion = null, ?string $stability = 'stable', bool $preferSource = false, bool $preferDist = false, bool $installDevPackages = false, ?array $repositories = null, bool $disablePlugins = false, bool $disableScripts = false, bool $noProgress = false, bool $secureHttp = true): bool { if (!$secureHttp) { $config->merge(['config' => ['secure-http' => false]], Config::SOURCE_COMMAND); @@ -388,7 +388,7 @@ protected function installRootPackage(IOInterface $io, Config $config, string $p throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); } - $composer = Factory::create($io, $config->all(), $disablePlugins, $disableScripts); + $composer = $this->createComposerInstance($input, $io, $config->all(), $disablePlugins, $disableScripts); $config = $composer->getConfig(); $rm = $composer->getRepositoryManager(); diff --git a/app/vendor/composer/composer/src/Composer/Command/DiagnoseCommand.php b/app/vendor/composer/composer/src/Composer/Command/DiagnoseCommand.php index fc594f7d9..fa5e9aa56 100644 --- a/app/vendor/composer/composer/src/Composer/Command/DiagnoseCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/DiagnoseCommand.php @@ -44,6 +44,8 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\ExecutableFinder; +use Composer\Util\Http\ProxyManager; +use Composer\Util\Http\RequestProxy; /** * @author Jordi Boggiano @@ -115,10 +117,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->write('Checking https connectivity to packagist: ', false); $this->outputResult($this->checkHttp('https', $config)); - $opts = stream_context_get_options(StreamContextFactory::getContext('http://example.org')); - if (!empty($opts['http']['proxy'])) { + $proxyManager = ProxyManager::getInstance(); + $protos = $config->get('disable-tls') === true ? ['http'] : ['http', 'https']; + try { + foreach ($protos as $proto) { + $proxy = $proxyManager->getProxyForRequest($proto.'://repo.packagist.org'); + if ($proxy->getStatus() !== '') { + $type = $proxy->isSecure() ? 'HTTPS' : 'HTTP'; + $io->write('Checking '.$type.' proxy with '.$proto.': ', false); + $this->outputResult($this->checkHttpProxy($proxy, $proto)); + } + } + } catch (TransportException $e) { $io->write('Checking HTTP proxy: ', false); - $this->outputResult($this->checkHttpProxy()); + $status = $this->checkConnectivityAndComposerNetworkHttpEnablement(); + $this->outputResult(is_string($status) ? $status : $e); } if (count($oauth = $config->get('github-oauth')) > 0) { @@ -186,7 +199,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $io->write('OpenSSL version: ' . (defined('OPENSSL_VERSION_TEXT') ? ''.OPENSSL_VERSION_TEXT.'' : 'missing')); - $io->write('cURL version: ' . $this->getCurlVersion()); + $io->write('curl version: ' . $this->getCurlVersion()); $finder = new ExecutableFinder; $hasSystemUnzip = (bool) $finder->find('unzip'); @@ -298,31 +311,38 @@ private function checkHttp(string $proto, Config $config) } /** - * @return string|true|\Exception + * @return string|\Exception */ - private function checkHttpProxy() + private function checkHttpProxy(RequestProxy $proxy, string $protocol) { $result = $this->checkConnectivityAndComposerNetworkHttpEnablement(); if ($result !== true) { return $result; } - $protocol = extension_loaded('openssl') ? 'https' : 'http'; try { - $json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->decodeJson(); - $hash = reset($json['provider-includes']); - $hash = $hash['sha256']; - $path = str_replace('%hash%', $hash, key($json['provider-includes'])); - $provider = $this->httpDownloader->get($protocol . '://repo.packagist.org/'.$path)->getBody(); - - if (hash('sha256', $provider) !== $hash) { - return 'It seems that your proxy is modifying http traffic on the fly'; + $proxyStatus = $proxy->getStatus(); + + if ($proxy->isExcludedByNoProxy()) { + return 'SKIP Because repo.packagist.org is '.$proxyStatus.''; } + + $json = $this->httpDownloader->get($protocol.'://repo.packagist.org/packages.json')->decodeJson(); + if (isset($json['provider-includes'])) { + $hash = reset($json['provider-includes']); + $hash = $hash['sha256']; + $path = str_replace('%hash%', $hash, key($json['provider-includes'])); + $provider = $this->httpDownloader->get($protocol.'://repo.packagist.org/'.$path)->getBody(); + + if (hash('sha256', $provider) !== $hash) { + return 'It seems that your proxy ('.$proxyStatus.') is modifying '.$protocol.' traffic on the fly'; + } + } + + return 'OK '.$proxyStatus.''; } catch (\Exception $e) { return $e; } - - return true; } /** diff --git a/app/vendor/composer/composer/src/Composer/Command/GlobalCommand.php b/app/vendor/composer/composer/src/Composer/Command/GlobalCommand.php index c6fc7fe7a..2841c2ae1 100644 --- a/app/vendor/composer/composer/src/Composer/Command/GlobalCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/GlobalCommand.php @@ -33,9 +33,13 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti { $application = $this->getApplication(); if ($input->mustSuggestArgumentValuesFor('command-name')) { - $suggestions->suggestValues(array_values(array_filter(array_map(static function (Command $command) { - return $command->isHidden() ? null : $command->getName(); - }, $application->all())))); + $suggestions->suggestValues(array_values(array_filter( + array_map(static function (Command $command) { + return $command->isHidden() ? null : $command->getName(); + }, $application->all()), function (?string $cmd) { + return $cmd !== null; + } + ))); return; } diff --git a/app/vendor/composer/composer/src/Composer/Command/InitCommand.php b/app/vendor/composer/composer/src/Composer/Command/InitCommand.php index 606bff820..5f6773daf 100644 --- a/app/vendor/composer/composer/src/Composer/Command/InitCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/InitCommand.php @@ -57,7 +57,7 @@ protected function configure() new InputOption('name', null, InputOption::VALUE_REQUIRED, 'Name of the package'), new InputOption('description', null, InputOption::VALUE_REQUIRED, 'Description of package'), new InputOption('author', null, InputOption::VALUE_REQUIRED, 'Author name of package'), - new InputOption('type', null, InputOption::VALUE_OPTIONAL, 'Type of package (e.g. library, project, metapackage, composer-plugin)'), + new InputOption('type', null, InputOption::VALUE_REQUIRED, 'Type of package (e.g. library, project, metapackage, composer-plugin)'), new InputOption('homepage', null, InputOption::VALUE_REQUIRED, 'Homepage of package'), new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()), new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()), @@ -87,7 +87,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = $this->getIO(); $allowlist = ['name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license', 'autoload']; - $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowlist))); + $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowlist)), function ($val) { return $val !== null && $val !== []; }); if (isset($options['name']) && !Preg::isMatch('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}D', $options['name'])) { throw new \InvalidArgumentException( diff --git a/app/vendor/composer/composer/src/Composer/Command/RemoveCommand.php b/app/vendor/composer/composer/src/Composer/Command/RemoveCommand.php index b40fb774e..9803190a4 100644 --- a/app/vendor/composer/composer/src/Composer/Command/RemoveCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/RemoveCommand.php @@ -43,7 +43,7 @@ protected function configure() { $this ->setName('remove') - ->setAliases(['rm']) + ->setAliases(['rm', 'uninstall']) ->setDescription('Removes a package from the require or require-dev') ->setDefinition([ new InputArgument('packages', InputArgument::IS_ARRAY, 'Packages that should be removed.', null, $this->suggestRootRequirement()), diff --git a/app/vendor/composer/composer/src/Composer/Command/SearchCommand.php b/app/vendor/composer/composer/src/Composer/Command/SearchCommand.php index 61f0f95eb..d95c94168 100644 --- a/app/vendor/composer/composer/src/Composer/Command/SearchCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/SearchCommand.php @@ -67,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if (!($composer = $this->tryComposer())) { - $composer = Factory::create($this->getIO(), [], $input->hasParameterOption('--no-plugins')); + $composer = $this->createComposerInstance($input, $this->getIO(), []); } $localRepo = $composer->getRepositoryManager()->getLocalRepository(); $installedRepo = new CompositeRepository([$localRepo, $platformRepo]); diff --git a/app/vendor/composer/composer/src/Composer/Command/ShowCommand.php b/app/vendor/composer/composer/src/Composer/Command/ShowCommand.php index 542482554..a1e7eb059 100644 --- a/app/vendor/composer/composer/src/Composer/Command/ShowCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/ShowCommand.php @@ -478,7 +478,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($showLatest && $showVersion) { foreach ($packages[$type] as $package) { - if (is_object($package)) { + if (is_object($package) && !Preg::isMatch($ignoredPackagesRegex, $package->getPrettyName())) { $latestPackage = $this->findLatestPackage($package, $composer, $platformRepo, $showMajorOnly, $showMinorOnly, $showPatchOnly, $platformReqFilter); if ($latestPackage === null) { continue; @@ -535,10 +535,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $packageViewData['homepage'] = $package instanceof CompletePackageInterface ? $package->getHomepage() : null; $packageViewData['source'] = PackageInfo::getViewSourceUrl($package); } - $nameLength = max($nameLength, strlen($package->getPrettyName())); + $nameLength = max($nameLength, strlen($packageViewData['name'])); if ($writeVersion) { $packageViewData['version'] = $package->getFullPrettyVersion(); - $versionLength = max($versionLength, strlen($package->getFullPrettyVersion())); + if ($format === 'text') { + $packageViewData['version'] = ltrim($packageViewData['version'], 'v'); + } + $versionLength = max($versionLength, strlen($packageViewData['version'])); } if ($writeReleaseDate) { if ($package->getReleaseDate() !== null) { @@ -553,6 +556,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($writeLatest && $latestPackage) { $packageViewData['latest'] = $latestPackage->getFullPrettyVersion(); + if ($format === 'text') { + $packageViewData['latest'] = ltrim($packageViewData['latest'], 'v'); + } $packageViewData['latest-status'] = $this->getUpdateStatus($latestPackage, $package); $latestLength = max($latestLength, strlen($packageViewData['latest'])); } elseif ($writeLatest) { diff --git a/app/vendor/composer/composer/src/Composer/Command/UpdateCommand.php b/app/vendor/composer/composer/src/Composer/Command/UpdateCommand.php index 40e566136..b4478cd04 100644 --- a/app/vendor/composer/composer/src/Composer/Command/UpdateCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/UpdateCommand.php @@ -296,7 +296,9 @@ private function getPackagesInteractively(IOInterface $io, InputInterface $input } } while (true); - $packages = array_filter($packages); + $packages = array_filter($packages, function (string $pkg) { + return $pkg !== ''; + }); if (!$packages) { throw new \InvalidArgumentException('You must enter minimum one package.'); } diff --git a/app/vendor/composer/composer/src/Composer/Command/ValidateCommand.php b/app/vendor/composer/composer/src/Composer/Command/ValidateCommand.php index a5d1e9c46..d9d8c7510 100644 --- a/app/vendor/composer/composer/src/Composer/Command/ValidateCommand.php +++ b/app/vendor/composer/composer/src/Composer/Command/ValidateCommand.php @@ -91,7 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int [$errors, $publishErrors, $warnings] = $validator->validate($file, $checkAll, $checkVersion); $lockErrors = []; - $composer = Factory::create($io, $file, $input->hasParameterOption('--no-plugins')); + $composer = $this->createComposerInstance($input, $io, $file); // config.lock = false ~= implicit --no-check-lock; --check-lock overrides $checkLock = ($checkLock && $composer->getConfig()->get('lock')) || $input->getOption('check-lock'); $locker = $composer->getLocker(); @@ -106,7 +106,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->outputResult($io, $file, $errors, $warnings, $checkPublish, $publishErrors, $checkLock, $lockErrors, true); // $errors include publish and lock errors when exists - $exitCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0); + $exitCode = count($errors) > 0 ? 2 : (($isStrict && count($warnings) > 0) ? 1 : 0); if ($input->getOption('with-dependencies')) { $localRepo = $composer->getRepositoryManager()->getLocalRepository(); @@ -122,7 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->outputResult($io, $package->getPrettyName(), $errors, $warnings, $checkPublish, $publishErrors); // $errors include publish errors when exists - $depCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0); + $depCode = count($errors) > 0 ? 2 : (($isStrict && count($warnings) > 0) ? 1 : 0); $exitCode = max($depCode, $exitCode); } } diff --git a/app/vendor/composer/composer/src/Composer/Composer.php b/app/vendor/composer/composer/src/Composer/Composer.php index 6ba062326..8d87940c3 100644 --- a/app/vendor/composer/composer/src/Composer/Composer.php +++ b/app/vendor/composer/composer/src/Composer/Composer.php @@ -51,9 +51,9 @@ class Composer extends PartialComposer * * @see getVersion() */ - public const VERSION = '2.7.1'; + public const VERSION = '2.7.6'; public const BRANCH_ALIAS_VERSION = ''; - public const RELEASE_DATE = '2024-02-09 15:26:28'; + public const RELEASE_DATE = '2024-05-04 23:03:15'; public const SOURCE_VERSION = ''; /** diff --git a/app/vendor/composer/composer/src/Composer/Config.php b/app/vendor/composer/composer/src/Composer/Config.php index f9da7d304..8d2885a3c 100644 --- a/app/vendor/composer/composer/src/Composer/Config.php +++ b/app/vendor/composer/composer/src/Composer/Config.php @@ -554,6 +554,8 @@ private function realpath(string $path): string * This should be used to read COMPOSER_ environment variables * that overload config values. * + * @param non-empty-string $var + * * @return string|false */ private function getComposerEnv(string $var) diff --git a/app/vendor/composer/composer/src/Composer/Console/Application.php b/app/vendor/composer/composer/src/Composer/Console/Application.php index 709ce6ef8..7919c29d9 100644 --- a/app/vendor/composer/composer/src/Composer/Console/Application.php +++ b/app/vendor/composer/composer/src/Composer/Console/Application.php @@ -12,6 +12,7 @@ namespace Composer\Console; +use Composer\Installer; use Composer\IO\NullIO; use Composer\Util\Filesystem; use Composer\Util\Platform; @@ -32,6 +33,7 @@ use Composer\Command; use Composer\Composer; use Composer\Factory; +use Composer\Downloader\TransportException; use Composer\IO\IOInterface; use Composer\IO\ConsoleIO; use Composer\Json\JsonValidationException; @@ -41,6 +43,7 @@ use Composer\Exception\NoSslException; use Composer\XdebugHandler\XdebugHandler; use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Composer\Util\Http\ProxyManager; /** * The console application that handles the commands @@ -215,7 +218,11 @@ public function doRun(InputInterface $input, OutputInterface $output): int $needsSudoCheck = !Platform::isWindows() && function_exists('exec') && !Platform::getEnv('COMPOSER_ALLOW_SUPERUSER') - && (ini_get('open_basedir') || !file_exists('/.dockerenv')); + && (ini_get('open_basedir') || ( + !file_exists('/.dockerenv') + && !file_exists('/run/.containerenv') + && !file_exists('/var/run/.containerenv') + )); $isNonAllowedRoot = false; // Clobber sudo credentials if COMPOSER_ALLOW_SUPERUSER is not set before loading plugins @@ -293,7 +300,8 @@ public function doRun(InputInterface $input, OutputInterface $output): int } if (!$this->disablePluginsByDefault && $isNonAllowedRoot && !$io->isInteractive()) { - $io->writeError('Composer plugins have been disabled for safety in this non-interactive session. Set COMPOSER_ALLOW_SUPERUSER=1 if you want to allow plugins to run as root/super user.'); + $io->writeError('Composer plugins have been disabled for safety in this non-interactive session.'); + $io->writeError('Set COMPOSER_ALLOW_SUPERUSER=1 if you want to allow plugins to run as root/super user.'); $this->disablePluginsByDefault = true; } @@ -376,6 +384,8 @@ function_exists('php_uname') ? php_uname('s') . ' / ' . php_uname('r') : 'Unknow } try { + $proxyManager = ProxyManager::getInstance(); + if ($input->hasParameterOption('--profile')) { $startTime = microtime(true); $this->io->enableDebugging($startTime); @@ -383,6 +393,11 @@ function_exists('php_uname') ? php_uname('s') . ' / ' . php_uname('r') : 'Unknow $result = parent::doRun($input, $output); + if (true === $input->hasParameterOption(['--version', '-V'], true)) { + $io->writeError(sprintf('PHP version %s (%s)', \PHP_VERSION, \PHP_BINARY)); + $io->writeError('Run the "diagnose" command to get more detailed diagnostics output.'); + } + // chdir back to $oldWorkingDir if set if (isset($oldWorkingDir) && '' !== $oldWorkingDir) { Silencer::call('chdir', $oldWorkingDir); @@ -392,6 +407,14 @@ function_exists('php_uname') ? php_uname('s') . ' / ' . php_uname('r') : 'Unknow $io->writeError('Memory usage: '.round(memory_get_usage() / 1024 / 1024, 2).'MiB (peak: '.round(memory_get_peak_usage() / 1024 / 1024, 2).'MiB), time: '.round(microtime(true) - $startTime, 2).'s'); } + if ($proxyManager->needsTransitionWarning()) { + $io->writeError(''); + $io->writeError('Composer now requires separate proxy environment variables for HTTP and HTTPS requests.'); + $io->writeError('Please set `https_proxy` in addition to your existing proxy environment variables.'); + $io->writeError('This fallback (and warning) will be removed in Composer 2.8.0.'); + $io->writeError('https://getcomposer.org/doc/faqs/how-to-use-composer-behind-a-proxy.md'); + } + return $result; } catch (ScriptExecutionException $e) { if ($this->getDisablePluginsByDefault() && $this->isRunningAsRoot() && !$this->io->isInteractive()) { @@ -419,6 +442,13 @@ function_exists('php_uname') ? php_uname('s') . ' / ' . php_uname('r') : 'Unknow return max(1, $e->getCode()); } + // override TransportException's code for the purpose of parent::run() using it as process exit code + // as http error codes are all beyond the 255 range of permitted exit codes + if ($e instanceof TransportException) { + $reflProp = new \ReflectionProperty($e, 'code'); + $reflProp->setValue($e, Installer::ERROR_TRANSPORT_EXCEPTION); + } + throw $e; } finally { restore_error_handler(); @@ -466,6 +496,11 @@ private function hintCommonErrors(\Throwable $exception, OutputInterface $output } Silencer::restore(); + if ($exception instanceof TransportException && str_contains($exception->getMessage(), 'Unable to use a proxy')) { + $io->writeError('The following exception indicates your proxy is misconfigured', true, IOInterface::QUIET); + $io->writeError('Check https://getcomposer.org/doc/faqs/how-to-use-composer-behind-a-proxy.md for details', true, IOInterface::QUIET); + } + if (Platform::isWindows() && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) { $io->writeError('The following exception may be caused by a stale entry in your cmd.exe AutoRun', true, IOInterface::QUIET); $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details', true, IOInterface::QUIET); diff --git a/app/vendor/composer/composer/src/Composer/DependencyResolver/LockTransaction.php b/app/vendor/composer/composer/src/Composer/DependencyResolver/LockTransaction.php index 70603b867..d77a21139 100644 --- a/app/vendor/composer/composer/src/Composer/DependencyResolver/LockTransaction.php +++ b/app/vendor/composer/composer/src/Composer/DependencyResolver/LockTransaction.php @@ -104,36 +104,71 @@ public function getNewLockPackages(bool $devMode, bool $updateMirrors = false): { $packages = []; foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) { - if (!$package instanceof AliasPackage) { - // if we're just updating mirrors we need to reset references to the same as currently "present" packages' references to keep the lock file as-is - // we do not reset references if the currently present package didn't have any, or if the type of VCS has changed - if ($updateMirrors && !isset($this->presentMap[spl_object_hash($package)])) { - foreach ($this->presentMap as $presentPackage) { - if ($package->getName() === $presentPackage->getName() && $package->getVersion() === $presentPackage->getVersion()) { - if ($presentPackage->getSourceReference() && $presentPackage->getSourceType() === $package->getSourceType()) { - $package->setSourceDistReferences($presentPackage->getSourceReference()); - // if the dist url is not one of those handled gracefully by setSourceDistReferences then we should overwrite it with the old one - if ($package->getDistUrl() !== null && !Preg::isMatch('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl())) { - $package->setDistUrl($presentPackage->getDistUrl()); - } - $package->setDistType($presentPackage->getDistType()); - if ($package instanceof Package) { - $package->setDistSha1Checksum($presentPackage->getDistSha1Checksum()); - } - } - if ($presentPackage->getReleaseDate() !== null && $package instanceof Package) { - $package->setReleaseDate($presentPackage->getReleaseDate()); - } - } - } - } - $packages[] = $package; + if ($package instanceof AliasPackage) { + continue; } + + // if we're just updating mirrors we need to reset everything to the same as currently "present" packages' references to keep the lock file as-is + if ($updateMirrors === true && !array_key_exists(spl_object_hash($package), $this->presentMap)) { + $package = $this->updateMirrorAndUrls($package); + } + + $packages[] = $package; } return $packages; } + /** + * Try to return the original package from presentMap with updated URLs/mirrors + * + * If the type of source/dist changed, then we do not update those and keep them as they were + */ + private function updateMirrorAndUrls(BasePackage $package): BasePackage + { + foreach ($this->presentMap as $presentPackage) { + if ($package->getName() !== $presentPackage->getName()) { + continue; + } + + if ($package->getVersion() !== $presentPackage->getVersion()) { + continue; + } + + if ($presentPackage->getSourceReference() === null) { + continue; + } + + if ($presentPackage->getSourceType() !== $package->getSourceType()) { + continue; + } + + if ($presentPackage instanceof Package) { + $presentPackage->setSourceUrl($package->getSourceUrl()); + $presentPackage->setSourceMirrors($package->getSourceMirrors()); + } + + // if the dist type changed, we only update the source url/mirrors + if ($presentPackage->getDistType() !== $package->getDistType()) { + return $presentPackage; + } + + // update dist url if it is in a known format + if ( + $package->getDistUrl() !== null + && $presentPackage->getDistReference() !== null + && Preg::isMatch('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl()) + ) { + $presentPackage->setDistUrl(Preg::replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $presentPackage->getDistReference(), $package->getDistUrl())); + } + $presentPackage->setDistMirrors($package->getDistMirrors()); + + return $presentPackage; + } + + return $package; + } + /** * Checks which of the given aliases from composer.json are actually in use for the lock file * @param list $aliases diff --git a/app/vendor/composer/composer/src/Composer/DependencyResolver/PoolBuilder.php b/app/vendor/composer/composer/src/Composer/DependencyResolver/PoolBuilder.php index d84d40906..52aefa251 100644 --- a/app/vendor/composer/composer/src/Composer/DependencyResolver/PoolBuilder.php +++ b/app/vendor/composer/composer/src/Composer/DependencyResolver/PoolBuilder.php @@ -106,6 +106,10 @@ class PoolBuilder private $updateAllowList = []; /** @var array> */ private $skippedLoad = []; + /** @var list */ + private $ignoredTypes = []; + /** @var list|null */ + private $allowedTypes = null; /** * If provided, only these package names are loaded @@ -170,6 +174,26 @@ public function __construct(array $acceptableStabilities, array $stabilityFlags, $this->temporaryConstraints = $temporaryConstraints; } + /** + * Packages of those types are ignored + * + * @param list $types + */ + public function setIgnoredTypes(array $types): void + { + $this->ignoredTypes = $types; + } + + /** + * Only packages of those types are allowed if set to non-null + * + * @param list|null $types + */ + public function setAllowedTypes(?array $types): void + { + $this->allowedTypes = $types; + } + /** * @param RepositoryInterface[] $repositories */ @@ -406,6 +430,10 @@ private function loadPackagesMarkedForLoading(Request $request, array $repositor } foreach ($result['packages'] as $package) { $this->loadedPerRepo[$repoIndex][$package->getName()][$package->getVersion()] = $package; + + if (in_array($package->getType(), $this->ignoredTypes, true) || ($this->allowedTypes !== null && !in_array($package->getType(), $this->allowedTypes, true))) { + continue; + } $this->loadPackage($request, $repositories, $package, !isset($this->pathRepoUnlocked[$package->getName()])); } } diff --git a/app/vendor/composer/composer/src/Composer/Downloader/FileDownloader.php b/app/vendor/composer/composer/src/Composer/Downloader/FileDownloader.php index 2a2847f33..adf26785c 100644 --- a/app/vendor/composer/composer/src/Composer/Downloader/FileDownloader.php +++ b/app/vendor/composer/composer/src/Composer/Downloader/FileDownloader.php @@ -27,6 +27,7 @@ use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Filesystem; +use Composer\Util\Http\Response; use Composer\Util\Platform; use Composer\Util\Silencer; use Composer\Util\HttpDownloader; @@ -99,9 +100,9 @@ public function __construct(IOInterface $io, Config $config, HttpDownloader $htt $this->httpDownloader = $httpDownloader; $this->cache = $cache; $this->process = $process ?? new ProcessExecutor($io); - $this->filesystem = $filesystem ?: new Filesystem($this->process); + $this->filesystem = $filesystem ?? new Filesystem($this->process); - if ($this->cache && $this->cache->gcIsNecessary()) { + if ($this->cache !== null && $this->cache->gcIsNecessary()) { $this->io->writeError('Running cache garbage collection', true, IOInterface::VERY_VERBOSE); $this->cache->gc($config->get('cache-files-ttl'), $config->get('cache-files-maxsize')); } @@ -120,7 +121,7 @@ public function getInstallationSource(): string */ public function download(PackageInterface $package, string $path, ?PackageInterface $prevPackage = null, bool $output = true): PromiseInterface { - if (!$package->getDistUrl()) { + if (null === $package->getDistUrl()) { throw new \InvalidArgumentException('The given package is missing url information'); } @@ -152,21 +153,15 @@ public function download(PackageInterface $package, string $path, ?PackageInterf $this->filesystem->ensureDirectoryExists($path); $this->filesystem->ensureDirectoryExists(dirname($fileName)); - $io = $this->io; - $cache = $this->cache; - $httpDownloader = $this->httpDownloader; - $eventDispatcher = $this->eventDispatcher; - $filesystem = $this->filesystem; - $accept = null; $reject = null; - $download = function () use ($io, $output, $httpDownloader, $cache, $cacheKeyGenerator, $eventDispatcher, $package, $fileName, &$urls, &$accept, &$reject) { + $download = function () use ($output, $cacheKeyGenerator, $package, $fileName, &$urls, &$accept, &$reject) { $url = reset($urls); $index = key($urls); - if ($eventDispatcher) { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed'], 'package', $package); - $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + if ($this->eventDispatcher !== null) { + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $url['processed'], 'package', $package); + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); if ($preFileDownloadEvent->getCustomCacheKey() !== null) { $url['cacheKey'] = $cacheKeyGenerator($package, $preFileDownloadEvent->getCustomCacheKey()); } elseif ($preFileDownloadEvent->getProcessedUrl() !== $url['processed']) { @@ -181,27 +176,27 @@ public function download(PackageInterface $package, string $path, ?PackageInterf $cacheKey = $url['cacheKey']; // use from cache if it is present and has a valid checksum or we have no checksum to check against - if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) { + if ($this->cache !== null && ($checksum === null || $checksum === '' || $checksum === $this->cache->sha1($cacheKey)) && $this->cache->copyTo($cacheKey, $fileName)) { if ($output) { - $io->writeError(" - Loading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") from cache", true, IOInterface::VERY_VERBOSE); + $this->io->writeError(" - Loading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") from cache", true, IOInterface::VERY_VERBOSE); } // mark the file as having been written in cache even though it is only read from cache, so that if // the cache is corrupt the archive will be deleted and the next attempt will re-download it // see https://github.com/composer/composer/issues/10028 - if (!$cache->isReadOnly()) { + if (!$this->cache->isReadOnly()) { $this->lastCacheWrites[$package->getName()] = $cacheKey; } $result = \React\Promise\resolve($fileName); } else { if ($output) { - $io->writeError(" - Downloading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $this->io->writeError(" - Downloading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); } - $result = $httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions()) + $result = $this->httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions()) ->then($accept, $reject); } - return $result->then(static function ($result) use ($fileName, $checksum, $url, $package, $eventDispatcher): string { + return $result->then(function ($result) use ($fileName, $checksum, $url, $package): string { // in case of retry, the first call's Promise chain finally calls this twice at the end, // once with $result being the returned $fileName from $accept, and then once for every // failed request with a null result, which can be skipped. @@ -214,31 +209,35 @@ public function download(PackageInterface $package, string $path, ?PackageInterf .' directory is writable and you have internet connectivity'); } - if ($checksum && hash_file('sha1', $fileName) !== $checksum) { + if ($checksum !== null && $checksum !== '' && hash_file('sha1', $fileName) !== $checksum) { throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url['base'].')'); } - if ($eventDispatcher) { + if ($this->eventDispatcher !== null) { $postFileDownloadEvent = new PostFileDownloadEvent(PluginEvents::POST_FILE_DOWNLOAD, $fileName, $checksum, $url['processed'], 'package', $package); - $eventDispatcher->dispatch($postFileDownloadEvent->getName(), $postFileDownloadEvent); + $this->eventDispatcher->dispatch($postFileDownloadEvent->getName(), $postFileDownloadEvent); } return $fileName; }); }; - $accept = function ($response) use ($cache, $package, $fileName, &$urls): string { + $accept = function (Response $response) use ($package, $fileName, &$urls): string { $url = reset($urls); $cacheKey = $url['cacheKey']; - FileDownloader::$downloadMetadata[$package->getName()] = @filesize($fileName) ?: $response->getHeader('Content-Length') ?: '?'; + $fileSize = @filesize($fileName); + if (false === $fileSize) { + $fileSize = $response->getHeader('Content-Length') ?? '?'; + } + FileDownloader::$downloadMetadata[$package->getName()] = $fileSize; if (Platform::getEnv('GITHUB_ACTIONS') !== false && Platform::getEnv('COMPOSER_TESTS_ARE_RUNNING') === false) { FileDownloader::$responseHeaders[$package->getName()] = $response->getHeaders(); } - if ($cache && !$cache->isReadOnly()) { + if ($this->cache !== null && !$this->cache->isReadOnly()) { $this->lastCacheWrites[$package->getName()] = $cacheKey; - $cache->copyFrom($cacheKey, $fileName); + $this->cache->copyFrom($cacheKey, $fileName); } $response->collect(); @@ -246,10 +245,10 @@ public function download(PackageInterface $package, string $path, ?PackageInterf return $fileName; }; - $reject = function ($e) use ($io, &$urls, $download, $fileName, $package, &$retries, $filesystem) { + $reject = function ($e) use (&$urls, $download, $fileName, $package, &$retries) { // clean up if (file_exists($fileName)) { - $filesystem->unlink($fileName); + $this->filesystem->unlink($fileName); } $this->clearLastCacheWrite($package); @@ -263,7 +262,7 @@ public function download(PackageInterface $package, string $path, ?PackageInterf if ($e instanceof TransportException) { // if we got an http response with a proper code, then requesting again will probably not help, abort - if ((0 !== $e->getCode() && !in_array($e->getCode(), [500, 502, 503, 504])) || !$retries) { + if (0 !== $e->getCode() && !in_array($e->getCode(), [500, 502, 503, 504], true)) { $retries = 0; } } @@ -274,7 +273,7 @@ public function download(PackageInterface $package, string $path, ?PackageInterf $urls = []; } - if ($retries) { + if ($retries > 0) { usleep(500000); $retries--; @@ -282,12 +281,12 @@ public function download(PackageInterface $package, string $path, ?PackageInterf } array_shift($urls); - if ($urls) { - if ($io->isDebug()) { - $io->writeError(' Failed downloading '.$package->getName().': ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage()); - $io->writeError(' Trying the next URL for '.$package->getName()); + if (\count($urls) > 0) { + if ($this->io->isDebug()) { + $this->io->writeError(' Failed downloading '.$package->getName().': ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage()); + $this->io->writeError(' Trying the next URL for '.$package->getName()); } else { - $io->writeError(' Failed downloading '.$package->getName().', trying the next URL ('.$e->getCode().': '.$e->getMessage().')'); + $this->io->writeError(' Failed downloading '.$package->getName().', trying the next URL ('.$e->getCode().': '.$e->getMessage().')'); } $retries = 3; @@ -328,8 +327,8 @@ public function cleanup(string $type, PackageInterface $package, string $path, ? ]; if (isset($this->additionalCleanupPaths[$package->getName()])) { - foreach ($this->additionalCleanupPaths[$package->getName()] as $path) { - $this->filesystem->remove($path); + foreach ($this->additionalCleanupPaths[$package->getName()] as $pathToClean) { + $this->filesystem->remove($pathToClean); } } @@ -355,19 +354,20 @@ public function install(PackageInterface $package, string $path, bool $output = $this->filesystem->ensureDirectoryExists($path); $this->filesystem->rename($this->getFileName($package, $path), $path . '/' . $this->getDistPath($package, PATHINFO_BASENAME)); - if ($package->getBinaries()) { - // Single files can not have a mode set like files in archives - // so we make sure if the file is a binary that it is executable - foreach ($package->getBinaries() as $bin) { - if (file_exists($path . '/' . $bin) && !is_executable($path . '/' . $bin)) { - Silencer::call('chmod', $path . '/' . $bin, 0777 & ~umask()); - } + // Single files can not have a mode set like files in archives + // so we make sure if the file is a binary that it is executable + foreach ($package->getBinaries() as $bin) { + if (file_exists($path . '/' . $bin) && !is_executable($path . '/' . $bin)) { + Silencer::call('chmod', $path . '/' . $bin, 0777 & ~umask()); } } return \React\Promise\resolve(null); } + /** + * @param PATHINFO_EXTENSION|PATHINFO_BASENAME $component + */ protected function getDistPath(PackageInterface $package, int $component): string { return pathinfo((string) parse_url(strtr((string) $package->getDistUrl(), '\\', '/'), PHP_URL_PATH), $component); @@ -375,7 +375,7 @@ protected function getDistPath(PackageInterface $package, int $component): strin protected function clearLastCacheWrite(PackageInterface $package): void { - if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) { + if ($this->cache !== null && isset($this->lastCacheWrites[$package->getName()])) { $this->cache->remove($this->lastCacheWrites[$package->getName()]); unset($this->lastCacheWrites[$package->getName()]); } @@ -389,7 +389,7 @@ protected function addCleanupPath(PackageInterface $package, string $path): void protected function removeCleanupPath(PackageInterface $package, string $path): void { if (isset($this->additionalCleanupPaths[$package->getName()])) { - $idx = array_search($path, $this->additionalCleanupPaths[$package->getName()]); + $idx = array_search($path, $this->additionalCleanupPaths[$package->getName()], true); if (false !== $idx) { unset($this->additionalCleanupPaths[$package->getName()][$idx]); } @@ -469,7 +469,7 @@ protected function processUrl(PackageInterface $package, string $url): string throw new \RuntimeException('You must enable the openssl extension to download files via https'); } - if ($package->getDistReference()) { + if ($package->getDistReference() !== null) { $url = UrlUtil::updateDistReference($this->config, $url, $package->getDistReference()); } @@ -495,10 +495,22 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin $this->filesystem->removeDirectory($targetDir.'_compare'); } - $this->download($package, $targetDir.'_compare', null, false); + $promise = $this->download($package, $targetDir.'_compare', null, false); + $promise->then(null, function ($ex) use (&$e) { + $e = $ex; + }); $this->httpDownloader->wait(); - $this->install($package, $targetDir.'_compare', false); + if ($e !== null) { + throw $e; + } + $promise = $this->install($package, $targetDir.'_compare', false); + $promise->then(null, function ($ex) use (&$e) { + $e = $ex; + }); $this->process->wait(); + if ($e !== null) { + throw $e; + } $comparer = new Comparer(); $comparer->setSource($targetDir.'_compare'); @@ -511,8 +523,12 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin $this->io = $prevIO; - if ($e) { - throw $e; + if ($e !== null) { + if ($this->io->isDebug()) { + throw $e; + } + + return 'Failed to detect changes: ['.get_class($e).'] '.$e->getMessage(); } $output = trim($output); diff --git a/app/vendor/composer/composer/src/Composer/Downloader/TransportException.php b/app/vendor/composer/composer/src/Composer/Downloader/TransportException.php index c91897c1c..5ab5ca6f3 100644 --- a/app/vendor/composer/composer/src/Composer/Downloader/TransportException.php +++ b/app/vendor/composer/composer/src/Composer/Downloader/TransportException.php @@ -26,6 +26,11 @@ class TransportException extends \RuntimeException /** @var array */ protected $responseInfo = []; + public function __construct(string $message = "", int $code = 400, \Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } + /** * @param array $headers */ diff --git a/app/vendor/composer/composer/src/Composer/EventDispatcher/EventDispatcher.php b/app/vendor/composer/composer/src/Composer/EventDispatcher/EventDispatcher.php index b91b4e8ed..cab856252 100644 --- a/app/vendor/composer/composer/src/Composer/EventDispatcher/EventDispatcher.php +++ b/app/vendor/composer/composer/src/Composer/EventDispatcher/EventDispatcher.php @@ -194,6 +194,8 @@ protected function doDispatch(Event $event) $this->pushEvent($event); + $autoloadersBefore = spl_autoload_functions(); + try { $returnMax = 0; foreach ($listeners as $callable) { @@ -411,6 +413,26 @@ protected function doDispatch(Event $event) } } finally { $this->popEvent(); + + $knownIdentifiers = []; + foreach ($autoloadersBefore as $key => $cb) { + $knownIdentifiers[$this->getCallbackIdentifier($cb)] = ['key' => $key, 'callback' => $cb]; + } + foreach (spl_autoload_functions() as $cb) { + // once we get to the first known autoloader, we can leave any appended autoloader without problems + if (isset($knownIdentifiers[$this->getCallbackIdentifier($cb)]) && $knownIdentifiers[$this->getCallbackIdentifier($cb)]['key'] === 0) { + break; + } + + // other newly appeared prepended autoloaders should be appended instead to ensure Composer loads its classes first + if ($cb instanceof ClassLoader) { + $cb->unregister(); + $cb->register(false); + } else { + spl_autoload_unregister($cb); + spl_autoload_register($cb); + } + } } return $returnMax; @@ -638,4 +660,23 @@ private function ensureBinDirIsInPath(): void } } } + + /** + * @param callable $cb DO NOT MOVE TO TYPE HINT as private autoload callbacks are not technically callable + */ + private function getCallbackIdentifier($cb): string + { + if (is_string($cb)) { + return 'fn:'.$cb; + } + if (is_object($cb)) { + return 'obj:'.spl_object_hash($cb); + } + if (is_array($cb)) { + return 'array:'.(is_string($cb[0]) ? $cb[0] : get_class($cb[0]) .'#'.spl_object_hash($cb[0])).'::'.$cb[1]; + } + + // not great but also do not want to break everything here + return 'unsupported'; + } } diff --git a/app/vendor/composer/composer/src/Composer/Factory.php b/app/vendor/composer/composer/src/Composer/Factory.php index 03a5fa2ae..0d455b7e3 100644 --- a/app/vendor/composer/composer/src/Composer/Factory.php +++ b/app/vendor/composer/composer/src/Composer/Factory.php @@ -681,7 +681,7 @@ public static function createHttpDownloader(IOInterface $io, Config $config, arr private static function useXdg(): bool { foreach (array_keys($_SERVER) as $key) { - if (strpos($key, 'XDG_') === 0) { + if (strpos((string) $key, 'XDG_') === 0) { return true; } } diff --git a/app/vendor/composer/composer/src/Composer/IO/BaseIO.php b/app/vendor/composer/composer/src/Composer/IO/BaseIO.php index 710f5a108..c4d40ba01 100644 --- a/app/vendor/composer/composer/src/Composer/IO/BaseIO.php +++ b/app/vendor/composer/composer/src/Composer/IO/BaseIO.php @@ -15,6 +15,7 @@ use Composer\Config; use Composer\Pcre\Preg; use Composer\Util\ProcessExecutor; +use Composer\Util\Silencer; use Psr\Log\LogLevel; abstract class BaseIO implements IOInterface @@ -219,6 +220,13 @@ public function log($level, $message, array $context = []): void { $message = (string) $message; + if ($context !== []) { + $json = Silencer::call('json_encode', $context, JSON_INVALID_UTF8_IGNORE|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE); + if ($json !== false) { + $message .= ' ' . $json; + } + } + if (in_array($level, [LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR])) { $this->writeError(''.$message.''); } elseif ($level === LogLevel::WARNING) { diff --git a/app/vendor/composer/composer/src/Composer/Installer.php b/app/vendor/composer/composer/src/Composer/Installer.php index fe50c8fde..5f07b0e17 100644 --- a/app/vendor/composer/composer/src/Composer/Installer.php +++ b/app/vendor/composer/composer/src/Composer/Installer.php @@ -80,6 +80,8 @@ class Installer // used/declared in SolverProblemsException, carried over here for completeness public const ERROR_DEPENDENCY_RESOLUTION_FAILED = 2; public const ERROR_AUDIT_FAILED = 5; + // technically exceptions are thrown with various status codes >400, but the process exit code is normalized to 100 + public const ERROR_TRANSPORT_EXCEPTION = 100; /** * @var IOInterface @@ -176,6 +178,10 @@ class Installer protected $errorOnAudit = false; /** @var Auditor::FORMAT_* */ protected $auditFormat = Auditor::FORMAT_SUMMARY; + /** @var list */ + private $ignoredTypes = ['php-ext', 'php-ext-zend']; + /** @var list|null */ + private $allowedTypes = null; /** @var bool */ protected $updateMirrors = false; @@ -492,7 +498,7 @@ protected function doUpdate(InstalledRepositoryInterface $localRepo, bool $doIns $request->setUpdateAllowList($this->updateAllowList, $this->updateAllowTransitiveDependencies); } - $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, $this->createPoolOptimizer($policy)); + $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, $this->createPoolOptimizer($policy), $this->ignoredTypes, $this->allowedTypes); $this->io->writeError('Updating dependencies'); @@ -750,7 +756,7 @@ protected function doInstall(InstalledRepositoryInterface $localRepo, bool $alre $request->requireName($link->getTarget(), $link->getConstraint()); } - $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher); + $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, null, $this->ignoredTypes, $this->allowedTypes); // solve dependencies $solver = new Solver($policy, $pool, $this->io); @@ -1103,6 +1109,32 @@ public static function create(IOInterface $io, Composer $composer): self ); } + /** + * Packages of those types are ignored, by default php-ext and php-ext-zend are ignored + * + * @param list $types + * @return $this + */ + public function setIgnoredTypes(array $types): self + { + $this->ignoredTypes = $types; + + return $this; + } + + /** + * Only packages of those types are allowed if set to non-null + * + * @param list|null $types + * @return $this + */ + public function setAllowedTypes(?array $types): self + { + $this->allowedTypes = $types; + + return $this; + } + /** * @return $this */ diff --git a/app/vendor/composer/composer/src/Composer/Installer/BinaryInstaller.php b/app/vendor/composer/composer/src/Composer/Installer/BinaryInstaller.php index ba50fe17f..9e3123ff8 100644 --- a/app/vendor/composer/composer/src/Composer/Installer/BinaryInstaller.php +++ b/app/vendor/composer/composer/src/Composer/Installer/BinaryInstaller.php @@ -225,7 +225,12 @@ protected function generateUnixyProxyCode(string $bin, string $link): string $phpunitHack1 = $phpunitHack2 = ''; // Don't expose autoload path when vendor dir was not set in custom installers if ($this->vendorDir) { - $globalsCode .= '$GLOBALS[\'_composer_autoload_path\'] = ' . $this->filesystem->findShortestPathCode($link, $this->vendorDir . '/autoload.php', false, true).";\n"; + // ensure comparisons work accurately if the CWD is a symlink, as $link is realpath'd already + $vendorDirReal = realpath($this->vendorDir); + if ($vendorDirReal === false) { + $vendorDirReal = $this->vendorDir; + } + $globalsCode .= '$GLOBALS[\'_composer_autoload_path\'] = ' . $this->filesystem->findShortestPathCode($link, $vendorDirReal . '/autoload.php', false, true).";\n"; } // Add workaround for PHPUnit process isolation if ($this->filesystem->normalizePath($bin) === $this->filesystem->normalizePath($this->vendorDir.'/phpunit/phpunit/phpunit')) { diff --git a/app/vendor/composer/composer/src/Composer/Json/JsonManipulator.php b/app/vendor/composer/composer/src/Composer/Json/JsonManipulator.php index 8d6759671..2e0f70411 100644 --- a/app/vendor/composer/composer/src/Composer/Json/JsonManipulator.php +++ b/app/vendor/composer/composer/src/Composer/Json/JsonManipulator.php @@ -416,6 +416,9 @@ public function removeSubNode(string $mainNode, string $name): bool if ($subName !== null) { $curVal = json_decode($children, true); unset($curVal[$name][$subName]); + if ($curVal[$name] === []) { + $curVal[$name] = new \ArrayObject(); + } $this->addSubNode($mainNode, $name, $curVal[$name]); } @@ -427,7 +430,10 @@ public function removeSubNode(string $mainNode, string $name): bool if ($subName !== null) { $curVal = json_decode($matches['content'], true); unset($curVal[$name][$subName]); - $childrenClean = $this->format($curVal); + if ($curVal[$name] === []) { + $curVal[$name] = new \ArrayObject(); + } + $childrenClean = $this->format($curVal, 0, true); } return $matches['start'] . $childrenClean . $matches['end']; @@ -534,12 +540,19 @@ public function removeMainKeyIfEmpty(string $key): bool /** * @param mixed $data */ - public function format($data, int $depth = 0): string + public function format($data, int $depth = 0, bool $wasObject = false): string { + if ($data instanceof \stdClass || $data instanceof \ArrayObject) { + $data = (array) $data; + $wasObject = true; + } + if (is_array($data)) { - reset($data); + if (\count($data) === 0) { + return $wasObject ? '{' . $this->newline . str_repeat($this->indent, $depth + 1) . '}' : '[]'; + } - if (is_numeric(key($data))) { + if (array_is_list($data)) { foreach ($data as $key => $val) { $data[$key] = $this->format($val, $depth + 1); } @@ -550,7 +563,7 @@ public function format($data, int $depth = 0): string $out = '{' . $this->newline; $elems = []; foreach ($data as $key => $val) { - $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode($key). ': '.$this->format($val, $depth + 1); + $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode((string) $key). ': '.$this->format($val, $depth + 1); } return $out . implode(','.$this->newline, $elems) . $this->newline . str_repeat($this->indent, $depth + 1) . '}'; diff --git a/app/vendor/composer/composer/src/Composer/Package/AliasPackage.php b/app/vendor/composer/composer/src/Composer/Package/AliasPackage.php index 75d3158cc..932ea3658 100644 --- a/app/vendor/composer/composer/src/Composer/Package/AliasPackage.php +++ b/app/vendor/composer/composer/src/Composer/Package/AliasPackage.php @@ -351,6 +351,11 @@ public function getIncludePaths(): array return $this->aliasOf->getIncludePaths(); } + public function getPhpExt(): ?array + { + return $this->aliasOf->getPhpExt(); + } + public function getReleaseDate(): ?\DateTimeInterface { return $this->aliasOf->getReleaseDate(); diff --git a/app/vendor/composer/composer/src/Composer/Package/Archiver/ArchiveManager.php b/app/vendor/composer/composer/src/Composer/Package/Archiver/ArchiveManager.php index 6efa47938..4b15fa844 100644 --- a/app/vendor/composer/composer/src/Composer/Package/Archiver/ArchiveManager.php +++ b/app/vendor/composer/composer/src/Composer/Package/Archiver/ArchiveManager.php @@ -99,7 +99,9 @@ public function getPackageFilenameParts(CompletePackageInterface $package): arra $parts['source_reference'] = substr(sha1($sourceReference), 0, 6); } - $parts = array_filter($parts); + $parts = array_filter($parts, function (?string $part) { + return $part !== null; + }); foreach ($parts as $key => $part) { $parts[$key] = str_replace('/', '-', $part); } diff --git a/app/vendor/composer/composer/src/Composer/Package/Dumper/ArrayDumper.php b/app/vendor/composer/composer/src/Composer/Package/Dumper/ArrayDumper.php index f713fbe33..046ba6674 100644 --- a/app/vendor/composer/composer/src/Composer/Package/Dumper/ArrayDumper.php +++ b/app/vendor/composer/composer/src/Composer/Package/Dumper/ArrayDumper.php @@ -44,11 +44,11 @@ public function dump(PackageInterface $package): array $data['version'] = $package->getPrettyVersion(); $data['version_normalized'] = $package->getVersion(); - if ($package->getTargetDir()) { + if ($package->getTargetDir() !== null) { $data['target-dir'] = $package->getTargetDir(); } - if ($package->getSourceType()) { + if ($package->getSourceType() !== null) { $data['source']['type'] = $package->getSourceType(); $data['source']['url'] = $package->getSourceUrl(); if (null !== ($value = $package->getSourceReference())) { @@ -59,7 +59,7 @@ public function dump(PackageInterface $package): array } } - if ($package->getDistType()) { + if ($package->getDistType() !== null) { $data['dist']['type'] = $package->getDistType(); $data['dist']['url'] = $package->getDistUrl(); if (null !== ($value = $package->getDistReference())) { @@ -74,15 +74,18 @@ public function dump(PackageInterface $package): array } foreach (BasePackage::$supportedLinkTypes as $type => $opts) { - if ($links = $package->{'get'.ucfirst($opts['method'])}()) { - foreach ($links as $link) { - $data[$type][$link->getTarget()] = $link->getPrettyConstraint(); - } - ksort($data[$type]); + $links = $package->{'get'.ucfirst($opts['method'])}(); + if (\count($links) === 0) { + continue; } + foreach ($links as $link) { + $data[$type][$link->getTarget()] = $link->getPrettyConstraint(); + } + ksort($data[$type]); } - if ($packages = $package->getSuggests()) { + $packages = $package->getSuggests(); + if (\count($packages) > 0) { ksort($packages); $data['suggest'] = $packages; } @@ -130,7 +133,7 @@ public function dump(PackageInterface $package): array if ($package instanceof RootPackageInterface) { $minimumStability = $package->getMinimumStability(); - if ($minimumStability) { + if ($minimumStability !== '') { $data['minimum-stability'] = $minimumStability; } } diff --git a/app/vendor/composer/composer/src/Composer/Package/Loader/ArrayLoader.php b/app/vendor/composer/composer/src/Composer/Package/Loader/ArrayLoader.php index 44b778f29..887f2913b 100644 --- a/app/vendor/composer/composer/src/Composer/Package/Loader/ArrayLoader.php +++ b/app/vendor/composer/composer/src/Composer/Package/Loader/ArrayLoader.php @@ -228,6 +228,10 @@ private function configureObject(PackageInterface $package, array $config): Base $package->setIncludePaths($config['include-path']); } + if (isset($config['php-ext'])) { + $package->setPhpExt($config['php-ext']); + } + if (!empty($config['time'])) { $time = Preg::isMatch('/^\d++$/D', $config['time']) ? '@'.$config['time'] : $config['time']; @@ -313,8 +317,8 @@ private function configureObject(PackageInterface $package, array $config): Base } /** - * @param array>>> $linkCache - * @param mixed[] $config + * @param array>>> $linkCache + * @param mixed[] $config */ private function configureCachedLinks(array &$linkCache, PackageInterface $package, array $config): void { diff --git a/app/vendor/composer/composer/src/Composer/Package/Loader/RootPackageLoader.php b/app/vendor/composer/composer/src/Composer/Package/Loader/RootPackageLoader.php index 7e9d386c2..64a169019 100644 --- a/app/vendor/composer/composer/src/Composer/Package/Loader/RootPackageLoader.php +++ b/app/vendor/composer/composer/src/Composer/Package/Loader/RootPackageLoader.php @@ -49,6 +49,11 @@ class RootPackageLoader extends ArrayLoader */ private $versionGuesser; + /** + * @var IOInterface|null + */ + private $io; + public function __construct(RepositoryManager $manager, Config $config, ?VersionParser $parser = null, ?VersionGuesser $versionGuesser = null, ?IOInterface $io = null) { parent::__construct($parser); @@ -56,6 +61,7 @@ public function __construct(RepositoryManager $manager, Config $config, ?Version $this->manager = $manager; $this->config = $config; $this->versionGuesser = $versionGuesser ?: new VersionGuesser($config, new ProcessExecutor($io), $this->versionParser); + $this->io = $io; } /** @@ -93,6 +99,14 @@ public function load(array $config, string $class = 'Composer\Package\RootPackag } if (!isset($config['version'])) { + if ($this->io !== null && $config['name'] !== '__root__' && 'project' !== ($config['type'] ?? '')) { + $this->io->warning( + sprintf( + "Composer could not detect the root package (%s) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version", + $config['name'] + ) + ); + } $config['version'] = '1.0.0'; $autoVersioned = true; } diff --git a/app/vendor/composer/composer/src/Composer/Package/Loader/ValidatingArrayLoader.php b/app/vendor/composer/composer/src/Composer/Package/Loader/ValidatingArrayLoader.php index a94448a61..6d1388e23 100644 --- a/app/vendor/composer/composer/src/Composer/Package/Loader/ValidatingArrayLoader.php +++ b/app/vendor/composer/composer/src/Composer/Package/Loader/ValidatingArrayLoader.php @@ -247,6 +247,12 @@ public function load(array $config, string $class = 'Composer\Package\CompletePa } } + $this->validateArray('php-ext'); + if (isset($this->config['php-ext']) && !in_array($this->config['type'] ?? '', ['php-ext', 'php-ext-zend'], true)) { + $this->errors[] = 'php-ext can only be set by packages of type "php-ext" or "php-ext-zend" which must be C extensions'; + unset($this->config['php-ext']); + } + $unboundConstraint = new Constraint('=', '10000000-dev'); foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { diff --git a/app/vendor/composer/composer/src/Composer/Package/Package.php b/app/vendor/composer/composer/src/Composer/Package/Package.php index 295bbd249..9d1b0ffc1 100644 --- a/app/vendor/composer/composer/src/Composer/Package/Package.php +++ b/app/vendor/composer/composer/src/Composer/Package/Package.php @@ -98,6 +98,8 @@ class Package extends BasePackage protected $isDefaultBranch = false; /** @var mixed[] */ protected $transportOptions = []; + /** @var array{priority?: int, configure-options?: list}|null */ + protected $phpExt = null; /** * Creates a new in memory package. @@ -590,6 +592,24 @@ public function getIncludePaths(): array return $this->includePaths; } + /** + * Sets the list of paths added to PHP's include path. + * + * @param array{extension-name?: string, priority?: int, support-zts?: bool, configure-options?: list}|null $phpExt List of directories. + */ + public function setPhpExt(?array $phpExt): void + { + $this->phpExt = $phpExt; + } + + /** + * @inheritDoc + */ + public function getPhpExt(): ?array + { + return $this->phpExt; + } + /** * Sets the notification URL */ diff --git a/app/vendor/composer/composer/src/Composer/Package/PackageInterface.php b/app/vendor/composer/composer/src/Composer/Package/PackageInterface.php index 4d874ef12..b7c9ecd45 100644 --- a/app/vendor/composer/composer/src/Composer/Package/PackageInterface.php +++ b/app/vendor/composer/composer/src/Composer/Package/PackageInterface.php @@ -181,6 +181,8 @@ public function getDistReference(): ?string; /** * Returns the sha1 checksum for the distribution archive of this version * + * Can be an empty string which should be treated as null + * * @return ?string */ public function getDistSha1Checksum(): ?string; @@ -321,6 +323,13 @@ public function getDevAutoload(): array; */ public function getIncludePaths(): array; + /** + * Returns the settings for php extension packages + * + * @return array{extension-name?: string, priority?: int, support-zts?: bool, configure-options?: list}|null + */ + public function getPhpExt(): ?array; + /** * Stores a reference to the repository that owns the package */ diff --git a/app/vendor/composer/composer/src/Composer/Platform/Version.php b/app/vendor/composer/composer/src/Composer/Platform/Version.php index 8a3f23e4a..56abb1bb1 100644 --- a/app/vendor/composer/composer/src/Composer/Platform/Version.php +++ b/app/vendor/composer/composer/src/Composer/Platform/Version.php @@ -20,7 +20,9 @@ class Version { /** - * @param bool $isFips Set by the method + * @param bool $isFips Set by the method + * + * @param-out bool $isFips */ public static function parseOpenssl(string $opensslVersion, ?bool &$isFips): ?string { diff --git a/app/vendor/composer/composer/src/Composer/Repository/PlatformRepository.php b/app/vendor/composer/composer/src/Composer/Repository/PlatformRepository.php index 2f8f206e8..61c70cf32 100644 --- a/app/vendor/composer/composer/src/Composer/Repository/PlatformRepository.php +++ b/app/vendor/composer/composer/src/Composer/Repository/PlatformRepository.php @@ -661,6 +661,7 @@ private function addExtension(string $name, string $prettyVersion): void $packageName = $this->buildPackageName($name); $ext = new CompletePackage($packageName, $version, $prettyVersion); $ext->setDescription('The '.$name.' PHP extension'.$extraDescription); + $ext->setType('php-ext'); if ($name === 'uuid') { $ext->setReplaces([ diff --git a/app/vendor/composer/composer/src/Composer/Repository/RepositorySet.php b/app/vendor/composer/composer/src/Composer/Repository/RepositorySet.php index 48cf424a4..f6e8c7802 100644 --- a/app/vendor/composer/composer/src/Composer/Repository/RepositorySet.php +++ b/app/vendor/composer/composer/src/Composer/Repository/RepositorySet.php @@ -310,10 +310,15 @@ public function isPackageAcceptable(array $names, string $stability): bool /** * Create a pool for dependency resolution from the packages in this repository set. + * + * @param list $ignoredTypes Packages of those types are ignored + * @param list|null $allowedTypes Only packages of those types are allowed if set to non-null */ - public function createPool(Request $request, IOInterface $io, ?EventDispatcher $eventDispatcher = null, ?PoolOptimizer $poolOptimizer = null): Pool + public function createPool(Request $request, IOInterface $io, ?EventDispatcher $eventDispatcher = null, ?PoolOptimizer $poolOptimizer = null, array $ignoredTypes = [], ?array $allowedTypes = null): Pool { $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher, $poolOptimizer, $this->temporaryConstraints); + $poolBuilder->setIgnoredTypes($ignoredTypes); + $poolBuilder->setAllowedTypes($allowedTypes); foreach ($this->repositories as $repo) { if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { diff --git a/app/vendor/composer/composer/src/Composer/Repository/Vcs/GitHubDriver.php b/app/vendor/composer/composer/src/Composer/Repository/Vcs/GitHubDriver.php index 44766a185..83c58887c 100644 --- a/app/vendor/composer/composer/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/app/vendor/composer/composer/src/Composer/Repository/Vcs/GitHubDriver.php @@ -286,6 +286,9 @@ private function getFundingInfo() case 'community_bridge': $result[$key]['url'] = 'https://funding.communitybridge.org/projects/' . basename($item['url']); break; + case 'buy_me_a_coffee': + $result[$key]['url'] = 'https://www.buymeacoffee.com/' . basename($item['url']); + break; } } diff --git a/app/vendor/composer/composer/src/Composer/Util/ErrorHandler.php b/app/vendor/composer/composer/src/Composer/Util/ErrorHandler.php index 4ed0cfefb..38cf84e44 100644 --- a/app/vendor/composer/composer/src/Composer/Util/ErrorHandler.php +++ b/app/vendor/composer/composer/src/Composer/Util/ErrorHandler.php @@ -63,7 +63,9 @@ public static function handle(int $level, string $message, string $file, int $li } return null; - }, array_slice(debug_backtrace(), 2)))); + }, array_slice(debug_backtrace(), 2)), function (?string $line) { + return $line !== null; + })); } } diff --git a/app/vendor/composer/composer/src/Composer/Util/Filesystem.php b/app/vendor/composer/composer/src/Composer/Util/Filesystem.php index 7ddcfa69f..ad0aae983 100644 --- a/app/vendor/composer/composer/src/Composer/Util/Filesystem.php +++ b/app/vendor/composer/composer/src/Composer/Util/Filesystem.php @@ -257,7 +257,21 @@ public function ensureDirectoryExists(string $directory) } if (!@mkdir($directory, 0777, true)) { - throw new \RuntimeException($directory.' does not exist and could not be created: '.(error_get_last()['message'] ?? '')); + $e = new \RuntimeException($directory.' does not exist and could not be created: '.(error_get_last()['message'] ?? '')); + + // in pathological cases with paths like path/to/broken-symlink/../foo is_dir will fail to detect path/to/foo + // but normalizing the ../ away first makes it work so we attempt this just in case, and if it still fails we + // report the initial error we had with the original path, and ignore the normalized path exception + // see https://github.com/composer/composer/issues/11864 + $normalized = $this->normalizePath($directory); + if ($normalized !== $directory) { + try { + $this->ensureDirectoryExists($normalized); + return; + } catch (\Throwable $ignoredEx) {} + } + + throw $e; } } } @@ -349,6 +363,9 @@ public function copyThenRemove(string $source, string $target) */ public function copy(string $source, string $target) { + // refs https://github.com/composer/composer/issues/11864 + $target = $this->normalizePath($target); + if (!is_dir($source)) { return copy($source, $target); } diff --git a/app/vendor/composer/composer/src/Composer/Util/Git.php b/app/vendor/composer/composer/src/Composer/Util/Git.php index f8e503d82..64b643216 100644 --- a/app/vendor/composer/composer/src/Composer/Util/Git.php +++ b/app/vendor/composer/composer/src/Composer/Util/Git.php @@ -298,7 +298,7 @@ public function syncMirror(string $url, string $dir): bool return true; } - public function fetchRefOrSyncMirror(string $url, string $dir, string $ref, string $prettyVersion = null): bool + public function fetchRefOrSyncMirror(string $url, string $dir, string $ref, ?string $prettyVersion = null): bool { if ($this->checkRefIsInMirror($dir, $ref)) { if (Preg::isMatch('{^[a-f0-9]{40}$}', $ref) && $prettyVersion !== null) { @@ -357,11 +357,12 @@ private function checkRefIsInMirror(string $dir, string $ref): bool } /** - * @param string[] $match + * @param array $match + * @param-out array $match */ private function isAuthenticationFailure(string $url, array &$match): bool { - if (!Preg::isMatch('{^(https?://)([^/]+)(.*)$}i', $url, $match)) { + if (!Preg::isMatchStrictGroups('{^(https?://)([^/]+)(.*)$}i', $url, $match)) { return false; } diff --git a/app/vendor/composer/composer/src/Composer/Util/Hg.php b/app/vendor/composer/composer/src/Composer/Util/Hg.php index 0e9f6e58f..c687542e0 100644 --- a/app/vendor/composer/composer/src/Composer/Util/Hg.php +++ b/app/vendor/composer/composer/src/Composer/Util/Hg.php @@ -58,10 +58,20 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd) } // Try with the authentication information available - if (Preg::isMatch('{^(https?)://((.+)(?:\:(.+))?@)?([^/]+)(/.*)?}mi', $url, $match) && $this->io->hasAuthentication((string) $match[5])) { - $auth = $this->io->getAuthentication((string) $match[5]); - $authenticatedUrl = $match[1] . '://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[5] . $match[6]; - + if ( + Preg::isMatch('{^(?Pssh|https?)://(?:(?P[^:@]+)(?::(?P[^:@]+))?@)?(?P[^/]+)(?P/.*)?}mi', $url, $matches) + && $this->io->hasAuthentication((string) $matches['host']) + ) { + if ($matches['proto'] === 'ssh') { + $user = ''; + if ($matches['user'] !== '' && $matches['user'] !== null) { + $user = rawurlencode($matches['user']) . '@'; + } + $authenticatedUrl = $matches['proto'] . '://' . $user . $matches['host'] . $matches['path']; + } else { + $auth = $this->io->getAuthentication((string) $matches['host']); + $authenticatedUrl = $matches['proto'] . '://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $matches['host'] . $matches['path']; + } $command = $commandCallable($authenticatedUrl); if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { @@ -70,10 +80,10 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd) $error = $this->process->getErrorOutput(); } else { - $error = 'The given URL (' . $url . ') does not match the required format (http(s)://(username:password@)example.com/path-to-repository)'; + $error = 'The given URL (' .$url. ') does not match the required format (ssh|http(s)://(username:password@)example.com/path-to-repository)'; } - $this->throwException('Failed to clone ' . $url . ', ' . "\n\n" . $error, $url); + $this->throwException("Failed to clone $url, \n\n" . $error, $url); } /** @@ -84,7 +94,9 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd) private function throwException($message, string $url): void { if (null === self::getVersion($this->process)) { - throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); + throw new \RuntimeException(Url::sanitize( + 'Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput() + )); } throw new \RuntimeException(Url::sanitize($message)); diff --git a/app/vendor/composer/composer/src/Composer/Util/Http/CurlDownloader.php b/app/vendor/composer/composer/src/Composer/Util/Http/CurlDownloader.php index f5bbe24aa..827d2a369 100644 --- a/app/vendor/composer/composer/src/Composer/Util/Http/CurlDownloader.php +++ b/app/vendor/composer/composer/src/Composer/Util/Http/CurlDownloader.php @@ -23,13 +23,14 @@ use Composer\Util\Url; use Composer\Util\HttpDownloader; use React\Promise\Promise; +use Symfony\Component\HttpFoundation\IpUtils; /** * @internal * @author Jordi Boggiano * @author Nicolas Grekas * @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int<0, max>, retries: int<0, max>, storeAuth: 'prompt'|bool, ipResolve: 4|6|null} - * @phpstan-type Job array{url: non-empty-string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: \CurlHandle, filename: string|null, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable} + * @phpstan-type Job array{url: non-empty-string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: \CurlHandle, filename: string|null, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable, primaryIp: string} */ class CurlDownloader { @@ -51,10 +52,6 @@ class CurlDownloader private $maxRedirects = 20; /** @var int */ private $maxRetries = 3; - /** @var ProxyManager */ - private $proxyManager; - /** @var bool */ - private $supportsSecureProxy; /** @var array */ protected $multiErrors = [ CURLM_BAD_HANDLE => ['CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'], @@ -116,11 +113,6 @@ public function __construct(IOInterface $io, Config $config, array $options = [] } $this->authHelper = new AuthHelper($io, $config); - $this->proxyManager = ProxyManager::getInstance(); - - $version = curl_version(); - $features = $version['features']; - $this->supportsSecureProxy = defined('CURL_VERSION_HTTPS_PROXY') && ($features & CURL_VERSION_HTTPS_PROXY); } /** @@ -225,10 +217,16 @@ private function initDownload(callable $resolve, callable $reject, string $origi $version = curl_version(); $features = $version['features']; - if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features)) { + if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features) !== 0) { curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); } + // curl 8.7.0 - 8.7.1 has a bug whereas automatic accept-encoding header results in an error when reading the response + // https://github.com/composer/composer/issues/11913 + if (isset($version['version']) && in_array($version['version'], ['8.7.0', '8.7.1'], true) && \defined('CURL_VERSION_LIBZ') && (CURL_VERSION_LIBZ & $features) !== 0) { + curl_setopt($curlHandle, CURLOPT_ENCODING, "gzip"); + } + $options['http']['header'] = $this->authHelper->addAuthenticationHeader($options['http']['header'], $origin, $url); $options = StreamContextFactory::initOptions($url, $options, true); @@ -244,26 +242,8 @@ private function initDownload(callable $resolve, callable $reject, string $origi } } - // Always set CURLOPT_PROXY to enable/disable proxy handling - // Any proxy authorization is included in the proxy url - $proxy = $this->proxyManager->getProxyForRequest($url); - if ($proxy->getUrl() !== '') { - curl_setopt($curlHandle, CURLOPT_PROXY, $proxy->getUrl()); - } - - // Curl needs certificate locations for secure proxies. - // CURLOPT_PROXY_SSL_VERIFY_PEER/HOST are enabled by default - if ($proxy->isSecure()) { - if (!$this->supportsSecureProxy) { - throw new TransportException('Connecting to a secure proxy using curl is not supported on PHP versions below 7.3.0.'); - } - if (!empty($options['ssl']['cafile'])) { - curl_setopt($curlHandle, CURLOPT_PROXY_CAINFO, $options['ssl']['cafile']); - } - if (!empty($options['ssl']['capath'])) { - curl_setopt($curlHandle, CURLOPT_PROXY_CAPATH, $options['ssl']['capath']); - } - } + $proxy = ProxyManager::getInstance()->getProxyForRequest($url); + curl_setopt_array($curlHandle, $proxy->getCurlOptions($options['ssl'] ?? [])); $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); @@ -279,9 +259,10 @@ private function initDownload(callable $resolve, callable $reject, string $origi 'bodyHandle' => $bodyHandle, 'resolve' => $resolve, 'reject' => $reject, + 'primaryIp' => '', ]; - $usingProxy = $proxy->getFormattedUrl(' using proxy (%s)'); + $usingProxy = $proxy->getStatus(' using proxy (%s)'); $ifModified = false !== stripos(implode(',', $options['http']['header']), 'if-modified-since:') ? ' if modified' : ''; if ($attributes['redirects'] === 0 && $attributes['retries'] === 0) { $this->io->writeError('Downloading ' . Url::sanitize($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG); @@ -505,6 +486,18 @@ public function tick(): void } } + if (isset($progress['primary_ip']) && $progress['primary_ip'] !== $this->jobs[$i]['primaryIp']) { + if ( + isset($this->jobs[$i]['options']['prevent_ip_access_callable']) && + is_callable($this->jobs[$i]['options']['prevent_ip_access_callable']) && + $this->jobs[$i]['options']['prevent_ip_access_callable']($progress['primary_ip']) + ) { + throw new TransportException(sprintf('IP "%s" is blocked for "%s".', $progress['primary_ip'], $progress['url'])); + } + + $this->jobs[$i]['primaryIp'] = (string) $progress['primary_ip']; + } + // TODO progress } } diff --git a/app/vendor/composer/composer/src/Composer/Util/Http/ProxyHelper.php b/app/vendor/composer/composer/src/Composer/Util/Http/ProxyHelper.php deleted file mode 100644 index 58056e429..000000000 --- a/app/vendor/composer/composer/src/Composer/Util/Http/ProxyHelper.php +++ /dev/null @@ -1,181 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Util\Http; - -/** - * Proxy discovery and helper class - * - * @internal - * @author John Stevenson - */ -class ProxyHelper -{ - /** - * Returns proxy environment values - * - * @return array{string|null, string|null, string|null} httpProxy, httpsProxy, noProxy values - * - * @throws \RuntimeException on malformed url - */ - public static function getProxyData(): array - { - $httpProxy = null; - $httpsProxy = null; - - // Handle http_proxy/HTTP_PROXY on CLI only for security reasons - if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { - if ($env = self::getProxyEnv(['http_proxy', 'HTTP_PROXY'], $name)) { - $httpProxy = self::checkProxy($env, $name); - } - } - - // Prefer CGI_HTTP_PROXY if available - if ($env = self::getProxyEnv(['CGI_HTTP_PROXY'], $name)) { - $httpProxy = self::checkProxy($env, $name); - } - - // Handle https_proxy/HTTPS_PROXY - if ($env = self::getProxyEnv(['https_proxy', 'HTTPS_PROXY'], $name)) { - $httpsProxy = self::checkProxy($env, $name); - } else { - $httpsProxy = $httpProxy; - } - - // Handle no_proxy - $noProxy = self::getProxyEnv(['no_proxy', 'NO_PROXY'], $name); - - return [$httpProxy, $httpsProxy, $noProxy]; - } - - /** - * Returns http context options for the proxy url - * - * @return array{http: array{proxy: string, header?: string}} - */ - public static function getContextOptions(string $proxyUrl): array - { - $proxy = parse_url($proxyUrl); - - // Remove any authorization - $proxyUrl = self::formatParsedUrl($proxy, false); - $proxyUrl = str_replace(['http://', 'https://'], ['tcp://', 'ssl://'], $proxyUrl); - - $options['http']['proxy'] = $proxyUrl; - - // Handle any authorization - if (isset($proxy['user'])) { - $auth = rawurldecode($proxy['user']); - - if (isset($proxy['pass'])) { - $auth .= ':' . rawurldecode($proxy['pass']); - } - $auth = base64_encode($auth); - // Set header as a string - $options['http']['header'] = "Proxy-Authorization: Basic {$auth}"; - } - - return $options; - } - - /** - * Sets/unsets request_fulluri value in http context options array - * - * @param mixed[] $options Set by method - */ - public static function setRequestFullUri(string $requestUrl, array &$options): void - { - if ('http' === parse_url($requestUrl, PHP_URL_SCHEME)) { - $options['http']['request_fulluri'] = true; - } else { - unset($options['http']['request_fulluri']); - } - } - - /** - * Searches $_SERVER for case-sensitive values - * - * @param string[] $names Names to search for - * @param string|null $name Name of any found value - * - * @return string|null The found value - */ - private static function getProxyEnv(array $names, ?string &$name): ?string - { - foreach ($names as $name) { - if (!empty($_SERVER[$name])) { - return $_SERVER[$name]; - } - } - - return null; - } - - /** - * Checks and formats a proxy url from the environment - * - * @throws \RuntimeException on malformed url - * @return string The formatted proxy url - */ - private static function checkProxy(string $proxyUrl, string $envName): string - { - $error = sprintf('malformed %s url', $envName); - $proxy = parse_url($proxyUrl); - - // We need parse_url to have identified a host - if (!isset($proxy['host'])) { - throw new \RuntimeException($error); - } - - $proxyUrl = self::formatParsedUrl($proxy, true); - - // We need a port because streams and curl use different defaults - if (!parse_url($proxyUrl, PHP_URL_PORT)) { - throw new \RuntimeException($error); - } - - return $proxyUrl; - } - - /** - * Formats a url from its component parts - * - * @param array{scheme?: string, host: string, port?: int, user?: string, pass?: string} $proxy - * - * @return string The formatted value - */ - private static function formatParsedUrl(array $proxy, bool $includeAuth): string - { - $proxyUrl = isset($proxy['scheme']) ? strtolower($proxy['scheme']) . '://' : ''; - - if ($includeAuth && isset($proxy['user'])) { - $proxyUrl .= $proxy['user']; - - if (isset($proxy['pass'])) { - $proxyUrl .= ':' . $proxy['pass']; - } - $proxyUrl .= '@'; - } - - $proxyUrl .= $proxy['host']; - - if (isset($proxy['port'])) { - $proxyUrl .= ':' . $proxy['port']; - } elseif (strpos($proxyUrl, 'http://') === 0) { - $proxyUrl .= ':80'; - } elseif (strpos($proxyUrl, 'https://') === 0) { - $proxyUrl .= ':443'; - } - - return $proxyUrl; - } -} diff --git a/app/vendor/composer/composer/src/Composer/Util/Http/ProxyItem.php b/app/vendor/composer/composer/src/Composer/Util/Http/ProxyItem.php new file mode 100644 index 000000000..2839be923 --- /dev/null +++ b/app/vendor/composer/composer/src/Composer/Util/Http/ProxyItem.php @@ -0,0 +1,119 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util\Http; + +/** + * @internal + * @author John Stevenson + */ +class ProxyItem +{ + /** @var non-empty-string */ + private $url; + /** @var non-empty-string */ + private $safeUrl; + /** @var ?non-empty-string */ + private $curlAuth; + /** @var string */ + private $optionsProxy; + /** @var ?non-empty-string */ + private $optionsAuth; + + /** + * @param string $proxyUrl The value from the environment + * @param string $envName The name of the environment variable + * @throws \RuntimeException If the proxy url is invalid + */ + public function __construct(string $proxyUrl, string $envName) + { + $syntaxError = sprintf('unsupported `%s` syntax', $envName); + + if (strpbrk($proxyUrl, "\r\n\t") !== false) { + throw new \RuntimeException($syntaxError); + } + if (false === ($proxy = parse_url($proxyUrl))) { + throw new \RuntimeException($syntaxError); + } + if (!isset($proxy['host'])) { + throw new \RuntimeException('unable to find proxy host in ' . $envName); + } + + $scheme = isset($proxy['scheme']) ? strtolower($proxy['scheme']) . '://' : 'http://'; + $safe = ''; + + if (isset($proxy['user'])) { + $safe = '***'; + $user = $proxy['user']; + $auth = rawurldecode($proxy['user']); + + if (isset($proxy['pass'])) { + $safe .= ':***'; + $user .= ':' . $proxy['pass']; + $auth .= ':' . rawurldecode($proxy['pass']); + } + + $safe .= '@'; + + if (strlen($user) > 0) { + $this->curlAuth = $user; + $this->optionsAuth = 'Proxy-Authorization: Basic ' . base64_encode($auth); + } + } + + $host = $proxy['host']; + $port = null; + + if (isset($proxy['port'])) { + $port = $proxy['port']; + } elseif ($scheme === 'http://') { + $port = 80; + } elseif ($scheme === 'https://') { + $port = 443; + } + + // We need a port because curl uses 1080 for http. Port 0 is reserved, + // but is considered valid depending on the PHP or Curl version. + if ($port === null) { + throw new \RuntimeException('unable to find proxy port in ' . $envName); + } + if ($port === 0) { + throw new \RuntimeException('port 0 is reserved in ' . $envName); + } + + $this->url = sprintf('%s%s:%d', $scheme, $host, $port); + $this->safeUrl = sprintf('%s%s%s:%d', $scheme, $safe, $host, $port); + + $scheme = str_replace(['http://', 'https://'], ['tcp://', 'ssl://'], $scheme); + $this->optionsProxy = sprintf('%s%s:%d', $scheme, $host, $port); + } + + /** + * Returns a RequestProxy instance for the scheme of the request url + * + * @param string $scheme The scheme of the request url + */ + public function toRequestProxy(string $scheme): RequestProxy + { + $options = ['http' => ['proxy' => $this->optionsProxy]]; + + if ($this->optionsAuth !== null) { + $options['http']['header'] = $this->optionsAuth; + } + + if ($scheme === 'http') { + $options['http']['request_fulluri'] = true; + } + + return new RequestProxy($this->url, $this->curlAuth, $options, $this->safeUrl); + } +} diff --git a/app/vendor/composer/composer/src/Composer/Util/Http/ProxyManager.php b/app/vendor/composer/composer/src/Composer/Util/Http/ProxyManager.php index 5af53ad0c..0571780fe 100644 --- a/app/vendor/composer/composer/src/Composer/Util/Http/ProxyManager.php +++ b/app/vendor/composer/composer/src/Composer/Util/Http/ProxyManager.php @@ -14,7 +14,6 @@ use Composer\Downloader\TransportException; use Composer\Util\NoProxyPattern; -use Composer\Util\Url; /** * @internal @@ -24,40 +23,40 @@ class ProxyManager { /** @var ?string */ private $error = null; - /** @var array{http: ?string, https: ?string} */ - private $fullProxy; - /** @var array{http: ?string, https: ?string} */ - private $safeProxy; - /** @var array{http: array{options: mixed[]|null}, https: array{options: mixed[]|null}} */ - private $streams; - /** @var bool */ - private $hasProxy; - /** @var ?string */ - private $info = null; + /** @var ?ProxyItem */ + private $httpProxy = null; + /** @var ?ProxyItem */ + private $httpsProxy = null; /** @var ?NoProxyPattern */ private $noProxyHandler = null; - /** @var ?ProxyManager */ + /** @var ?self */ private static $instance = null; + /** The following 3 properties can be removed after the transition period */ + + /** @var bool */ + private $ignoreHttpsProxy = false; + /** @var bool */ + private $isTransitional = false; + /** @var bool */ + private $needsTransitionWarning = false; + private function __construct() { - $this->fullProxy = $this->safeProxy = [ - 'http' => null, - 'https' => null, - ]; - - $this->streams['http'] = $this->streams['https'] = [ - 'options' => null, - ]; + // this can be removed after the transition period + $this->isTransitional = true; - $this->hasProxy = false; - $this->initProxyData(); + try { + $this->getProxyData(); + } catch (\RuntimeException $e) { + $this->error = $e->getMessage(); + } } public static function getInstance(): ProxyManager { - if (!self::$instance) { + if (self::$instance === null) { self::$instance = new self(); } @@ -79,95 +78,116 @@ public static function reset(): void */ public function getProxyForRequest(string $requestUrl): RequestProxy { - if ($this->error) { + if ($this->error !== null) { throw new TransportException('Unable to use a proxy: '.$this->error); } - $scheme = parse_url($requestUrl, PHP_URL_SCHEME) ?: 'http'; - $proxyUrl = ''; - $options = []; - $formattedProxyUrl = ''; - - if ($this->hasProxy && in_array($scheme, ['http', 'https'], true) && $this->fullProxy[$scheme]) { - if ($this->noProxy($requestUrl)) { - $formattedProxyUrl = 'excluded by no_proxy'; - } else { - $proxyUrl = $this->fullProxy[$scheme]; - $options = $this->streams[$scheme]['options']; - ProxyHelper::setRequestFullUri($requestUrl, $options); - $formattedProxyUrl = $this->safeProxy[$scheme]; - } + $scheme = (string) parse_url($requestUrl, PHP_URL_SCHEME); + $proxy = $this->getProxyForScheme($scheme); + + if ($proxy === null) { + return RequestProxy::none(); + } + + if ($this->noProxy($requestUrl)) { + return RequestProxy::noProxy(); } - return new RequestProxy($proxyUrl, $options, $formattedProxyUrl); + return $proxy->toRequestProxy($scheme); } /** - * Returns true if a proxy is being used + * Returns true if the user needs to set an https_proxy environment variable * - * @return bool If false any error will be in $message + * This method can be removed after the transition period */ - public function isProxying(): bool + public function needsTransitionWarning(): bool { - return $this->hasProxy; + return $this->needsTransitionWarning; } /** - * Returns proxy configuration info which can be shown to the user - * - * @return string|null Safe proxy URL or an error message if setting up proxy failed or null if no proxy was configured + * Returns a ProxyItem if one is set for the scheme, otherwise null */ - public function getFormattedProxy(): ?string + private function getProxyForScheme(string $scheme): ?ProxyItem { - return $this->hasProxy ? $this->info : $this->error; + if ($scheme === 'http') { + return $this->httpProxy; + } + + if ($scheme === 'https') { + // this can be removed after the transition period + if ($this->isTransitional && $this->httpsProxy === null) { + if ($this->httpProxy !== null && !$this->ignoreHttpsProxy) { + $this->needsTransitionWarning = true; + + return $this->httpProxy; + } + } + + return $this->httpsProxy; + } + + return null; } /** - * Initializes proxy values from the environment + * Finds proxy values from the environment and sets class properties */ - private function initProxyData(): void + private function getProxyData(): void { - try { - [$httpProxy, $httpsProxy, $noProxy] = ProxyHelper::getProxyData(); - } catch (\RuntimeException $e) { - $this->error = $e->getMessage(); - - return; + // Handle http_proxy/HTTP_PROXY on CLI only for security reasons + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + [$env, $name] = $this->getProxyEnv('http_proxy'); + if ($env !== null) { + $this->httpProxy = new ProxyItem($env, $name); + } } - $info = []; - - if ($httpProxy) { - $info[] = $this->setData($httpProxy, 'http'); + // Handle cgi_http_proxy/CGI_HTTP_PROXY if needed + if ($this->httpProxy === null) { + [$env, $name] = $this->getProxyEnv('cgi_http_proxy'); + if ($env !== null) { + $this->httpProxy = new ProxyItem($env, $name); + } } - if ($httpsProxy) { - $info[] = $this->setData($httpsProxy, 'https'); + + // Handle https_proxy/HTTPS_PROXY + [$env, $name] = $this->getProxyEnv('https_proxy'); + if ($env !== null) { + $this->httpsProxy = new ProxyItem($env, $name); } - if ($this->hasProxy) { - $this->info = implode(', ', $info); - if ($noProxy) { - $this->noProxyHandler = new NoProxyPattern($noProxy); - } + + // Handle no_proxy/NO_PROXY + [$env, $name] = $this->getProxyEnv('no_proxy'); + if ($env !== null) { + $this->noProxyHandler = new NoProxyPattern($env); } } /** - * Sets initial data - * - * @param non-empty-string $url Proxy url - * @param 'http'|'https' $scheme Environment variable scheme + * Searches $_SERVER for case-sensitive values * - * @return non-empty-string + * @return array{0: string|null, 1: string} value, name */ - private function setData($url, $scheme): string + private function getProxyEnv(string $envName): array { - $safeProxy = Url::sanitize($url); - $this->fullProxy[$scheme] = $url; - $this->safeProxy[$scheme] = $safeProxy; - $this->streams[$scheme]['options'] = ProxyHelper::getContextOptions($url); - $this->hasProxy = true; + $names = [strtolower($envName), strtoupper($envName)]; + + foreach ($names as $name) { + if (is_string($_SERVER[$name] ?? null)) { + if ($_SERVER[$name] !== '') { + return [$_SERVER[$name], $name]; + } + // this can be removed after the transition period + if ($this->isTransitional && strtolower($name) === 'https_proxy') { + $this->ignoreHttpsProxy = true; + break; + } + } + } - return sprintf('%s=%s', $scheme, $safeProxy); + return [null, '']; } /** @@ -175,6 +195,10 @@ private function setData($url, $scheme): string */ private function noProxy(string $requestUrl): bool { - return $this->noProxyHandler && $this->noProxyHandler->test($requestUrl); + if ($this->noProxyHandler === null) { + return false; + } + + return $this->noProxyHandler->test($requestUrl); } } diff --git a/app/vendor/composer/composer/src/Composer/Util/Http/RequestProxy.php b/app/vendor/composer/composer/src/Composer/Util/Http/RequestProxy.php index c80c8799e..d9df68861 100644 --- a/app/vendor/composer/composer/src/Composer/Util/Http/RequestProxy.php +++ b/app/vendor/composer/composer/src/Composer/Util/Http/RequestProxy.php @@ -12,78 +12,157 @@ namespace Composer\Util\Http; -use Composer\Util\Url; +use Composer\Downloader\TransportException; /** * @internal * @author John Stevenson + * + * @phpstan-type contextOptions array{http: array{proxy: string, header?: string, request_fulluri?: bool}} */ class RequestProxy { - /** @var mixed[] */ + /** @var ?contextOptions */ private $contextOptions; - /** @var bool */ - private $isSecure; - /** @var string */ - private $formattedUrl; - /** @var string */ + /** @var ?non-empty-string */ + private $status; + /** @var ?non-empty-string */ private $url; + /** @var ?non-empty-string */ + private $auth; /** - * @param mixed[] $contextOptions + * @param ?non-empty-string $url The proxy url, without authorization + * @param ?non-empty-string $auth Authorization for curl + * @param ?contextOptions $contextOptions + * @param ?non-empty-string $status */ - public function __construct(string $url, array $contextOptions, string $formattedUrl) + public function __construct(?string $url, ?string $auth, ?array $contextOptions, ?string $status) { $this->url = $url; + $this->auth = $auth; $this->contextOptions = $contextOptions; - $this->formattedUrl = $formattedUrl; - $this->isSecure = 0 === strpos($url, 'https://'); + $this->status = $status; + } + + public static function none(): RequestProxy + { + return new self(null, null, null, null); + } + + public static function noProxy(): RequestProxy + { + return new self(null, null, null, 'excluded by no_proxy'); } /** - * Returns an array of context options + * Returns the context options to use for this request, otherwise null * - * @return mixed[] + * @return ?contextOptions */ - public function getContextOptions(): array + public function getContextOptions(): ?array { return $this->contextOptions; } /** - * Returns the safe proxy url from the last request + * Returns an array of curl proxy options + * + * @param array $sslOptions + * @return array + */ + public function getCurlOptions(array $sslOptions): array + { + if ($this->isSecure() && !$this->supportsSecureProxy()) { + throw new TransportException('Cannot use an HTTPS proxy. PHP >= 7.3 and cUrl >= 7.52.0 are required.'); + } + + // Always set a proxy url, even an empty value, because it tells curl + // to ignore proxy environment variables + $options = [CURLOPT_PROXY => (string) $this->url]; + + // If using a proxy, tell curl to ignore no_proxy environment variables + if ($this->url !== null) { + $options[CURLOPT_NOPROXY] = ''; + } + + // Set any authorization + if ($this->auth !== null) { + $options[CURLOPT_PROXYAUTH] = CURLAUTH_BASIC; + $options[CURLOPT_PROXYUSERPWD] = $this->auth; + } + + if ($this->isSecure()) { + if (isset($sslOptions['cafile'])) { + $options[CURLOPT_PROXY_CAINFO] = $sslOptions['cafile']; + } + if (isset($sslOptions['capath'])) { + $options[CURLOPT_PROXY_CAPATH] = $sslOptions['capath']; + } + } + + return $options; + } + + /** + * Returns proxy info associated with this request * - * @param string|null $format Output format specifier - * @return string Safe proxy, no proxy or empty + * An empty return value means that the user has not set a proxy. + * A non-empty value will either be the sanitized proxy url if a proxy is + * required, or a message indicating that a no_proxy value has disabled the + * proxy. + * + * @param ?string $format Output format specifier */ - public function getFormattedUrl(?string $format = ''): string + public function getStatus(?string $format = null): string { - $result = ''; - if ($this->formattedUrl) { - $format = $format ?: '%s'; - $result = sprintf($format, $this->formattedUrl); + if ($this->status === null) { + return ''; + } + + $format = $format ?? '%s'; + if (strpos($format, '%s') !== false) { + return sprintf($format, $this->status); } - return $result; + throw new \InvalidArgumentException('String format specifier is missing'); } /** - * Returns the proxy url + * Returns true if the request url has been excluded by a no_proxy value * - * @return string Proxy url or empty + * A false value can also mean that the user has not set a proxy. */ - public function getUrl(): string + public function isExcludedByNoProxy(): bool { - return $this->url; + return $this->status !== null && $this->url === null; } /** - * Returns true if this is a secure-proxy + * Returns true if this is a secure (HTTPS) proxy * - * @return bool False if not secure or there is no proxy + * A false value means that this is either an HTTP proxy, or that a proxy + * is not required for this request, or that the user has not set a proxy. */ public function isSecure(): bool { - return $this->isSecure; + return 0 === strpos((string) $this->url, 'https://'); + } + + /** + * Returns true if an HTTPS proxy can be used. + * + * This depends on PHP7.3+ for CURL_VERSION_HTTPS_PROXY + * and curl including the feature (from version 7.52.0) + */ + public function supportsSecureProxy(): bool + { + if (false === ($version = curl_version()) || !defined('CURL_VERSION_HTTPS_PROXY')) { + return false; + } + + $features = $version['features']; + + return (bool) ($features & CURL_VERSION_HTTPS_PROXY); } } diff --git a/app/vendor/composer/composer/src/Composer/Util/Platform.php b/app/vendor/composer/composer/src/Composer/Util/Platform.php index 3c971d105..45060c85c 100644 --- a/app/vendor/composer/composer/src/Composer/Util/Platform.php +++ b/app/vendor/composer/composer/src/Composer/Util/Platform.php @@ -55,6 +55,8 @@ public static function getCwd(bool $allowEmpty = false): string /** * getenv() equivalent but reads from the runtime global variables first * + * @param non-empty-string $name + * * @return string|false */ public static function getEnv(string $name) @@ -99,6 +101,7 @@ public static function expandPath(string $path): string return Preg::replaceCallback('#^(\$|(?P%))(?P\w++)(?(percent)%)(?P.*)#', static function ($matches): string { assert(is_string($matches['var'])); + assert('' !== $matches['var']); // Treat HOME as an alias for USERPROFILE on Windows for legacy reasons if (Platform::isWindows() && $matches['var'] === 'HOME') { @@ -149,7 +152,9 @@ public static function isWindowsSubsystemForLinux(): bool !ini_get('open_basedir') && is_readable('/proc/version') && false !== stripos((string)Silencer::call('file_get_contents', '/proc/version'), 'microsoft') - && !file_exists('/.dockerenv') // docker running inside WSL should not be seen as WSL + && !file_exists('/.dockerenv') // Docker and Podman running inside WSL should not be seen as WSL + && !file_exists('/run/.containerenv') + && !file_exists('/var/run/.containerenv') ) { return self::$isWindowsSubsystemForLinux = true; } diff --git a/app/vendor/composer/composer/src/Composer/Util/RemoteFilesystem.php b/app/vendor/composer/composer/src/Composer/Util/RemoteFilesystem.php index 8abf015a7..560ef35db 100644 --- a/app/vendor/composer/composer/src/Composer/Util/RemoteFilesystem.php +++ b/app/vendor/composer/composer/src/Composer/Util/RemoteFilesystem.php @@ -64,8 +64,6 @@ class RemoteFilesystem private $redirects; /** @var int */ private $maxRedirects = 20; - /** @var ProxyManager */ - private $proxyManager; /** * Constructor. @@ -91,7 +89,6 @@ public function __construct(IOInterface $io, Config $config, array $options = [] $this->options = array_replace_recursive($this->options, $options); $this->config = $config; $this->authHelper = $authHelper ?? new AuthHelper($io, $config); - $this->proxyManager = ProxyManager::getInstance(); } /** @@ -249,6 +246,10 @@ protected function get(string $originUrl, string $fileUrl, array $additionalOpti $origFileUrl = $fileUrl; + if (isset($options['prevent_ip_access_callable'])) { + throw new \RuntimeException("RemoteFilesystem doesn't support the 'prevent_ip_access_callable' config."); + } + if (isset($options['gitlab-token'])) { $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['gitlab-token']; unset($options['gitlab-token']); @@ -272,8 +273,8 @@ protected function get(string $originUrl, string $fileUrl, array $additionalOpti $ctx = StreamContextFactory::getContext($fileUrl, $options, ['notification' => [$this, 'callbackGet']]); - $proxy = $this->proxyManager->getProxyForRequest($fileUrl); - $usingProxy = $proxy->getFormattedUrl(' using proxy (%s)'); + $proxy = ProxyManager::getInstance()->getProxyForRequest($fileUrl); + $usingProxy = $proxy->getStatus(' using proxy (%s)'); $this->io->writeError((strpos($origFileUrl, 'http') === 0 ? 'Downloading ' : 'Reading ') . Url::sanitize($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); unset($origFileUrl, $proxy, $usingProxy); @@ -509,6 +510,8 @@ protected function get(string $originUrl, string $fileUrl, array $additionalOpti * @param int $maxFileSize The maximum allowed file size * * @return string|false The response contents or false on failure + * + * @param-out list $responseHeaders */ protected function getRemoteContents(string $originUrl, string $fileUrl, $context, ?array &$responseHeaders = null, ?int $maxFileSize = null) { diff --git a/app/vendor/composer/composer/src/Composer/Util/StreamContextFactory.php b/app/vendor/composer/composer/src/Composer/Util/StreamContextFactory.php index 57fbe0f0e..be4c976a9 100644 --- a/app/vendor/composer/composer/src/Composer/Util/StreamContextFactory.php +++ b/app/vendor/composer/composer/src/Composer/Util/StreamContextFactory.php @@ -76,7 +76,8 @@ public static function initOptions(string $url, array $options, bool $forCurl = // Add stream proxy options if there is a proxy if (!$forCurl) { $proxy = ProxyManager::getInstance()->getProxyForRequest($url); - if ($proxyOptions = $proxy->getContextOptions()) { + $proxyOptions = $proxy->getContextOptions(); + if ($proxyOptions !== null) { $isHttpsRequest = 0 === strpos($url, 'https://'); if ($proxy->isSecure()) { diff --git a/app/vendor/composer/composer/src/Composer/Util/TlsHelper.php b/app/vendor/composer/composer/src/Composer/Util/TlsHelper.php index aca227270..5ab2bf9c9 100644 --- a/app/vendor/composer/composer/src/Composer/Util/TlsHelper.php +++ b/app/vendor/composer/composer/src/Composer/Util/TlsHelper.php @@ -76,13 +76,18 @@ public static function getCertificateNames($certificate): ?array if (isset($info['extensions']['subjectAltName'])) { $subjectAltNames = Preg::split('{\s*,\s*}', $info['extensions']['subjectAltName']); - $subjectAltNames = array_filter(array_map(static function ($name): ?string { - if (0 === strpos($name, 'DNS:')) { - return strtolower(ltrim(substr($name, 4))); + $subjectAltNames = array_filter( + array_map(static function ($name): ?string { + if (0 === strpos($name, 'DNS:')) { + return strtolower(ltrim(substr($name, 4))); + } + + return null; + }, $subjectAltNames), + function (?string $san) { + return $san !== null; } - - return null; - }, $subjectAltNames)); + ); $subjectAltNames = array_values($subjectAltNames); } diff --git a/app/vendor/composer/installed.json b/app/vendor/composer/installed.json index a2fd4c65a..fdbb72411 100644 --- a/app/vendor/composer/installed.json +++ b/app/vendor/composer/installed.json @@ -114,17 +114,17 @@ }, { "name": "cakephp/cakephp", - "version": "4.5.4", - "version_normalized": "4.5.4.0", + "version": "4.6.0", + "version_normalized": "4.6.0.0", "source": { "type": "git", "url": "https://github.com/cakephp/cakephp.git", - "reference": "a28d65864a67d52482dba82207a4a22c15a59ef4" + "reference": "b8585672346c0654311c77500ce613cdf37687cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/cakephp/zipball/a28d65864a67d52482dba82207a4a22c15a59ef4", - "reference": "a28d65864a67d52482dba82207a4a22c15a59ef4", + "url": "https://api.github.com/repos/cakephp/cakephp/zipball/b8585672346c0654311c77500ce613cdf37687cc", + "reference": "b8585672346c0654311c77500ce613cdf37687cc", "shasum": "" }, "require": { @@ -181,7 +181,7 @@ "lib-ICU": "To use locale-aware features in the I18n and Database packages", "paragonie/csp-builder": "CSP builder, to use the CSP Middleware" }, - "time": "2024-03-02T03:23:45+00:00", + "time": "2025-03-23T02:36:48+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -285,17 +285,17 @@ }, { "name": "cakephp/chronos", - "version": "2.4.4", - "version_normalized": "2.4.4.0", + "version": "2.4.5", + "version_normalized": "2.4.5.0", "source": { "type": "git", "url": "https://github.com/cakephp/chronos.git", - "reference": "03208c18eb3267490662e68671bb9c22aa804492" + "reference": "b0321ab7658af9e7abcb3dd876f226e6f3dbb81f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/chronos/zipball/03208c18eb3267490662e68671bb9c22aa804492", - "reference": "03208c18eb3267490662e68671bb9c22aa804492", + "url": "https://api.github.com/repos/cakephp/chronos/zipball/b0321ab7658af9e7abcb3dd876f226e6f3dbb81f", + "reference": "b0321ab7658af9e7abcb3dd876f226e6f3dbb81f", "shasum": "" }, "require": { @@ -305,7 +305,7 @@ "cakephp/cakephp-codesniffer": "^4.5", "phpunit/phpunit": "^8.0 || ^9.0" }, - "time": "2023-11-03T05:26:08+00:00", + "time": "2024-07-30T22:26:11+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -597,31 +597,31 @@ }, { "name": "composer/ca-bundle", - "version": "1.4.1", - "version_normalized": "1.4.1.0", + "version": "1.5.6", + "version_normalized": "1.5.6.0", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd" + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", - "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f65c239c970e7f072f067ab78646e9f0b2935175", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "psr/log": "^1.0", - "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, - "time": "2024-02-23T10:16:52+00:00", + "time": "2025-03-06T14:30:56+00:00", "type": "library", "extra": { "branch-alias": { @@ -656,7 +656,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.4.1" + "source": "https://github.com/composer/ca-bundle/tree/1.5.6" }, "funding": [ { @@ -752,17 +752,17 @@ }, { "name": "composer/composer", - "version": "2.7.1", - "version_normalized": "2.7.1.0", + "version": "2.7.6", + "version_normalized": "2.7.6.0", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc" + "reference": "fabd995783b633829fd4280e272284b39b6ae702" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", - "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", + "url": "https://api.github.com/repos/composer/composer/zipball/fabd995783b633829fd4280e272284b39b6ae702", + "reference": "fabd995783b633829fd4280e272284b39b6ae702", "shasum": "" }, "require": { @@ -801,19 +801,19 @@ "ext-zip": "Enabling the zip extension allows you to unzip archives", "ext-zlib": "Allow gzip compression of HTTP requests" }, - "time": "2024-02-09T14:26:28+00:00", + "time": "2024-05-04T21:03:15+00:00", "bin": [ "bin/composer" ], "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.7-dev" - }, "phpstan": { "includes": [ "phpstan/rules.neon" ] + }, + "branch-alias": { + "dev-main": "2.7-dev" } }, "installation-source": "dist", @@ -849,7 +849,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.7.1" + "source": "https://github.com/composer/composer/tree/2.7.6" }, "funding": [ { @@ -2116,33 +2116,33 @@ }, { "name": "laminas/laminas-httphandlerrunner", - "version": "2.10.0", - "version_normalized": "2.10.0.0", + "version": "2.11.0", + "version_normalized": "2.11.0.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-httphandlerrunner.git", - "reference": "35a0ba92e940a2f9533754f5a56187fa321f7693" + "reference": "c428d9f67f280d155637cbe2b7245b5188c8cdae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-httphandlerrunner/zipball/35a0ba92e940a2f9533754f5a56187fa321f7693", - "reference": "35a0ba92e940a2f9533754f5a56187fa321f7693", + "url": "https://api.github.com/repos/laminas/laminas-httphandlerrunner/zipball/c428d9f67f280d155637cbe2b7245b5188c8cdae", + "reference": "c428d9f67f280d155637cbe2b7245b5188c8cdae", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "psr/http-message": "^1.0 || ^2.0", "psr/http-message-implementation": "^1.0 || ^2.0", "psr/http-server-handler": "^1.0" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.5.0", - "laminas/laminas-diactoros": "^3.3.0", - "phpunit/phpunit": "^10.5.5", - "psalm/plugin-phpunit": "^0.18.4", - "vimeo/psalm": "^5.18" + "laminas/laminas-coding-standard": "~3.0.0", + "laminas/laminas-diactoros": "^3.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^5.26.1" }, - "time": "2024-01-04T10:50:34+00:00", + "time": "2024-10-17T20:37:17+00:00", "type": "library", "extra": { "laminas": { @@ -3549,24 +3549,24 @@ }, { "name": "psr/http-factory", - "version": "1.0.2", - "version_normalized": "1.0.2.0", + "version": "1.1.0", + "version_normalized": "1.1.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, - "time": "2023-04-10T20:10:41+00:00", + "time": "2024-04-15T12:06:14+00:00", "type": "library", "extra": { "branch-alias": { @@ -3589,7 +3589,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -3601,7 +3601,7 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, "install-path": "../psr/http-factory" }, @@ -6502,23 +6502,23 @@ }, { "name": "symfony/process", - "version": "v7.0.4", - "version_normalized": "7.0.4.0", + "version": "v7.2.5", + "version_normalized": "7.2.5.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9" + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/0e7727191c3b71ebec6d529fa0e50a01ca5679e9", - "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9", + "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", "shasum": "" }, "require": { "php": ">=8.2" }, - "time": "2024-02-22T20:27:20+00:00", + "time": "2025-03-13T12:21:46+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6546,7 +6546,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.0.4" + "source": "https://github.com/symfony/process/tree/v7.2.5" }, "funding": [ { @@ -6950,33 +6950,40 @@ }, { "name": "twig/twig", - "version": "v3.8.0", - "version_normalized": "3.8.0.0", + "version": "v3.20.0", + "version_normalized": "3.20.0.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d" + "reference": "3468920399451a384bef53cf7996965f7cd40183" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183", + "reference": "3468920399451a384bef53cf7996965f7cd40183", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php80": "^1.22" + "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { + "phpstan/phpstan": "^2.0", "psr/container": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0" + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, - "time": "2023-11-21T18:54:41+00:00", + "time": "2025-02-13T08:34:43+00:00", "type": "library", "installation-source": "dist", "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -7009,7 +7016,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.8.0" + "source": "https://github.com/twigphp/Twig/tree/v3.20.0" }, "funding": [ { diff --git a/app/vendor/composer/installed.php b/app/vendor/composer/installed.php index 64bb5e4c5..f84ac603a 100644 --- a/app/vendor/composer/installed.php +++ b/app/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'cakephp/app', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'd3454ff41d7456735e8553ca6919a2645f65754a', + 'reference' => 'c74c4238cec5e773ee785ea12ff87f2e2d371be0', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'cakephp/app' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'd3454ff41d7456735e8553ca6919a2645f65754a', + 'reference' => 'c74c4238cec5e773ee785ea12ff87f2e2d371be0', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -40,13 +40,13 @@ 'cakephp/cache' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/cakephp' => array( - 'pretty_version' => '4.5.4', - 'version' => '4.5.4.0', - 'reference' => 'a28d65864a67d52482dba82207a4a22c15a59ef4', + 'pretty_version' => '4.6.0', + 'version' => '4.6.0.0', + 'reference' => 'b8585672346c0654311c77500ce613cdf37687cc', 'type' => 'library', 'install_path' => __DIR__ . '/../cakephp/cakephp', 'aliases' => array(), @@ -62,9 +62,9 @@ 'dev_requirement' => true, ), 'cakephp/chronos' => array( - 'pretty_version' => '2.4.4', - 'version' => '2.4.4.0', - 'reference' => '03208c18eb3267490662e68671bb9c22aa804492', + 'pretty_version' => '2.4.5', + 'version' => '2.4.5.0', + 'reference' => 'b0321ab7658af9e7abcb3dd876f226e6f3dbb81f', 'type' => 'library', 'install_path' => __DIR__ . '/../cakephp/chronos', 'aliases' => array(), @@ -73,31 +73,31 @@ 'cakephp/collection' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/console' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/core' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/database' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/datasource' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/debug_kit' => array( @@ -112,37 +112,37 @@ 'cakephp/event' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/filesystem' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/form' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/http' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/i18n' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/log' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/migrations' => array( @@ -157,7 +157,7 @@ 'cakephp/orm' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/plugin-installer' => array( @@ -181,19 +181,19 @@ 'cakephp/utility' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'cakephp/validation' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '4.5.4', + 0 => '4.6.0', ), ), 'composer/ca-bundle' => array( - 'pretty_version' => '1.4.1', - 'version' => '1.4.1.0', - 'reference' => '3ce240142f6d59b808dd65c1f52f7a1c252e6cfd', + 'pretty_version' => '1.5.6', + 'version' => '1.5.6.0', + 'reference' => 'f65c239c970e7f072f067ab78646e9f0b2935175', 'type' => 'library', 'install_path' => __DIR__ . '/./ca-bundle', 'aliases' => array(), @@ -209,9 +209,9 @@ 'dev_requirement' => true, ), 'composer/composer' => array( - 'pretty_version' => '2.7.1', - 'version' => '2.7.1.0', - 'reference' => 'aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc', + 'pretty_version' => '2.7.6', + 'version' => '2.7.6.0', + 'reference' => 'fabd995783b633829fd4280e272284b39b6ae702', 'type' => 'library', 'install_path' => __DIR__ . '/./composer', 'aliases' => array(), @@ -362,9 +362,9 @@ 'dev_requirement' => false, ), 'laminas/laminas-httphandlerrunner' => array( - 'pretty_version' => '2.10.0', - 'version' => '2.10.0.0', - 'reference' => '35a0ba92e940a2f9533754f5a56187fa321f7693', + 'pretty_version' => '2.11.0', + 'version' => '2.11.0.0', + 'reference' => 'c428d9f67f280d155637cbe2b7245b5188c8cdae', 'type' => 'library', 'install_path' => __DIR__ . '/../laminas/laminas-httphandlerrunner', 'aliases' => array(), @@ -570,9 +570,9 @@ ), ), 'psr/http-factory' => array( - 'pretty_version' => '1.0.2', - 'version' => '1.0.2.0', - 'reference' => 'e616d01114759c4c489f93b099585439f795fe35', + 'pretty_version' => '1.1.0', + 'version' => '1.1.0.0', + 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-factory', 'aliases' => array(), @@ -994,9 +994,9 @@ 'dev_requirement' => true, ), 'symfony/process' => array( - 'pretty_version' => 'v7.0.4', - 'version' => '7.0.4.0', - 'reference' => '0e7727191c3b71ebec6d529fa0e50a01ca5679e9', + 'pretty_version' => 'v7.2.5', + 'version' => '7.2.5.0', + 'reference' => '87b7c93e57df9d8e39a093d32587702380ff045d', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/process', 'aliases' => array(), @@ -1048,9 +1048,9 @@ 'dev_requirement' => true, ), 'twig/twig' => array( - 'pretty_version' => 'v3.8.0', - 'version' => '3.8.0.0', - 'reference' => '9d15f0ac07f44dc4217883ec6ae02fd555c6f71d', + 'pretty_version' => 'v3.20.0', + 'version' => '3.20.0.0', + 'reference' => '3468920399451a384bef53cf7996965f7cd40183', 'type' => 'library', 'install_path' => __DIR__ . '/../twig/twig', 'aliases' => array(), diff --git a/app/vendor/laminas/laminas-httphandlerrunner/.laminas-ci.json b/app/vendor/laminas/laminas-httphandlerrunner/.laminas-ci.json new file mode 100644 index 000000000..6238f6f3d --- /dev/null +++ b/app/vendor/laminas/laminas-httphandlerrunner/.laminas-ci.json @@ -0,0 +1,6 @@ +{ + "ignore_php_platform_requirements": { + "8.4": true + }, + "backwardCompatibilityCheck": true +} diff --git a/app/vendor/laminas/laminas-httphandlerrunner/composer.json b/app/vendor/laminas/laminas-httphandlerrunner/composer.json index 8a77f9b06..a729714ff 100644 --- a/app/vendor/laminas/laminas-httphandlerrunner/composer.json +++ b/app/vendor/laminas/laminas-httphandlerrunner/composer.json @@ -33,17 +33,17 @@ } }, "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "psr/http-message": "^1.0 || ^2.0", "psr/http-message-implementation": "^1.0 || ^2.0", "psr/http-server-handler": "^1.0" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.5.0", - "laminas/laminas-diactoros": "^3.3.0", - "phpunit/phpunit": "^10.5.5", - "psalm/plugin-phpunit": "^0.18.4", - "vimeo/psalm": "^5.18" + "laminas/laminas-coding-standard": "~3.0.0", + "laminas/laminas-diactoros": "^3.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^5.26.1" }, "autoload": { "psr-4": { diff --git a/app/vendor/psr/http-factory/composer.json b/app/vendor/psr/http-factory/composer.json index d1bbddeea..82a1d3266 100644 --- a/app/vendor/psr/http-factory/composer.json +++ b/app/vendor/psr/http-factory/composer.json @@ -1,6 +1,6 @@ { "name": "psr/http-factory", - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "psr", "psr-7", @@ -18,8 +18,11 @@ "homepage": "https://www.php-fig.org/" } ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "autoload": { diff --git a/app/vendor/psr/http-factory/src/UploadedFileFactoryInterface.php b/app/vendor/psr/http-factory/src/UploadedFileFactoryInterface.php index 7db4e30af..d7adbf0e2 100644 --- a/app/vendor/psr/http-factory/src/UploadedFileFactoryInterface.php +++ b/app/vendor/psr/http-factory/src/UploadedFileFactoryInterface.php @@ -15,10 +15,10 @@ interface UploadedFileFactoryInterface * * @param StreamInterface $stream Underlying stream representing the * uploaded file content. - * @param int $size in bytes + * @param int|null $size in bytes * @param int $error PHP file upload error - * @param string $clientFilename Filename as provided by the client, if any. - * @param string $clientMediaType Media type as provided by the client, if any. + * @param string|null $clientFilename Filename as provided by the client, if any. + * @param string|null $clientMediaType Media type as provided by the client, if any. * * @return UploadedFileInterface * @@ -26,9 +26,9 @@ interface UploadedFileFactoryInterface */ public function createUploadedFile( StreamInterface $stream, - int $size = null, + ?int $size = null, int $error = \UPLOAD_ERR_OK, - string $clientFilename = null, - string $clientMediaType = null + ?string $clientFilename = null, + ?string $clientMediaType = null ): UploadedFileInterface; } diff --git a/app/vendor/symfony/process/CHANGELOG.md b/app/vendor/symfony/process/CHANGELOG.md index e26819b5b..3e33cd0bc 100644 --- a/app/vendor/symfony/process/CHANGELOG.md +++ b/app/vendor/symfony/process/CHANGELOG.md @@ -1,13 +1,17 @@ CHANGELOG ========= +7.1 +--- + + * Add `Process::setIgnoredSignals()` to disable signal propagation to the child process + 6.4 --- * Add `PhpSubprocess` to handle PHP subprocesses that take over the configuration from their parent * Add `RunProcessMessage` and `RunProcessMessageHandler` - * Support using `Process::findExecutable()` independently of `open_basedir` 5.2.0 ----- diff --git a/app/vendor/symfony/process/Exception/ProcessFailedException.php b/app/vendor/symfony/process/Exception/ProcessFailedException.php index 499809eef..de8a9e983 100644 --- a/app/vendor/symfony/process/Exception/ProcessFailedException.php +++ b/app/vendor/symfony/process/Exception/ProcessFailedException.php @@ -20,15 +20,14 @@ */ class ProcessFailedException extends RuntimeException { - private Process $process; - - public function __construct(Process $process) - { + public function __construct( + private Process $process, + ) { if ($process->isSuccessful()) { throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); } - $error = sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", + $error = \sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText(), @@ -36,7 +35,7 @@ public function __construct(Process $process) ); if (!$process->isOutputDisabled()) { - $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", + $error .= \sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput() ); diff --git a/app/vendor/symfony/process/Exception/ProcessSignaledException.php b/app/vendor/symfony/process/Exception/ProcessSignaledException.php index 0fed8ac30..3fd13e5d7 100644 --- a/app/vendor/symfony/process/Exception/ProcessSignaledException.php +++ b/app/vendor/symfony/process/Exception/ProcessSignaledException.php @@ -20,13 +20,10 @@ */ final class ProcessSignaledException extends RuntimeException { - private Process $process; - - public function __construct(Process $process) - { - $this->process = $process; - - parent::__construct(sprintf('The process has been signaled with signal "%s".', $process->getTermSignal())); + public function __construct( + private Process $process, + ) { + parent::__construct(\sprintf('The process has been signaled with signal "%s".', $process->getTermSignal())); } public function getProcess(): Process diff --git a/app/vendor/symfony/process/Exception/ProcessStartFailedException.php b/app/vendor/symfony/process/Exception/ProcessStartFailedException.php new file mode 100644 index 000000000..372547259 --- /dev/null +++ b/app/vendor/symfony/process/Exception/ProcessStartFailedException.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception for processes failed during startup. + */ +class ProcessStartFailedException extends ProcessFailedException +{ + public function __construct( + private Process $process, + ?string $message, + ) { + if ($process->isStarted()) { + throw new InvalidArgumentException('Expected a process that failed during startup, but the given process was started successfully.'); + } + + $error = \sprintf('The command "%s" failed.'."\n\nWorking directory: %s\n\nError: %s", + $process->getCommandLine(), + $process->getWorkingDirectory(), + $message ?? 'unknown' + ); + + // Skip parent constructor + RuntimeException::__construct($error); + } + + public function getProcess(): Process + { + return $this->process; + } +} diff --git a/app/vendor/symfony/process/Exception/ProcessTimedOutException.php b/app/vendor/symfony/process/Exception/ProcessTimedOutException.php index 252e11127..d3fe49342 100644 --- a/app/vendor/symfony/process/Exception/ProcessTimedOutException.php +++ b/app/vendor/symfony/process/Exception/ProcessTimedOutException.php @@ -23,15 +23,11 @@ class ProcessTimedOutException extends RuntimeException public const TYPE_GENERAL = 1; public const TYPE_IDLE = 2; - private Process $process; - private int $timeoutType; - - public function __construct(Process $process, int $timeoutType) - { - $this->process = $process; - $this->timeoutType = $timeoutType; - - parent::__construct(sprintf( + public function __construct( + private Process $process, + private int $timeoutType, + ) { + parent::__construct(\sprintf( 'The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout() @@ -58,7 +54,7 @@ public function getExceededTimeout(): ?float return match ($this->timeoutType) { self::TYPE_GENERAL => $this->process->getTimeout(), self::TYPE_IDLE => $this->process->getIdleTimeout(), - default => throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)), + default => throw new \LogicException(\sprintf('Unknown timeout type "%d".', $this->timeoutType)), }; } } diff --git a/app/vendor/symfony/process/ExecutableFinder.php b/app/vendor/symfony/process/ExecutableFinder.php index ceb7a5588..5cc652512 100644 --- a/app/vendor/symfony/process/ExecutableFinder.php +++ b/app/vendor/symfony/process/ExecutableFinder.php @@ -19,7 +19,15 @@ */ class ExecutableFinder { - private array $suffixes = ['.exe', '.bat', '.cmd', '.com']; + private const CMD_BUILTINS = [ + 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', + 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', + 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', + 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', + 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', + ]; + + private array $suffixes = []; /** * Replaces default suffixes of executable. @@ -30,7 +38,10 @@ public function setSuffixes(array $suffixes): void } /** - * Adds new possible suffix to check for executable. + * Adds new possible suffix to check for executable, including the dot (.). + * + * $finder = new ExecutableFinder(); + * $finder->addSuffix('.foo'); */ public function addSuffix(string $suffix): void { @@ -46,18 +57,27 @@ public function addSuffix(string $suffix): void */ public function find(string $name, ?string $default = null, array $extraDirs = []): ?string { + // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes + if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) { + return $name; + } + $dirs = array_merge( explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), $extraDirs ); - $suffixes = ['']; + $suffixes = $this->suffixes; if ('\\' === \DIRECTORY_SEPARATOR) { $pathExt = getenv('PATHEXT'); - $suffixes = array_merge($pathExt ? explode(\PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes); + $suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']); } + $suffixes = '' !== pathinfo($name, PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { + if ('' === $dir) { + $dir = '.'; + } if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { return $file; } @@ -68,8 +88,13 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } } - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && @is_executable($executablePath)) { + if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { + return $default; + } + + $execResult = exec('command -v -- '.escapeshellarg($name)); + + if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) { return $executablePath; } diff --git a/app/vendor/symfony/process/InputStream.php b/app/vendor/symfony/process/InputStream.php index cd91029e8..586e74293 100644 --- a/app/vendor/symfony/process/InputStream.php +++ b/app/vendor/symfony/process/InputStream.php @@ -46,7 +46,7 @@ public function write(mixed $input): void return; } if ($this->isClosed()) { - throw new RuntimeException(sprintf('"%s" is closed.', static::class)); + throw new RuntimeException(\sprintf('"%s" is closed.', static::class)); } $this->input[] = ProcessUtils::validateInput(__METHOD__, $input); } diff --git a/app/vendor/symfony/process/Messenger/RunProcessContext.php b/app/vendor/symfony/process/Messenger/RunProcessContext.php index b5ade0722..5e2230404 100644 --- a/app/vendor/symfony/process/Messenger/RunProcessContext.php +++ b/app/vendor/symfony/process/Messenger/RunProcessContext.php @@ -27,7 +27,7 @@ public function __construct( Process $process, ) { $this->exitCode = $process->getExitCode(); - $this->output = $process->isOutputDisabled() ? null : $process->getOutput(); - $this->errorOutput = $process->isOutputDisabled() ? null : $process->getErrorOutput(); + $this->output = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getOutput(); + $this->errorOutput = !$process->isStarted() || $process->isOutputDisabled() ? null : $process->getErrorOutput(); } } diff --git a/app/vendor/symfony/process/PhpExecutableFinder.php b/app/vendor/symfony/process/PhpExecutableFinder.php index 4a882e0f2..9f9218f98 100644 --- a/app/vendor/symfony/process/PhpExecutableFinder.php +++ b/app/vendor/symfony/process/PhpExecutableFinder.php @@ -32,15 +32,8 @@ public function __construct() public function find(bool $includeArgs = true): string|false { if ($php = getenv('PHP_BINARY')) { - if (!is_executable($php)) { - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if (\function_exists('exec') && $php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { - if (!is_executable($php)) { - return false; - } - } else { - return false; - } + if (!is_executable($php) && !$php = $this->executableFinder->find($php)) { + return false; } if (@is_dir($php)) { @@ -81,6 +74,10 @@ public function find(bool $includeArgs = true): string|false $dirs[] = 'C:\xampp\php\\'; } + if ($herdPath = getenv('HERD_HOME')) { + $dirs[] = $herdPath.\DIRECTORY_SEPARATOR.'bin'; + } + return $this->executableFinder->find('php', false, $dirs); } diff --git a/app/vendor/symfony/process/PhpProcess.php b/app/vendor/symfony/process/PhpProcess.php index 01d88954d..0e7ff8464 100644 --- a/app/vendor/symfony/process/PhpProcess.php +++ b/app/vendor/symfony/process/PhpProcess.php @@ -52,7 +52,7 @@ public function __construct(string $script, ?string $cwd = null, ?array $env = n public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static { - throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); + throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } public function start(?callable $callback = null, array $env = []): void diff --git a/app/vendor/symfony/process/PhpSubprocess.php b/app/vendor/symfony/process/PhpSubprocess.php index a97f8b26e..bdd4173c2 100644 --- a/app/vendor/symfony/process/PhpSubprocess.php +++ b/app/vendor/symfony/process/PhpSubprocess.php @@ -75,7 +75,7 @@ public function __construct(array $command, ?string $cwd = null, ?array $env = n public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static { - throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); + throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } public function start(?callable $callback = null, array $env = []): void @@ -106,7 +106,7 @@ private function writeTmpIni(array $iniFiles, string $tmpDir): string throw new RuntimeException('Unable to read ini: '.$file); } // Check and remove directives after HOST and PATH sections - if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches)) { + if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches, \PREG_OFFSET_CAPTURE)) { $data = substr($data, 0, $matches[0][1]); } diff --git a/app/vendor/symfony/process/Pipes/AbstractPipes.php b/app/vendor/symfony/process/Pipes/AbstractPipes.php index cbbb72770..51a566f3b 100644 --- a/app/vendor/symfony/process/Pipes/AbstractPipes.php +++ b/app/vendor/symfony/process/Pipes/AbstractPipes.php @@ -101,7 +101,7 @@ protected function write(): ?array } elseif (!isset($this->inputBuffer[0])) { if (!\is_string($input)) { if (!\is_scalar($input)) { - throw new InvalidArgumentException(sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', get_debug_type($this->input), get_debug_type($input))); + throw new InvalidArgumentException(\sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', get_debug_type($this->input), get_debug_type($input))); } $input = (string) $input; } diff --git a/app/vendor/symfony/process/Pipes/UnixPipes.php b/app/vendor/symfony/process/Pipes/UnixPipes.php index 7bd0db0e9..6f95a3328 100644 --- a/app/vendor/symfony/process/Pipes/UnixPipes.php +++ b/app/vendor/symfony/process/Pipes/UnixPipes.php @@ -22,16 +22,12 @@ */ class UnixPipes extends AbstractPipes { - private ?bool $ttyMode; - private bool $ptyMode; - private bool $haveReadSupport; - - public function __construct(?bool $ttyMode, bool $ptyMode, mixed $input, bool $haveReadSupport) - { - $this->ttyMode = $ttyMode; - $this->ptyMode = $ptyMode; - $this->haveReadSupport = $haveReadSupport; - + public function __construct( + private ?bool $ttyMode, + private bool $ptyMode, + mixed $input, + private bool $haveReadSupport, + ) { parent::__construct($input); } @@ -74,7 +70,7 @@ public function getDescriptors(): array return [ ['pty'], ['pty'], - ['pty'], + ['pipe', 'w'], // stderr needs to be in a pipe to correctly split error and output, since PHP will use the same stream for both ]; } diff --git a/app/vendor/symfony/process/Pipes/WindowsPipes.php b/app/vendor/symfony/process/Pipes/WindowsPipes.php index 8033442a4..116b8e30e 100644 --- a/app/vendor/symfony/process/Pipes/WindowsPipes.php +++ b/app/vendor/symfony/process/Pipes/WindowsPipes.php @@ -33,12 +33,11 @@ class WindowsPipes extends AbstractPipes Process::STDOUT => 0, Process::STDERR => 0, ]; - private bool $haveReadSupport; - - public function __construct(mixed $input, bool $haveReadSupport) - { - $this->haveReadSupport = $haveReadSupport; + public function __construct( + mixed $input, + private bool $haveReadSupport, + ) { if ($this->haveReadSupport) { // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. // Workaround for this problem is to use temporary files instead of pipes on Windows platform. @@ -53,7 +52,7 @@ public function __construct(mixed $input, bool $haveReadSupport) set_error_handler(function ($type, $msg) use (&$lastError) { $lastError = $msg; }); for ($i = 0;; ++$i) { foreach ($pipes as $pipe => $name) { - $file = sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); + $file = \sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); if (!$h = fopen($file.'.lock', 'w')) { if (file_exists($file.'.lock')) { diff --git a/app/vendor/symfony/process/Process.php b/app/vendor/symfony/process/Process.php index 1aaf730de..6fe1086ea 100644 --- a/app/vendor/symfony/process/Process.php +++ b/app/vendor/symfony/process/Process.php @@ -15,6 +15,7 @@ use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessSignaledException; +use Symfony\Component\Process\Exception\ProcessStartFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\Pipes\UnixPipes; @@ -76,13 +77,14 @@ class Process implements \IteratorAggregate private bool $tty = false; private bool $pty; private array $options = ['suppress_errors' => true, 'bypass_shell' => true]; + private array $ignoredSignals = []; private WindowsPipes|UnixPipes $processPipes; private ?int $latestSignal = null; - private ?int $cachedExitCode = null; private static ?bool $sigchild = null; + private static array $executables = []; /** * Exit codes translation table. @@ -234,11 +236,11 @@ public function __clone() * * @return int The exit status code * - * @throws RuntimeException When process can't be launched - * @throws RuntimeException When process is already running - * @throws ProcessTimedOutException When process timed out - * @throws ProcessSignaledException When process stopped after receiving signal - * @throws LogicException In case a callback is provided and output has been disabled + * @throws ProcessStartFailedException When process can't be launched + * @throws RuntimeException When process is already running + * @throws ProcessTimedOutException When process timed out + * @throws ProcessSignaledException When process stopped after receiving signal + * @throws LogicException In case a callback is provided and output has been disabled * * @final */ @@ -285,9 +287,9 @@ public function mustRun(?callable $callback = null, array $env = []): static * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * - * @throws RuntimeException When process can't be launched - * @throws RuntimeException When process is already running - * @throws LogicException In case a callback is provided and output has been disabled + * @throws ProcessStartFailedException When process can't be launched + * @throws RuntimeException When process is already running + * @throws LogicException In case a callback is provided and output has been disabled */ public function start(?callable $callback = null, array $env = []): void { @@ -307,12 +309,7 @@ public function start(?callable $callback = null, array $env = []): void $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv(); if (\is_array($commandline = $this->commandline)) { - $commandline = implode(' ', array_map($this->escapeArgument(...), $commandline)); - - if ('\\' !== \DIRECTORY_SEPARATOR) { - // exec is mandatory to deal with sending a signal to the process - $commandline = 'exec '.$commandline; - } + $commandline = array_values(array_map(strval(...), $commandline)); } else { $commandline = $this->replacePlaceholders($commandline, $env); } @@ -323,6 +320,11 @@ public function start(?callable $callback = null, array $env = []): void // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors[3] = ['pipe', 'w']; + if (\is_array($commandline)) { + // exec is mandatory to deal with sending a signal to the process + $commandline = 'exec '.$this->buildShellCommandline($commandline); + } + // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; $commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code'; @@ -336,13 +338,42 @@ public function start(?callable $callback = null, array $env = []): void } if (!is_dir($this->cwd)) { - throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd)); + throw new RuntimeException(\sprintf('The provided cwd "%s" does not exist.', $this->cwd)); } - $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + $lastError = null; + set_error_handler(function ($type, $msg) use (&$lastError) { + $lastError = $msg; + + return true; + }); - if (!\is_resource($process)) { - throw new RuntimeException('Unable to launch a new process.'); + $oldMask = []; + + if ($this->ignoredSignals && \function_exists('pcntl_sigprocmask')) { + // we block signals we want to ignore, as proc_open will use fork / posix_spawn which will copy the signal mask this allow to block + // signals in the child process + pcntl_sigprocmask(\SIG_BLOCK, $this->ignoredSignals, $oldMask); + } + + try { + $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + + // Ensure array vs string commands behave the same + if (!$process && \is_array($commandline)) { + $process = @proc_open('exec '.$this->buildShellCommandline($commandline), $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + } + } finally { + if ($this->ignoredSignals && \function_exists('pcntl_sigprocmask')) { + // we restore the signal mask here to avoid any side effects + pcntl_sigprocmask(\SIG_SETMASK, $oldMask); + } + + restore_error_handler(); + } + + if (!$process) { + throw new ProcessStartFailedException($this, $lastError); } $this->process = $process; $this->status = self::STATUS_STARTED; @@ -367,8 +398,8 @@ public function start(?callable $callback = null, array $env = []): void * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * - * @throws RuntimeException When process can't be launched - * @throws RuntimeException When process is already running + * @throws ProcessStartFailedException When process can't be launched + * @throws RuntimeException When process is already running * * @see start() * @@ -944,7 +975,7 @@ public function getLastOutputTime(): ?float */ public function getCommandLine(): string { - return \is_array($this->commandline) ? implode(' ', array_map($this->escapeArgument(...), $this->commandline)) : $this->commandline; + return $this->buildShellCommandline($this->commandline); } /** @@ -1189,12 +1220,26 @@ public function setOptions(array $options): void foreach ($options as $key => $value) { if (!\in_array($key, $existingOptions)) { $this->options = $defaultOptions; - throw new LogicException(sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions))); + throw new LogicException(\sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions))); } $this->options[$key] = $value; } } + /** + * Defines a list of posix signals that will not be propagated to the process. + * + * @param list<\SIG*> $signals + */ + public function setIgnoredSignals(array $signals): void + { + if ($this->isRunning()) { + throw new RuntimeException('Setting ignored signals while the process is running is not possible.'); + } + + $this->ignoredSignals = $signals; + } + /** * Returns whether TTY is supported on the current operating system. */ @@ -1202,7 +1247,7 @@ public static function isTtySupported(): bool { static $isTtySupported; - return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)); + return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT) && @is_writable('/dev/tty')); } /** @@ -1278,21 +1323,10 @@ protected function updateStatus(bool $blocking): void return; } - $this->processInformation = proc_get_status($this->process); - $running = $this->processInformation['running']; - - // In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call. - // Subsequent calls return -1 as the process is discarded. This workaround caches the first - // retrieved exit status for consistent results in later calls, mimicking PHP 8.3 behavior. - if (\PHP_VERSION_ID < 80300) { - if (!isset($this->cachedExitCode) && !$running && -1 !== $this->processInformation['exitcode']) { - $this->cachedExitCode = $this->processInformation['exitcode']; - } - - if (isset($this->cachedExitCode) && !$running && -1 === $this->processInformation['exitcode']) { - $this->processInformation['exitcode'] = $this->cachedExitCode; - } + if ($this->processInformation['running'] ?? true) { + $this->processInformation = proc_get_status($this->process); } + $running = $this->processInformation['running']; $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); @@ -1389,8 +1423,9 @@ private function readPipes(bool $blocking, bool $close): void private function close(): int { $this->processPipes->close(); - if (\is_resource($this->process)) { + if ($this->process) { proc_close($this->process); + $this->process = null; } $this->exitcode = $this->processInformation['exitcode']; $this->status = self::STATUS_TERMINATED; @@ -1444,6 +1479,11 @@ private function resetProcessData(): void */ private function doSignal(int $signal, bool $throwException): bool { + // Signal seems to be send when sigchild is enable, this allow blocking the signal correctly in this case + if ($this->isSigchildEnabled() && \in_array($signal, $this->ignoredSignals)) { + return false; + } + if (null === $pid = $this->getPid()) { if ($throwException) { throw new LogicException('Cannot send signal on a non running process.'); @@ -1453,10 +1493,10 @@ private function doSignal(int $signal, bool $throwException): bool } if ('\\' === \DIRECTORY_SEPARATOR) { - exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); + exec(\sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); if ($exitCode && $this->isRunning()) { if ($throwException) { - throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output))); + throw new RuntimeException(\sprintf('Unable to kill the process (%s).', implode(' ', $output))); } return false; @@ -1466,12 +1506,12 @@ private function doSignal(int $signal, bool $throwException): bool $ok = @proc_terminate($this->process, $signal); } elseif (\function_exists('posix_kill')) { $ok = @posix_kill($pid, $signal); - } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { + } elseif ($ok = proc_open(\sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { $ok = false === fgets($pipes[2]); } if (!$ok) { if ($throwException) { - throw new RuntimeException(sprintf('Error while sending signal "%s".', $signal)); + throw new RuntimeException(\sprintf('Error while sending signal "%s".', $signal)); } return false; @@ -1486,9 +1526,25 @@ private function doSignal(int $signal, bool $throwException): bool return true; } - private function prepareWindowsCommandLine(string $cmd, array &$env): string + private function buildShellCommandline(string|array $commandline): string { - $uid = uniqid('', true); + if (\is_string($commandline)) { + return $commandline; + } + + if ('\\' === \DIRECTORY_SEPARATOR && isset($commandline[0][0]) && \strlen($commandline[0]) === strcspn($commandline[0], ':/\\')) { + // On Windows, we don't rely on the OS to find the executable if possible to avoid lookups + // in the current directory which could be untrusted. Instead we use the ExecutableFinder. + $commandline[0] = (self::$executables[$commandline[0]] ??= (new ExecutableFinder())->find($commandline[0])) ?? $commandline[0]; + } + + return implode(' ', array_map($this->escapeArgument(...), $commandline)); + } + + private function prepareWindowsCommandLine(string|array $cmd, array &$env): string + { + $cmd = $this->buildShellCommandline($cmd); + $uid = bin2hex(random_bytes(4)); $cmd = preg_replace_callback( '/"(?:( [^"%!^]*+ @@ -1524,7 +1580,14 @@ function ($m) use (&$env, $uid) { $cmd ); - $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; + static $comSpec; + + if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) { + // Escape according to CommandLineToArgvW rules + $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec) .'"'; + } + + $cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; foreach ($this->processPipes->getFiles() as $offset => $filename) { $cmd .= ' '.$offset.'>"'.$filename.'"'; } @@ -1540,7 +1603,7 @@ function ($m) use (&$env, $uid) { private function requireProcessIsStarted(string $functionName): void { if (!$this->isStarted()) { - throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName)); + throw new LogicException(\sprintf('Process must be started before calling "%s()".', $functionName)); } } @@ -1552,7 +1615,7 @@ private function requireProcessIsStarted(string $functionName): void private function requireProcessIsTerminated(string $functionName): void { if (!$this->isTerminated()) { - throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName)); + throw new LogicException(\sprintf('Process must be terminated before calling "%s()".', $functionName)); } } @@ -1570,7 +1633,7 @@ private function escapeArgument(?string $argument): string if (str_contains($argument, "\0")) { $argument = str_replace("\0", '?', $argument); } - if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) { + if (!preg_match('/[()%!^"<>&|\s]/', $argument)) { return $argument; } $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); @@ -1582,7 +1645,7 @@ private function replacePlaceholders(string $commandline, array $env): string { return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) { if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) { - throw new InvalidArgumentException(sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline); + throw new InvalidArgumentException(\sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline); } return $this->escapeArgument($env[$matches[1]]); diff --git a/app/vendor/symfony/process/ProcessUtils.php b/app/vendor/symfony/process/ProcessUtils.php index 092c5ccf7..a2dbde9f7 100644 --- a/app/vendor/symfony/process/ProcessUtils.php +++ b/app/vendor/symfony/process/ProcessUtils.php @@ -56,7 +56,7 @@ public static function validateInput(string $caller, mixed $input): mixed return new \IteratorIterator($input); } - throw new InvalidArgumentException(sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller)); + throw new InvalidArgumentException(\sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller)); } return $input; diff --git a/app/vendor/twig/twig/CHANGELOG b/app/vendor/twig/twig/CHANGELOG index 2b8341fd8..5d7ea9527 100644 --- a/app/vendor/twig/twig/CHANGELOG +++ b/app/vendor/twig/twig/CHANGELOG @@ -1,3 +1,245 @@ +# 3.20.0 (2025-02-13) + + * Fix support for ignoring syntax errors in an undefined handler in guard + * Add configuration for Commonmark + * Fix wrong array index + * Bump minimum PHP version to 8.1 + * Add support for registering callbacks for undefined functions, filters or token parsers in the IntegrationTestCase + * Use correct line number for `ForElseNode` + * Fix timezone conversion on strings + +# 3.19.0 (2025-01-28) + + * Fix a security issue where escaping was missing when using `??` + * Deprecate `Token::getType()`, use `Token::test()` instead + * Add `Token::toEnglish()` + * Add `ForElseNode` + * Deprecate `Twig\ExpressionParser::parseOnlyArguments()` and + `Twig\ExpressionParser::parseArguments()` (use + `Twig\ExpressionParser::parseNamedArguments()` instead) + * Fix `constant()` behavior when used with `??` + * Add the `invoke` filter + * Make `{}` optional for the `types` tag + * Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes + * Ignore static properties when using the dot operator + +# 3.18.0 (2024-12-29) + + * Fix unary operator precedence change + * Ignore `SyntaxError` exceptions from undefined handlers when using the `guard` tag + * Add a way to stream template rendering (`TemplateWrapper::stream()` and `TemplateWrapper::streamBlock()`) + +# 3.17.1 (2024-12-12) + + * Fix the null coalescing operator when the test returns null + * Fix the Elvis operator when used as '? :' instead of '?:' + * Support for invoking closures + +# 3.17.0 (2024-12-10) + + * Fix ArrayAccess with objects as keys + * Support underscores in number literals + * Deprecate `ConditionalExpression` and `NullCoalesceExpression` (use `ConditionalTernary` and `NullCoalesceBinary` instead) + +# 3.16.0 (2024-11-29) + + * Deprecate `InlinePrint` + * Fix having macro variables starting with an underscore + * Deprecate not passing a `Source` instance to `TokenStream` + * Deprecate returning `null` from `TwigFilter::getSafe()` and `TwigFunction::getSafe()`, return `[]` instead + +# 3.15.0 (2024-11-17) + + * [BC BREAK] Add support for accessing class constants with the dot operator; + this can be a BC break if you don't use UPPERCASE constant names + * Add Spanish inflector support for the `plural` and `singular` filters in the String extension + * Deprecate `TempNameExpression` in favor of `LocalVariable` + * Deprecate `NameExpression` in favor of `ContextVariable` + * Deprecate `AssignNameExpression` in favor of `AssignContextVariable` + * Remove `MacroAutoImportNodeVisitor` + * Deprecate `MethodCallExpression` in favor of `MacroReferenceExpression` + * Fix support for the "is defined" test on `_self.xxx` (auto-imported) macros + * Fix support for the "is defined" test on inherited macros + * Add named arguments support for the dot operator arguments (`foo.bar(some: arg)`) + * Add named arguments support for macros + * Add a new `guard` tag that allows to test if some Twig callables are available at compilation time + * Allow arrow functions everywhere + * Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a `\Closure`) + * Add support for triggering deprecations for future operator precedence changes + * Deprecate using the `not` unary operator in an expression with ``*``, ``/``, ``//``, or ``%`` without using explicit parentheses to clarify precedence + * Deprecate using the `??` binary operator without explicit parentheses + * Deprecate using the `~` binary operator in an expression with `+` or `-` without using parentheses to clarify precedence + * Deprecate not passing `AbstractExpression` args to most constructor arguments for classes extending `AbstractExpression` + * Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`) + * Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead. + * Add support for inline comments + * Add `Profile::getStartTime()` and `Profile::getEndTime()` + * Fix "ignore missing" when used on an "embed" tag + * Fix the possibility to override an aliased block (via use) + * Add template cache hot reload + * Allow Twig callable argument names to be free-form (snake-case or camelCase) independently of the PHP callable signature + They were automatically converted to snake-cased before + * Deprecate the `attribute` function; use the `.` notation and wrap the name with parenthesis instead + * Add support for argument unpackaging + * Add JSON support for the file extension escaping strategy + * Support Markup instances (and any other \Stringable) as dynamic mapping keys + * Deprecate the `sandbox` tag + * Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options) + * Add the `enum` function + * Add support for logical `xor` operator + +# 3.14.2 (2024-11-07) + + * Fix an infinite recursion in the sandbox code + +# 3.14.1 (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.14.0 (2024-09-09) + + * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context + * Add the possibility to reset globals via `Environment::resetGlobals()` + * Deprecate `Environment::mergeGlobals()` + +# 3.13.0 (2024-09-07) + + * Add the `types` tag (experimental) + * Deprecate the `Twig\Test\NodeTestCase::getTests()` data provider, override `provideTests()` instead. + * Mark `Twig\Test\NodeTestCase::getEnvironment()` as final, override `createEnvironment()` instead. + * Deprecate `Twig\Test\NodeTestCase::getVariableGetter()`, call `createVariableGetter()` instead. + * Deprecate `Twig\Test\NodeTestCase::getAttributeGetter()`, call `createAttributeGetter()` instead. + * Deprecate not overriding `Twig\Test\IntegrationTestCase::getFixturesDirectory()`, this method will be abstract in 4.0 + * Marked `Twig\Test\IntegrationTestCase::getTests()` and `getLegacyTests()` as final + +# 3.12.0 (2024-08-29) + + * Deprecate the fact that the `extends` and `use` tags are always allowed in a sandboxed template. + This behavior will change in 4.0 where these tags will need to be explicitly allowed like any other tag. + * Deprecate the "tag" constructor argument of the "Twig\Node\Node" class as the tag is now automatically set by the Parser when needed + * Fix precedence of two-word tests when the first word is a valid test + * Deprecate the `spaceless` filter + * Deprecate some internal methods from `Parser`: `getBlockStack()`, `hasBlock()`, `getBlock()`, `hasMacro()`, `hasTraits()`, `getParent()` + * Deprecate passing `null` to `Twig\Parser::setParent()` + * Update `Node::__toString()` to include the node tag if set + * Add support for integers in methods of `Twig\Node\Node` that take a Node name + * Deprecate not passing a `BodyNode` instance as the body of a `ModuleNode` or `MacroNode` constructor + * Deprecate returning "null" from "TokenParserInterface::parse()". + * Deprecate `OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES` + * Fix performance regression when `use_yield` is `false` (which is the default) + * Improve compatibility when `use_yield` is `false` (as extensions still using `echo` will work as is) + * Accept colons (`:`) in addition to equals (`=`) to separate argument names and values in named arguments + * Add the `html_cva` function (in the HTML extra package) + * Add support for named arguments to the `block` and `attribute` functions + * Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments + * Add a `CallableArgumentsExtractor` class + * Deprecate passing a name to `FunctionExpression`, `FilterExpression`, and `TestExpression`; + pass a `TwigFunction`, `TwigFilter`, or `TestFilter` instead + * Deprecate all Twig callable attributes on `FunctionExpression`, `FilterExpression`, and `TestExpression` + * Deprecate the `filter` node of `FilterExpression` + * Add the notion of Twig callables (functions, filters, and tests) + * Bump minimum PHP version to 8.0 + * Fix integration tests when a test has more than one data/expect section and deprecations + * Add the `enum_cases` function + +# 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 + +# 3.11.0 (2024-08-08) + + * Deprecate `OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER` + * Add `Twig\Cache\ChainCache` and `Twig\Cache\ReadOnlyFilesystemCache` + * Add the possibility to deprecate attributes and nodes on `Node` + * Add the possibility to add a package and a version to the `deprecated` tag + * Add the possibility to add a package for filter/function/test deprecations + * Mark `ConstantExpression` as being `@final` + * Add the `find` filter + * Fix optimizer mode validation in `OptimizerNodeVisitor` + * Add the possibility to yield from a generator in `PrintNode` + * Add the `shuffle` filter + * Add the `singular` and `plural` filters in `StringExtension` + * Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()` + * Deprecate `Twig\ExpressionParser\parseHashExpression()` in favor of + `Twig\ExpressionParser::parseMappingExpression()` + * Deprecate `Twig\ExpressionParser\parseArrayExpression()` in favor of + `Twig\ExpressionParser::parseSequenceExpression()` + * Add `sequence` and `mapping` tests + * Deprecate `Twig\Node\Expression\NameExpression::isSimple()` and + `Twig\Node\Expression\NameExpression::isSpecial()` + +# 3.10.3 (2024-05-16) + + * Fix missing ; in generated code + +# 3.10.2 (2024-05-14) + + * Fix support for the deprecated escaper signature + +# 3.10.1 (2024-05-12) + + * Fix BC break on escaper extension + * Fix constant return type + +# 3.10.0 (2024-05-11) + + * Make `CoreExtension::formatDate`, `CoreExtension::convertDate`, and + `CoreExtension::formatNumber` part of the public API + * Add `needs_charset` option for filters and functions + * Extract the escaping logic from the `EscaperExtension` class to a new + `EscaperRuntime` class. + + The following methods from ``Twig\\Extension\\EscaperExtension`` are + deprecated: ``setEscaper()``, ``getEscapers()``, ``setSafeClasses``, + ``addSafeClasses()``. Use the same methods on the + ``Twig\\Runtime\\EscaperRuntime`` class instead. + * Fix capturing output from extensions that still use echo + * Fix a PHP warning in the Lexer on malformed templates + * Fix blocks not available under some circumstances + * Synchronize source context in templates when setting a Node on a Node + +# 3.9.3 (2024-04-18) + + * Add missing `twig_escape_filter_is_safe` deprecated function + * Fix yield usage with CaptureNode + * Add missing unwrap call when using a TemplateWrapper instance internally + * Ensure Lexer is initialized early on + +# 3.9.2 (2024-04-17) + + * Fix usage of display_end hook + +# 3.9.1 (2024-04-17) + + * Fix missing `$blocks` variable in `CaptureNode` + +# 3.9.0 (2024-04-16) + + * Add support for PHP 8.4 + * Deprecate AbstractNodeVisitor + * Deprecate passing Template to Environment::resolveTemplate(), Environment::load(), and Template::loadTemplate() + * Add a new "yield" mode for output generation; + Node implementations that use "echo" or "print" should use "yield" instead; + all Node implementations should be flagged with `#[YieldReady]` once they've been made ready for "yield"; + the "use_yield" Environment option can be turned on when all nodes have been made `#[YieldReady]`; + "yield" will be the only strategy supported in the next major version + * Add return type for Symfony 7 compatibility + * Fix premature loop exit in Security Policy lookup of allowed methods/properties + * Deprecate all internal extension functions in favor of methods on the extension classes + * Mark all extension functions as @internal + * Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source + * Throw a proper Twig exception when using cycle on an empty array + # 3.8.0 (2023-11-21) * Catch errors thrown during template rendering @@ -186,7 +428,7 @@ * removed Parser::isReservedMacroName() * removed SanboxedPrintNode * removed Node::setTemplateName() - * made classes maked as "@final" final + * made classes marked as "@final" final * removed InitRuntimeInterface, ExistsLoaderInterface, and SourceContextLoaderInterface * removed the "spaceless" tag * removed Twig\Environment::getBaseTemplateClass() and Twig\Environment::setBaseTemplateClass() diff --git a/app/vendor/twig/twig/README.rst b/app/vendor/twig/twig/README.rst index fbe7e9a9f..7bf8c673e 100644 --- a/app/vendor/twig/twig/README.rst +++ b/app/vendor/twig/twig/README.rst @@ -11,7 +11,7 @@ Sponsors .. raw:: html - + Blackfire.io diff --git a/app/vendor/twig/twig/composer.json b/app/vendor/twig/twig/composer.json index 1b1726fe8..366236637 100644 --- a/app/vendor/twig/twig/composer.json +++ b/app/vendor/twig/twig/composer.json @@ -24,16 +24,23 @@ } ], "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.22", + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "^1.3", "symfony/polyfill-ctype": "^1.8" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0", - "psr/container": "^1.0|^2.0" + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0", + "psr/container": "^1.0|^2.0", + "phpstan/phpstan": "^2.0" }, "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4" : { "Twig\\" : "src/" } diff --git a/app/vendor/twig/twig/phpstan-baseline.neon b/app/vendor/twig/twig/phpstan-baseline.neon new file mode 100644 index 000000000..131ed97b4 --- /dev/null +++ b/app/vendor/twig/twig/phpstan-baseline.neon @@ -0,0 +1,25 @@ +parameters: + ignoreErrors: + - # The method is dynamically generated by the CheckSecurityNode + message: '#^Call to an undefined method Twig\\Template\:\:checkSecurity\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Extension/CoreExtension.php + + - # 2 parameters will be required + message: '#^Method Twig\\Node\\IncludeNode\:\:addGetTemplate\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/Node/IncludeNode.php + + - # int|string will be supported in 4.x + message: '#^PHPDoc tag @param for parameter $name with type int|string is not subtype of native type string\.$#' + identifier: parameter.phpDocType + count: 5 + path: src/Node/Node.php + + - # Adding 0 to the string representation of a number is valid and what we want here + message: '#^Binary operation "\+" between 0 and string results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Lexer.php diff --git a/app/vendor/twig/twig/phpstan.neon.dist b/app/vendor/twig/twig/phpstan.neon.dist new file mode 100644 index 000000000..6d94e4109 --- /dev/null +++ b/app/vendor/twig/twig/phpstan.neon.dist @@ -0,0 +1,9 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 3 + paths: + - src + excludePaths: + - src/Test diff --git a/app/vendor/twig/twig/src/AbstractTwigCallable.php b/app/vendor/twig/twig/src/AbstractTwigCallable.php new file mode 100644 index 000000000..804f336cb --- /dev/null +++ b/app/vendor/twig/twig/src/AbstractTwigCallable.php @@ -0,0 +1,187 @@ + + */ +abstract class AbstractTwigCallable implements TwigCallableInterface +{ + protected $options; + + private $name; + private $dynamicName; + private $callable; + private $arguments; + + public function __construct(string $name, $callable = null, array $options = []) + { + $this->name = $this->dynamicName = $name; + $this->callable = $callable; + $this->arguments = []; + $this->options = array_merge([ + 'needs_environment' => false, + 'needs_context' => false, + 'needs_charset' => false, + 'is_variadic' => false, + 'deprecation_info' => null, + 'deprecated' => false, + 'deprecating_package' => '', + 'alternative' => null, + ], $options); + + if ($this->options['deprecation_info'] && !$this->options['deprecation_info'] instanceof DeprecatedCallableInfo) { + throw new \LogicException(\sprintf('The "deprecation_info" option must be an instance of "%s".', DeprecatedCallableInfo::class)); + } + + if ($this->options['deprecated']) { + if ($this->options['deprecation_info']) { + throw new \LogicException('When setting the "deprecation_info" option, you need to remove the obsolete deprecated options.'); + } + + trigger_deprecation('twig/twig', '3.15', 'Using the "deprecated", "deprecating_package", and "alternative" options is deprecated, pass a "deprecation_info" one instead.'); + + $this->options['deprecation_info'] = new DeprecatedCallableInfo( + $this->options['deprecating_package'], + $this->options['deprecated'], + null, + $this->options['alternative'], + ); + } + + if ($this->options['deprecation_info']) { + $this->options['deprecation_info']->setName($name); + $this->options['deprecation_info']->setType($this->getType()); + } + } + + public function __toString(): string + { + return \sprintf('%s(%s)', static::class, $this->name); + } + + public function getName(): string + { + return $this->name; + } + + public function getDynamicName(): string + { + return $this->dynamicName; + } + + /** + * @return callable|array{class-string, string}|null + */ + public function getCallable() + { + return $this->callable; + } + + public function getNodeClass(): string + { + return $this->options['node_class']; + } + + public function needsCharset(): bool + { + return $this->options['needs_charset']; + } + + public function needsEnvironment(): bool + { + return $this->options['needs_environment']; + } + + public function needsContext(): bool + { + return $this->options['needs_context']; + } + + /** + * @return static + */ + public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self + { + $new = clone $this; + $new->name = $name; + $new->dynamicName = $dynamicName; + $new->arguments = $arguments; + + return $new; + } + + /** + * @deprecated since Twig 3.12, use withDynamicArguments() instead + */ + public function setArguments(array $arguments): void + { + trigger_deprecation('twig/twig', '3.12', 'The "%s::setArguments()" method is deprecated, use "%s::withDynamicArguments()" instead.', static::class, static::class); + + $this->arguments = $arguments; + } + + public function getArguments(): array + { + return $this->arguments; + } + + public function isVariadic(): bool + { + return $this->options['is_variadic']; + } + + public function isDeprecated(): bool + { + return (bool) $this->options['deprecation_info']; + } + + public function triggerDeprecation(?string $file = null, ?int $line = null): void + { + $this->options['deprecation_info']->triggerDeprecation($file, $line); + } + + /** + * @deprecated since Twig 3.15 + */ + public function getDeprecatingPackage(): string + { + trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class); + + return $this->options['deprecating_package']; + } + + /** + * @deprecated since Twig 3.15 + */ + public function getDeprecatedVersion(): string + { + trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class); + + return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; + } + + /** + * @deprecated since Twig 3.15 + */ + public function getAlternative(): ?string + { + trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class); + + return $this->options['alternative']; + } + + public function getMinimalNumberOfRequiredArguments(): int + { + return ($this->options['needs_charset'] ? 1 : 0) + ($this->options['needs_environment'] ? 1 : 0) + ($this->options['needs_context'] ? 1 : 0) + \count($this->arguments); + } +} diff --git a/app/vendor/twig/twig/src/Attribute/FirstClassTwigCallableReady.php b/app/vendor/twig/twig/src/Attribute/FirstClassTwigCallableReady.php new file mode 100644 index 000000000..ffd8cffc8 --- /dev/null +++ b/app/vendor/twig/twig/src/Attribute/FirstClassTwigCallableReady.php @@ -0,0 +1,20 @@ + + */ +final class ChainCache implements CacheInterface, RemovableCacheInterface +{ + /** + * @param iterable $caches The ordered list of caches used to store and fetch cached items + */ + public function __construct( + private iterable $caches, + ) { + } + + public function generateKey(string $name, string $className): string + { + return $className.'#'.$name; + } + + public function write(string $key, string $content): void + { + $splitKey = $this->splitKey($key); + + foreach ($this->caches as $cache) { + $cache->write($cache->generateKey(...$splitKey), $content); + } + } + + public function load(string $key): void + { + [$name, $className] = $this->splitKey($key); + + foreach ($this->caches as $cache) { + $cache->load($cache->generateKey($name, $className)); + + if (class_exists($className, false)) { + break; + } + } + } + + public function getTimestamp(string $key): int + { + $splitKey = $this->splitKey($key); + + foreach ($this->caches as $cache) { + if (0 < $timestamp = $cache->getTimestamp($cache->generateKey(...$splitKey))) { + return $timestamp; + } + } + + return 0; + } + + public function remove(string $name, string $cls): void + { + foreach ($this->caches as $cache) { + if ($cache instanceof RemovableCacheInterface) { + $cache->remove($name, $cls); + } + } + } + + /** + * @return string[] + */ + private function splitKey(string $key): array + { + return array_reverse(explode('#', $key, 2)); + } +} diff --git a/app/vendor/twig/twig/src/Cache/FilesystemCache.php b/app/vendor/twig/twig/src/Cache/FilesystemCache.php index 4024adbd7..5840585e3 100644 --- a/app/vendor/twig/twig/src/Cache/FilesystemCache.php +++ b/app/vendor/twig/twig/src/Cache/FilesystemCache.php @@ -16,7 +16,7 @@ * * @author Andrew Tch */ -class FilesystemCache implements CacheInterface +class FilesystemCache implements CacheInterface, RemovableCacheInterface { public const FORCE_BYTECODE_INVALIDATION = 1; @@ -50,11 +50,11 @@ public function write(string $key, string $content): void if (false === @mkdir($dir, 0777, true)) { clearstatcache(true, $dir); if (!is_dir($dir)) { - throw new \RuntimeException(sprintf('Unable to create the cache directory (%s).', $dir)); + throw new \RuntimeException(\sprintf('Unable to create the cache directory (%s).', $dir)); } } } elseif (!is_writable($dir)) { - throw new \RuntimeException(sprintf('Unable to write in the cache directory (%s).', $dir)); + throw new \RuntimeException(\sprintf('Unable to write in the cache directory (%s).', $dir)); } $tmpFile = tempnam($dir, basename($key)); @@ -73,7 +73,15 @@ public function write(string $key, string $content): void return; } - throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $key)); + throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $key)); + } + + public function remove(string $name, string $cls): void + { + $key = $this->generateKey($name, $cls); + if (!@unlink($key) && file_exists($key)) { + throw new \RuntimeException(\sprintf('Failed to delete cache file "%s".', $key)); + } } public function getTimestamp(string $key): int diff --git a/app/vendor/twig/twig/src/Cache/NullCache.php b/app/vendor/twig/twig/src/Cache/NullCache.php index 8d20d59d8..1ae216928 100644 --- a/app/vendor/twig/twig/src/Cache/NullCache.php +++ b/app/vendor/twig/twig/src/Cache/NullCache.php @@ -16,7 +16,7 @@ * * @author Fabien Potencier */ -final class NullCache implements CacheInterface +final class NullCache implements CacheInterface, RemovableCacheInterface { public function generateKey(string $name, string $className): string { @@ -35,4 +35,8 @@ public function getTimestamp(string $key): int { return 0; } + + public function remove(string $name, string $cls): void + { + } } diff --git a/app/vendor/twig/twig/src/Cache/ReadOnlyFilesystemCache.php b/app/vendor/twig/twig/src/Cache/ReadOnlyFilesystemCache.php new file mode 100644 index 000000000..3ba6514c9 --- /dev/null +++ b/app/vendor/twig/twig/src/Cache/ReadOnlyFilesystemCache.php @@ -0,0 +1,25 @@ + + */ +class ReadOnlyFilesystemCache extends FilesystemCache +{ + public function write(string $key, string $content): void + { + // Do nothing with the content, it's a read-only filesystem. + } +} diff --git a/app/vendor/twig/twig/src/Cache/RemovableCacheInterface.php b/app/vendor/twig/twig/src/Cache/RemovableCacheInterface.php new file mode 100644 index 000000000..05da56913 --- /dev/null +++ b/app/vendor/twig/twig/src/Cache/RemovableCacheInterface.php @@ -0,0 +1,20 @@ + + */ +interface RemovableCacheInterface +{ + public function remove(string $name, string $cls): void; +} diff --git a/app/vendor/twig/twig/src/Compiler.php b/app/vendor/twig/twig/src/Compiler.php index eb652c61a..ce8b17c98 100644 --- a/app/vendor/twig/twig/src/Compiler.php +++ b/app/vendor/twig/twig/src/Compiler.php @@ -22,15 +22,16 @@ class Compiler private $lastLine; private $source; private $indentation; - private $env; private $debugInfo = []; private $sourceOffset; private $sourceLine; private $varNameSalt = 0; + private $didUseEcho = false; + private $didUseEchoStack = []; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + private Environment $env, + ) { } public function getEnvironment(): Environment @@ -66,9 +67,20 @@ public function reset(int $indentation = 0) public function compile(Node $node, int $indentation = 0) { $this->reset($indentation); - $node->compile($this); + $this->didUseEchoStack[] = $this->didUseEcho; - return $this; + try { + $this->didUseEcho = false; + $node->compile($this); + + if ($this->didUseEcho) { + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, \get_class($node)); + } + + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } } /** @@ -76,13 +88,24 @@ public function compile(Node $node, int $indentation = 0) */ public function subcompile(Node $node, bool $raw = true) { - if (false === $raw) { + if (!$raw) { $this->source .= str_repeat(' ', $this->indentation * 4); } - $node->compile($this); + $this->didUseEchoStack[] = $this->didUseEcho; - return $this; + try { + $this->didUseEcho = false; + $node->compile($this); + + if ($this->didUseEcho) { + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, \get_class($node)); + } + + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } } /** @@ -92,6 +115,7 @@ public function subcompile(Node $node, bool $raw = true) */ public function raw(string $string) { + $this->checkForEcho($string); $this->source .= $string; return $this; @@ -105,6 +129,7 @@ public function raw(string $string) public function write(...$strings) { foreach ($strings as $string) { + $this->checkForEcho($string); $this->source .= str_repeat(' ', $this->indentation * 4).$string; } @@ -118,7 +143,7 @@ public function write(...$strings) */ public function string(string $value) { - $this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); + $this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); return $this; } @@ -145,7 +170,7 @@ public function repr($value) } elseif (\is_bool($value)) { $this->raw($value ? 'true' : 'false'); } elseif (\is_array($value)) { - $this->raw('array('); + $this->raw('['); $first = true; foreach ($value as $key => $v) { if (!$first) { @@ -156,7 +181,7 @@ public function repr($value) $this->raw(' => '); $this->repr($v); } - $this->raw(')'); + $this->raw(']'); } else { $this->string($value); } @@ -170,7 +195,7 @@ public function repr($value) public function addDebugInfo(Node $node) { if ($node->getTemplateLine() != $this->lastLine) { - $this->write(sprintf("// line %d\n", $node->getTemplateLine())); + $this->write(\sprintf("// line %d\n", $node->getTemplateLine())); $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset); $this->sourceOffset = \strlen($this->source); @@ -218,6 +243,15 @@ public function outdent(int $step = 1) public function getVarName(): string { - return sprintf('__internal_compile_%d', $this->varNameSalt++); + return \sprintf('_v%d', $this->varNameSalt++); + } + + private function checkForEcho(string $string): void + { + if ($this->didUseEcho) { + return; + } + + $this->didUseEcho = preg_match('/^\s*+(echo|print)\b/', $string, $m) ? $m[1] : false; } } diff --git a/app/vendor/twig/twig/src/DeprecatedCallableInfo.php b/app/vendor/twig/twig/src/DeprecatedCallableInfo.php new file mode 100644 index 000000000..2db9f3d28 --- /dev/null +++ b/app/vendor/twig/twig/src/DeprecatedCallableInfo.php @@ -0,0 +1,67 @@ + + */ +final class DeprecatedCallableInfo +{ + private string $type; + private string $name; + + public function __construct( + private string $package, + private string $version, + private ?string $altName = null, + private ?string $altPackage = null, + private ?string $altVersion = null, + ) { + } + + public function setType(string $type): void + { + $this->type = $type; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function triggerDeprecation(?string $file = null, ?int $line = null): void + { + $message = \sprintf('Twig %s "%s" is deprecated', ucfirst($this->type), $this->name); + + if ($this->altName) { + $message .= \sprintf('; use "%s"', $this->altName); + if ($this->altPackage) { + $message .= \sprintf(' from the "%s" package', $this->altPackage); + } + if ($this->altVersion) { + $message .= \sprintf(' (available since version %s)', $this->altVersion); + } + $message .= ' instead'; + } + + if ($file) { + $message .= \sprintf(' in %s', $file); + if ($line) { + $message .= \sprintf(' at line %d', $line); + } + } + + $message .= '.'; + + trigger_deprecation($this->package, $this->version, $message); + } +} diff --git a/app/vendor/twig/twig/src/Environment.php b/app/vendor/twig/twig/src/Environment.php index d7d51cdb1..17765cd2b 100644 --- a/app/vendor/twig/twig/src/Environment.php +++ b/app/vendor/twig/twig/src/Environment.php @@ -14,6 +14,7 @@ use Twig\Cache\CacheInterface; use Twig\Cache\FilesystemCache; use Twig\Cache\NullCache; +use Twig\Cache\RemovableCacheInterface; use Twig\Error\Error; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; @@ -22,6 +23,7 @@ use Twig\Extension\EscaperExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\OptimizerExtension; +use Twig\Extension\YieldNotReadyExtension; use Twig\Loader\ArrayLoader; use Twig\Loader\ChainLoader; use Twig\Loader\LoaderInterface; @@ -30,6 +32,8 @@ use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\Runtime\EscaperRuntime; +use Twig\RuntimeLoader\FactoryRuntimeLoader; use Twig\RuntimeLoader\RuntimeLoaderInterface; use Twig\TokenParser\TokenParserInterface; @@ -40,10 +44,10 @@ */ class Environment { - public const VERSION = '3.8.0'; - public const VERSION_ID = 30800; + public const VERSION = '3.20.0'; + public const VERSION_ID = 32000; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 8; + public const MINOR_VERSION = 20; public const RELEASE_VERSION = 0; public const EXTRA_VERSION = ''; @@ -60,12 +64,15 @@ class Environment private $resolvedGlobals; private $loadedTemplates; private $strictVariables; - private $templateClassPrefix = '__TwigTemplate_'; private $originalCache; private $extensionSet; private $runtimeLoaders = []; private $runtimes = []; private $optionsHash; + /** @var bool */ + private $useYield; + private $defaultRuntimeLoader; + private array $hotCache = []; /** * Constructor. @@ -97,8 +104,12 @@ class Environment * * optimizations: A flag that indicates which optimizations to apply * (default to -1 which means that all optimizations are enabled; * set it to 0 to disable). + * + * * use_yield: true: forces templates to exclusively use "yield" instead of "echo" (all extensions must be yield ready) + * false (default): allows templates to use a mix of "yield" and "echo" calls to allow for a progressive migration + * Switch to "true" when possible as this will be the only supported mode in Twig 4.0 */ - public function __construct(LoaderInterface $loader, $options = []) + public function __construct(LoaderInterface $loader, array $options = []) { $this->setLoader($loader); @@ -110,22 +121,42 @@ public function __construct(LoaderInterface $loader, $options = []) 'cache' => false, 'auto_reload' => null, 'optimizations' => -1, + 'use_yield' => false, ], $options); + $this->useYield = (bool) $options['use_yield']; $this->debug = (bool) $options['debug']; $this->setCharset($options['charset'] ?? 'UTF-8'); $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload']; $this->strictVariables = (bool) $options['strict_variables']; $this->setCache($options['cache']); $this->extensionSet = new ExtensionSet(); + $this->defaultRuntimeLoader = new FactoryRuntimeLoader([ + EscaperRuntime::class => function () { return new EscaperRuntime($this->charset); }, + ]); $this->addExtension(new CoreExtension()); - $this->addExtension(new EscaperExtension($options['autoescape'])); + $escaperExt = new EscaperExtension($options['autoescape']); + $escaperExt->setEnvironment($this, false); + $this->addExtension($escaperExt); + if (\PHP_VERSION_ID >= 80000) { + $this->addExtension(new YieldNotReadyExtension($this->useYield)); + } $this->addExtension(new OptimizerExtension($options['optimizations'])); } + /** + * @internal + */ + public function useYield(): bool + { + return $this->useYield; + } + /** * Enables debugging mode. + * + * @return void */ public function enableDebug() { @@ -135,6 +166,8 @@ public function enableDebug() /** * Disables debugging mode. + * + * @return void */ public function disableDebug() { @@ -154,6 +187,8 @@ public function isDebug() /** * Enables the auto_reload option. + * + * @return void */ public function enableAutoReload() { @@ -162,6 +197,8 @@ public function enableAutoReload() /** * Disables the auto_reload option. + * + * @return void */ public function disableAutoReload() { @@ -180,6 +217,8 @@ public function isAutoReload() /** * Enables the strict_variables option. + * + * @return void */ public function enableStrictVariables() { @@ -189,6 +228,8 @@ public function enableStrictVariables() /** * Disables the strict_variables option. + * + * @return void */ public function disableStrictVariables() { @@ -206,6 +247,18 @@ public function isStrictVariables() return $this->strictVariables; } + public function removeCache(string $name): void + { + $cls = $this->getTemplateClass($name); + $this->hotCache[$name] = $cls.'_'.bin2hex(random_bytes(16)); + + if ($this->cache instanceof RemovableCacheInterface) { + $this->cache->remove($name, $cls); + } else { + throw new \LogicException(\sprintf('The "%s" cache class does not support removing template cache as it does not implement the "RemovableCacheInterface" interface.', \get_class($this->cache))); + } + } + /** * Gets the current cache implementation. * @@ -226,6 +279,8 @@ public function getCache($original = true) * @param CacheInterface|string|false $cache A Twig\Cache\CacheInterface implementation, * an absolute path to the compiled templates, * or false to disable cache + * + * @return void */ public function setCache($cache) { @@ -249,7 +304,6 @@ public function setCache($cache) * * * The cache key for the given template; * * The currently enabled extensions; - * * Whether the Twig C extension is available or not; * * PHP version; * * Twig version; * * Options with what environment was created. @@ -259,11 +313,11 @@ public function setCache($cache) * * @internal */ - public function getTemplateClass(string $name, int $index = null): string + public function getTemplateClass(string $name, ?int $index = null): string { - $key = $this->getLoader()->getCacheKey($name).$this->optionsHash; + $key = ($this->hotCache[$name] ?? $this->getLoader()->getCacheKey($name)).$this->optionsHash; - return $this->templateClassPrefix.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index); + return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index); } /** @@ -308,6 +362,11 @@ public function load($name): TemplateWrapper if ($name instanceof TemplateWrapper) { return $name; } + if ($name instanceof Template) { + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__); + + return $name; + } return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name)); } @@ -318,8 +377,8 @@ public function load($name): TemplateWrapper * This method is for internal use only and should never be called * directly. * - * @param string $name The template name - * @param int $index The index if it is an embedded template + * @param string $name The template name + * @param int|null $index The index if it is an embedded template * * @throws LoaderError When the template cannot be found * @throws RuntimeError When a previously generated cache is corrupted @@ -327,7 +386,7 @@ public function load($name): TemplateWrapper * * @internal */ - public function loadTemplate(string $cls, string $name, int $index = null): Template + public function loadTemplate(string $cls, string $name, ?int $index = null): Template { $mainCls = $cls; if (null !== $index) { @@ -348,8 +407,10 @@ public function loadTemplate(string $cls, string $name, int $index = null): Temp if (!class_exists($cls, false)) { $source = $this->getLoader()->getSourceContext($name); $content = $this->compileSource($source); - $this->cache->write($key, $content); - $this->cache->load($key); + if (!isset($this->hotCache[$name])) { + $this->cache->write($key, $content); + $this->cache->load($key); + } if (!class_exists($mainCls, false)) { /* Last line of defense if either $this->bcWriteCacheFile was used, @@ -361,7 +422,7 @@ public function loadTemplate(string $cls, string $name, int $index = null): Temp } if (!class_exists($cls, false)) { - throw new RuntimeError(sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source); + throw new RuntimeError(\sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source); } } } @@ -376,19 +437,19 @@ public function loadTemplate(string $cls, string $name, int $index = null): Temp * * This method should not be used as a generic way to load templates. * - * @param string $template The template source - * @param string $name An optional name of the template to be used in error messages + * @param string $template The template source + * @param string|null $name An optional name of the template to be used in error messages * * @throws LoaderError When the template cannot be found * @throws SyntaxError When an error occurred during compilation */ - public function createTemplate(string $template, string $name = null): TemplateWrapper + public function createTemplate(string $template, ?string $name = null): TemplateWrapper { $hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $template, false); if (null !== $name) { - $name = sprintf('%s (string template %s)', $name, $hash); + $name = \sprintf('%s (string template %s)', $name, $hash); } else { - $name = sprintf('__string_template__%s', $hash); + $name = \sprintf('__string_template__%s', $hash); } $loader = new ChainLoader([ @@ -421,10 +482,10 @@ public function isTemplateFresh(string $name, int $time): bool /** * Tries to load a template consecutively from an array. * - * Similar to load() but it also accepts instances of \Twig\Template and - * \Twig\TemplateWrapper, and an array of templates where each is tried to be loaded. + * Similar to load() but it also accepts instances of \Twig\TemplateWrapper + * and an array of templates where each is tried to be loaded. * - * @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively + * @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively * * @throws LoaderError When none of the templates can be found * @throws SyntaxError When an error occurred during compilation @@ -438,7 +499,9 @@ public function resolveTemplate($names): TemplateWrapper $count = \count($names); foreach ($names as $name) { if ($name instanceof Template) { - return $name; + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', Template::class, __METHOD__); + + return new TemplateWrapper($this, $name); } if ($name instanceof TemplateWrapper) { return $name; @@ -451,9 +514,12 @@ public function resolveTemplate($names): TemplateWrapper return $this->load($name); } - throw new LoaderError(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names))); + throw new LoaderError(\sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names))); } + /** + * @return void + */ public function setLexer(Lexer $lexer) { $this->lexer = $lexer; @@ -471,6 +537,9 @@ public function tokenize(Source $source): TokenStream return $this->lexer->tokenize($source); } + /** + * @return void + */ public function setParser(Parser $parser) { $this->parser = $parser; @@ -490,6 +559,9 @@ public function parse(TokenStream $stream): ModuleNode return $this->parser->parse($stream); } + /** + * @return void + */ public function setCompiler(Compiler $compiler) { $this->compiler = $compiler; @@ -520,10 +592,13 @@ public function compileSource(Source $source): string $e->setSourceContext($source); throw $e; } catch (\Exception $e) { - throw new SyntaxError(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e); + throw new SyntaxError(\sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e); } } + /** + * @return void + */ public function setLoader(LoaderInterface $loader) { $this->loader = $loader; @@ -534,9 +609,12 @@ public function getLoader(): LoaderInterface return $this->loader; } + /** + * @return void + */ public function setCharset(string $charset) { - if ('UTF8' === $charset = null === $charset ? null : strtoupper($charset)) { + if ('UTF8' === $charset = strtoupper($charset ?: '')) { // iconv on Windows requires "UTF-8" instead of "UTF8" $charset = 'UTF-8'; } @@ -554,6 +632,9 @@ public function hasExtension(string $class): bool return $this->extensionSet->hasExtension($class); } + /** + * @return void + */ public function addRuntimeLoader(RuntimeLoaderInterface $loader) { $this->runtimeLoaders[] = $loader; @@ -594,9 +675,16 @@ public function getRuntime(string $class) } } - throw new RuntimeError(sprintf('Unable to load the "%s" runtime.', $class)); + if (null !== $runtime = $this->defaultRuntimeLoader->load($class)) { + return $this->runtimes[$class] = $runtime; + } + + throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class)); } + /** + * @return void + */ public function addExtension(ExtensionInterface $extension) { $this->extensionSet->addExtension($extension); @@ -605,6 +693,8 @@ public function addExtension(ExtensionInterface $extension) /** * @param ExtensionInterface[] $extensions An array of extensions + * + * @return void */ public function setExtensions(array $extensions) { @@ -620,6 +710,9 @@ public function getExtensions(): array return $this->extensionSet->getExtensions(); } + /** + * @return void + */ public function addTokenParser(TokenParserInterface $parser) { $this->extensionSet->addTokenParser($parser); @@ -643,11 +736,17 @@ public function getTokenParser(string $name): ?TokenParserInterface return $this->extensionSet->getTokenParser($name); } + /** + * @param callable(string): (TokenParserInterface|false) $callable + */ public function registerUndefinedTokenParserCallback(callable $callable): void { $this->extensionSet->registerUndefinedTokenParserCallback($callable); } + /** + * @return void + */ public function addNodeVisitor(NodeVisitorInterface $visitor) { $this->extensionSet->addNodeVisitor($visitor); @@ -663,6 +762,9 @@ public function getNodeVisitors(): array return $this->extensionSet->getNodeVisitors(); } + /** + * @return void + */ public function addFilter(TwigFilter $filter) { $this->extensionSet->addFilter($filter); @@ -676,6 +778,9 @@ public function getFilter(string $name): ?TwigFilter return $this->extensionSet->getFilter($name); } + /** + * @param callable(string): (TwigFilter|false) $callable + */ public function registerUndefinedFilterCallback(callable $callable): void { $this->extensionSet->registerUndefinedFilterCallback($callable); @@ -697,6 +802,9 @@ public function getFilters(): array return $this->extensionSet->getFilters(); } + /** + * @return void + */ public function addTest(TwigTest $test) { $this->extensionSet->addTest($test); @@ -720,6 +828,9 @@ public function getTest(string $name): ?TwigTest return $this->extensionSet->getTest($name); } + /** + * @return void + */ public function addFunction(TwigFunction $function) { $this->extensionSet->addFunction($function); @@ -733,6 +844,9 @@ public function getFunction(string $name): ?TwigFunction return $this->extensionSet->getFunction($name); } + /** + * @param callable(string): (TwigFunction|false) $callable + */ public function registerUndefinedFunctionCallback(callable $callable): void { $this->extensionSet->registerUndefinedFunctionCallback($callable); @@ -761,11 +875,13 @@ public function getFunctions(): array * but after, you can only update existing globals. * * @param mixed $value The global value + * + * @return void */ public function addGlobal(string $name, $value) { if ($this->extensionSet->isInitialized() && !\array_key_exists($name, $this->getGlobals())) { - throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name)); + throw new \LogicException(\sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name)); } if (null !== $this->resolvedGlobals) { @@ -776,8 +892,6 @@ public function addGlobal(string $name, $value) } /** - * @internal - * * @return array */ public function getGlobals(): array @@ -793,23 +907,26 @@ public function getGlobals(): array return array_merge($this->extensionSet->getGlobals(), $this->globals); } + public function resetGlobals(): void + { + $this->resolvedGlobals = null; + $this->extensionSet->resetGlobals(); + } + + /** + * @deprecated since Twig 3.14 + */ public function mergeGlobals(array $context): array { - // we don't use array_merge as the context being generally - // bigger than globals, this code is faster. - foreach ($this->getGlobals() as $key => $value) { - if (!\array_key_exists($key, $context)) { - $context[$key] = $value; - } - } + trigger_deprecation('twig/twig', '3.14', 'The "%s" method is deprecated.', __METHOD__); - return $context; + return $context + $this->getGlobals(); } /** * @internal * - * @return array}> + * @return array}> */ public function getUnaryOperators(): array { @@ -819,7 +936,7 @@ public function getUnaryOperators(): array /** * @internal * - * @return array, associativity: ExpressionParser::OPERATOR_*}> + * @return array, associativity: ExpressionParser::OPERATOR_*}> */ public function getBinaryOperators(): array { @@ -835,6 +952,7 @@ private function updateOptionsHash(): void self::VERSION, (int) $this->debug, (int) $this->strictVariables, + $this->useYield ? '1' : '0', ]); } } diff --git a/app/vendor/twig/twig/src/Error/Error.php b/app/vendor/twig/twig/src/Error/Error.php index bca1fa64c..61c309fa1 100644 --- a/app/vendor/twig/twig/src/Error/Error.php +++ b/app/vendor/twig/twig/src/Error/Error.php @@ -53,7 +53,7 @@ class Error extends \Exception * @param int $lineno The template line where the error occurred * @param Source|null $source The source context where the error occurred */ - public function __construct(string $message, int $lineno = -1, Source $source = null, \Throwable $previous = null) + public function __construct(string $message, int $lineno = -1, ?Source $source = null, ?\Throwable $previous = null) { parent::__construct('', 0, $previous); @@ -93,7 +93,7 @@ public function getSourceContext(): ?Source return $this->name ? new Source($this->sourceCode, $this->name, $this->sourcePath) : null; } - public function setSourceContext(Source $source = null): void + public function setSourceContext(?Source $source = null): void { if (null === $source) { $this->sourceCode = $this->name = $this->sourcePath = null; @@ -142,16 +142,16 @@ private function updateRepr(): void } if ($this->name) { - if (\is_string($this->name) || (\is_object($this->name) && method_exists($this->name, '__toString'))) { - $name = sprintf('"%s"', $this->name); + if (\is_string($this->name) || $this->name instanceof \Stringable) { + $name = \sprintf('"%s"', $this->name); } else { $name = json_encode($this->name); } - $this->message .= sprintf(' in %s', $name); + $this->message .= \sprintf(' in %s', $name); } if ($this->lineno && $this->lineno >= 0) { - $this->message .= sprintf(' at line %d', $this->lineno); + $this->message .= \sprintf(' at line %d', $this->lineno); } if ($dot) { diff --git a/app/vendor/twig/twig/src/Error/SyntaxError.php b/app/vendor/twig/twig/src/Error/SyntaxError.php index 77c437c68..841b653f5 100644 --- a/app/vendor/twig/twig/src/Error/SyntaxError.php +++ b/app/vendor/twig/twig/src/Error/SyntaxError.php @@ -41,6 +41,6 @@ public function addSuggestions(string $name, array $items): void asort($alternatives); - $this->appendMessage(sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives)))); + $this->appendMessage(\sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives)))); } } diff --git a/app/vendor/twig/twig/src/ExpressionParser.php b/app/vendor/twig/twig/src/ExpressionParser.php index 13e0f0876..233139ee4 100644 --- a/app/vendor/twig/twig/src/ExpressionParser.php +++ b/app/vendor/twig/twig/src/ExpressionParser.php @@ -12,26 +12,30 @@ namespace Twig; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Error\SyntaxError; +use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ArrowFunctionExpression; -use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\Binary\AbstractBinary; use Twig\Node\Expression\Binary\ConcatBinary; -use Twig\Node\Expression\BlockReferenceExpression; -use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\MethodCallExpression; -use Twig\Node\Expression\NameExpression; -use Twig\Node\Expression\ParentExpression; +use Twig\Node\Expression\MacroReferenceExpression; +use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Expression\TestExpression; use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; +use Twig\Node\Expression\Unary\SpreadUnary; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\ContextVariable; +use Twig\Node\Expression\Variable\LocalVariable; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\Node; +use Twig\Node\Nodes; /** * Parses expressions. @@ -48,24 +52,50 @@ class ExpressionParser public const OPERATOR_LEFT = 1; public const OPERATOR_RIGHT = 2; - private $parser; - private $env; - /** @var array}> */ + /** @var array}> */ private $unaryOperators; - /** @var array, associativity: self::OPERATOR_*}> */ + /** @var array, associativity: self::OPERATOR_*}> */ private $binaryOperators; - - public function __construct(Parser $parser, Environment $env) - { - $this->parser = $parser; - $this->env = $env; + private $readyNodes = []; + private array $precedenceChanges = []; + private bool $deprecationCheck = true; + + public function __construct( + private Parser $parser, + private Environment $env, + ) { $this->unaryOperators = $env->getUnaryOperators(); $this->binaryOperators = $env->getBinaryOperators(); + + $ops = []; + foreach ($this->unaryOperators as $n => $c) { + $ops[] = $c + ['name' => $n, 'type' => 'unary']; + } + foreach ($this->binaryOperators as $n => $c) { + $ops[] = $c + ['name' => $n, 'type' => 'binary']; + } + foreach ($ops as $config) { + if (!isset($config['precedence_change'])) { + continue; + } + $name = $config['type'].'_'.$config['name']; + $min = min($config['precedence_change']->getNewPrecedence(), $config['precedence']); + $max = max($config['precedence_change']->getNewPrecedence(), $config['precedence']); + foreach ($ops as $c) { + if ($c['precedence'] > $min && $c['precedence'] < $max) { + $this->precedenceChanges[$c['type'].'_'.$c['name']][] = $name; + } + } + } } - public function parseExpression($precedence = 0, $allowArrow = false) + public function parseExpression($precedence = 0) { - if ($allowArrow && $arrow = $this->parseArrow()) { + if (\func_num_args() > 1) { + trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); + } + + if ($arrow = $this->parseArrow()) { return $arrow; } @@ -82,11 +112,20 @@ public function parseExpression($precedence = 0, $allowArrow = false) } elseif (isset($op['callable'])) { $expr = $op['callable']($this->parser, $expr); } else { - $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true); + $previous = $this->setDeprecationCheck(true); + try { + $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); + } finally { + $this->setDeprecationCheck($previous); + } $class = $op['class']; $expr = new $class($expr, $expr1, $token->getLine()); } + $expr->setAttribute('operator', 'binary_'.$token->getValue()); + + $this->triggerPrecedenceDeprecations($expr); + $token = $this->parser->getCurrentToken(); } @@ -97,6 +136,43 @@ public function parseExpression($precedence = 0, $allowArrow = false) return $expr; } + private function triggerPrecedenceDeprecations(AbstractExpression $expr): void + { + // Check that the all nodes that are between the 2 precedences have explicit parentheses + if (!$expr->hasAttribute('operator') || !isset($this->precedenceChanges[$expr->getAttribute('operator')])) { + return; + } + + if (str_starts_with($unaryOp = $expr->getAttribute('operator'), 'unary')) { + if ($expr->hasExplicitParentheses()) { + return; + } + $target = explode('_', $unaryOp)[1]; + /** @var AbstractExpression $node */ + $node = $expr->getNode('node'); + foreach ($this->precedenceChanges as $operatorName => $changes) { + if (!\in_array($unaryOp, $changes)) { + continue; + } + if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) { + $change = $this->unaryOperators[$target]['precedence_change']; + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $target, $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); + } + } + } else { + foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operatorName) { + foreach ($expr as $node) { + /** @var AbstractExpression $node */ + if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { + $op = explode('_', $operatorName)[1]; + $change = $this->binaryOperators[$op]['precedence_change']; + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $op, $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); + } + } + } + } + } + /** * @return ArrowFunctionExpression|null */ @@ -105,54 +181,54 @@ private function parseArrow() $stream = $this->parser->getStream(); // short array syntax (one argument, no parentheses)? - if ($stream->look(1)->test(/* Token::ARROW_TYPE */ 12)) { + if ($stream->look(1)->test(Token::ARROW_TYPE)) { $line = $stream->getCurrent()->getLine(); - $token = $stream->expect(/* Token::NAME_TYPE */ 5); - $names = [new AssignNameExpression($token->getValue(), $token->getLine())]; - $stream->expect(/* Token::ARROW_TYPE */ 12); + $token = $stream->expect(Token::NAME_TYPE); + $names = [new AssignContextVariable($token->getValue(), $token->getLine())]; + $stream->expect(Token::ARROW_TYPE); - return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line); + return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line); } // first, determine if we are parsing an arrow function by finding => (long form) $i = 0; - if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) { return null; } ++$i; while (true) { // variable name ++$i; - if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) { break; } ++$i; } - if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) { + if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) { return null; } ++$i; - if (!$stream->look($i)->test(/* Token::ARROW_TYPE */ 12)) { + if (!$stream->look($i)->test(Token::ARROW_TYPE)) { return null; } // yes, let's parse it properly - $token = $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '('); + $token = $stream->expect(Token::PUNCTUATION_TYPE, '('); $line = $token->getLine(); $names = []; while (true) { - $token = $stream->expect(/* Token::NAME_TYPE */ 5); - $names[] = new AssignNameExpression($token->getValue(), $token->getLine()); + $token = $stream->expect(Token::NAME_TYPE); + $names[] = new AssignContextVariable($token->getValue(), $token->getLine()); - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')'); - $stream->expect(/* Token::ARROW_TYPE */ 12); + $stream->expect(Token::PUNCTUATION_TYPE, ')'); + $stream->expect(Token::ARROW_TYPE); - return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line); + return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line); } private function getPrimary(): AbstractExpression @@ -165,11 +241,23 @@ private function getPrimary(): AbstractExpression $expr = $this->parseExpression($operator['precedence']); $class = $operator['class']; - return $this->parsePostfixExpression(new $class($expr, $token->getLine())); - } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + $expr = new $class($expr, $token->getLine()); + $expr->setAttribute('operator', 'unary_'.$token->getValue()); + + if ($this->deprecationCheck) { + $this->triggerPrecedenceDeprecations($expr); + } + + return $this->parsePostfixExpression($expr); + } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) { $this->parser->getStream()->next(); - $expr = $this->parseExpression(); - $this->parser->getStream()->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'An opened parenthesis is not properly closed'); + $previous = $this->setDeprecationCheck(false); + try { + $expr = $this->parseExpression()->setExplicitParentheses(); + } finally { + $this->setDeprecationCheck($previous); + } + $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); return $this->parsePostfixExpression($expr); } @@ -179,23 +267,17 @@ private function getPrimary(): AbstractExpression private function parseConditionalExpression($expr): AbstractExpression { - while ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, '?')) { - if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { - $expr2 = $this->parseExpression(); - if ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { - // Ternary operator (expr ? expr2 : expr3) - $expr3 = $this->parseExpression(); - } else { - // Ternary without else (expr ? expr2) - $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine()); - } - } else { - // Ternary without then (expr ?: expr3) - $expr2 = $expr; + while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) { + $expr2 = $this->parseExpression(); + if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { + // Ternary operator (expr ? expr2 : expr3) $expr3 = $this->parseExpression(); + } else { + // Ternary without else (expr ? expr2) + $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine()); } - $expr = new ConditionalExpression($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine()); + $expr = new ConditionalTernary($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine()); } return $expr; @@ -203,19 +285,19 @@ private function parseConditionalExpression($expr): AbstractExpression private function isUnary(Token $token): bool { - return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->unaryOperators[$token->getValue()]); + return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]); } private function isBinary(Token $token): bool { - return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->binaryOperators[$token->getValue()]); + return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]); } public function parsePrimaryExpression() { $token = $this->parser->getCurrentToken(); - switch ($token->getType()) { - case /* Token::NAME_TYPE */ 5: + switch (true) { + case $token->test(Token::NAME_TYPE): $this->parser->getStream()->next(); switch ($token->getValue()) { case 'true': @@ -239,53 +321,44 @@ public function parsePrimaryExpression() if ('(' === $this->parser->getCurrentToken()->getValue()) { $node = $this->getFunctionNode($token->getValue(), $token->getLine()); } else { - $node = new NameExpression($token->getValue(), $token->getLine()); + $node = new ContextVariable($token->getValue(), $token->getLine()); } } break; - case /* Token::NUMBER_TYPE */ 6: + case $token->test(Token::NUMBER_TYPE): $this->parser->getStream()->next(); $node = new ConstantExpression($token->getValue(), $token->getLine()); break; - case /* Token::STRING_TYPE */ 7: - case /* Token::INTERPOLATION_START_TYPE */ 10: + case $token->test(Token::STRING_TYPE): + case $token->test(Token::INTERPOLATION_START_TYPE): $node = $this->parseStringExpression(); break; - case /* Token::OPERATOR_TYPE */ 8: + case $token->test(Token::PUNCTUATION_TYPE): + $node = match ($token->getValue()) { + '[' => $this->parseSequenceExpression(), + '{' => $this->parseMappingExpression(), + default => throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()), + }; + break; + + case $token->test(Token::OPERATOR_TYPE): if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { // in this context, string operators are variable names $this->parser->getStream()->next(); - $node = new NameExpression($token->getValue(), $token->getLine()); + $node = new ContextVariable($token->getValue(), $token->getLine()); break; } - if (isset($this->unaryOperators[$token->getValue()])) { - $class = $this->unaryOperators[$token->getValue()]['class']; - if (!\in_array($class, [NegUnary::class, PosUnary::class])) { - throw new SyntaxError(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); - } - - $this->parser->getStream()->next(); - $expr = $this->parsePrimaryExpression(); - - $node = new $class($expr, $token->getLine()); - break; + if ('=' === $token->getValue() && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { + throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } // no break default: - if ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '[')) { - $node = $this->parseArrayExpression(); - } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) { - $node = $this->parseHashExpression(); - } elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { - throw new SyntaxError(sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); - } else { - throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); - } + throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } return $this->parsePostfixExpression($node); @@ -299,12 +372,12 @@ public function parseStringExpression() // a string cannot be followed by another string in a single expression $nextCanBeString = true; while (true) { - if ($nextCanBeString && $token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) { + if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) { $nodes[] = new ConstantExpression($token->getValue(), $token->getLine()); $nextCanBeString = false; - } elseif ($stream->nextIf(/* Token::INTERPOLATION_START_TYPE */ 10)) { + } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) { $nodes[] = $this->parseExpression(); - $stream->expect(/* Token::INTERPOLATION_END_TYPE */ 11); + $stream->expect(Token::INTERPOLATION_END_TYPE); $nextCanBeString = true; } else { break; @@ -319,26 +392,35 @@ public function parseStringExpression() return $expr; } + /** + * @deprecated since Twig 3.11, use parseSequenceExpression() instead + */ public function parseArrayExpression() + { + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__); + + return $this->parseSequenceExpression(); + } + + public function parseSequenceExpression() { $stream = $this->parser->getStream(); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'An array element was expected'); + $stream->expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected'); $node = new ArrayExpression([], $stream->getCurrent()->getLine()); $first = true; - while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { + while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) { if (!$first) { - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'An array element must be followed by a comma'); + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma'); // trailing ,? - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { break; } } $first = false; - if ($stream->test(/* Token::SPREAD_TYPE */ 13)) { - $stream->next(); + if ($stream->nextIf(Token::SPREAD_TYPE)) { $expr = $this->parseExpression(); $expr->setAttribute('spread', true); $node->addElement($expr); @@ -346,68 +428,77 @@ public function parseArrayExpression() $node->addElement($this->parseExpression()); } } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened array is not properly closed'); + $stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed'); return $node; } + /** + * @deprecated since Twig 3.11, use parseMappingExpression() instead + */ public function parseHashExpression() + { + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.', __METHOD__); + + return $this->parseMappingExpression(); + } + + public function parseMappingExpression() { $stream = $this->parser->getStream(); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A hash element was expected'); + $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); $node = new ArrayExpression([], $stream->getCurrent()->getLine()); $first = true; - while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) { + while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { if (!$first) { - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A hash value must be followed by a comma'); + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma'); // trailing ,? - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) { + if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { break; } } $first = false; - if ($stream->test(/* Token::SPREAD_TYPE */ 13)) { - $stream->next(); + if ($stream->nextIf(Token::SPREAD_TYPE)) { $value = $this->parseExpression(); $value->setAttribute('spread', true); $node->addElement($value); continue; } - // a hash key can be: + // a mapping key can be: // // * a number -- 12 // * a string -- 'a' // * a name, which is equivalent to a string -- a // * an expression, which must be enclosed in parentheses -- (1 + 2) - if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) { + if ($token = $stream->nextIf(Token::NAME_TYPE)) { $key = new ConstantExpression($token->getValue(), $token->getLine()); // {a} is a shortcut for {a:a} if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) { - $value = new NameExpression($key->getAttribute('value'), $key->getTemplateLine()); + $value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine()); $node->addElement($value, $key); continue; } - } elseif (($token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) || $token = $stream->nextIf(/* Token::NUMBER_TYPE */ 6)) { + } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) { $key = new ConstantExpression($token->getValue(), $token->getLine()); - } elseif ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + } elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $key = $this->parseExpression(); } else { $current = $stream->getCurrent(); - throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->toEnglish(), $current->getValue()), $current->getLine(), $stream->getSourceContext()); } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A hash key must be followed by a colon (:)'); + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)'); $value = $this->parseExpression(); $node->addElement($value, $key); } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened hash is not properly closed'); + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); return $node; } @@ -416,7 +507,7 @@ public function parsePostfixExpression($node) { while (true) { $token = $this->parser->getCurrentToken(); - if (/* Token::PUNCTUATION_TYPE */ 9 == $token->getType()) { + if ($token->test(Token::PUNCTUATION_TYPE)) { if ('.' == $token->getValue() || '[' == $token->getValue()) { $node = $this->parseSubscriptExpression($node); } elseif ('|' == $token->getValue()) { @@ -434,124 +525,38 @@ public function parsePostfixExpression($node) public function getFunctionNode($name, $line) { - switch ($name) { - case 'parent': - $this->parseArguments(); - if (!\count($this->parser->getBlockStack())) { - throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext()); - } - - if (!$this->parser->getParent() && !$this->parser->hasTraits()) { - throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext()); - } - - return new ParentExpression($this->parser->peekBlockStack(), $line); - case 'block': - $args = $this->parseArguments(); - if (\count($args) < 1) { - throw new SyntaxError('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext()); - } + if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { + return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $this->createArguments($line), $line); + } - return new BlockReferenceExpression($args->getNode(0), \count($args) > 1 ? $args->getNode(1) : null, $line); - case 'attribute': - $args = $this->parseArguments(); - if (\count($args) < 2) { - throw new SyntaxError('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext()); - } + $args = $this->parseNamedArguments(); + $function = $this->getFunction($name, $line); - return new GetAttrExpression($args->getNode(0), $args->getNode(1), \count($args) > 2 ? $args->getNode(2) : null, Template::ANY_CALL, $line); - default: - if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { - $arguments = new ArrayExpression([], $line); - foreach ($this->parseArguments() as $n) { - $arguments->addElement($n); - } + if ($function->getParserCallable()) { + $fakeNode = new EmptyNode($line); + $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); - $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line); - $node->setAttribute('safe', true); - - return $node; - } + return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line); + } - $args = $this->parseArguments(true); - $class = $this->getFunctionNodeClass($name, $line); + if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } - return new $class($name, $args, $line); + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); } + + return new $class($ready ? $function : $function->getName(), $args, $line); } public function parseSubscriptExpression($node) { - $stream = $this->parser->getStream(); - $token = $stream->next(); - $lineno = $token->getLine(); - $arguments = new ArrayExpression([], $lineno); - $type = Template::ANY_CALL; - if ('.' == $token->getValue()) { - $token = $stream->next(); - if ( - /* Token::NAME_TYPE */ 5 == $token->getType() - || - /* Token::NUMBER_TYPE */ 6 == $token->getType() - || - (/* Token::OPERATOR_TYPE */ 8 == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue())) - ) { - $arg = new ConstantExpression($token->getValue(), $lineno); - - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { - $type = Template::METHOD_CALL; - foreach ($this->parseArguments() as $n) { - $arguments->addElement($n); - } - } - } else { - throw new SyntaxError(sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext()); - } - - if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) { - $name = $arg->getAttribute('value'); - - $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno); - $node->setAttribute('safe', true); - - return $node; - } - } else { - $type = Template::ARRAY_CALL; - - // slice? - $slice = false; - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ':')) { - $slice = true; - $arg = new ConstantExpression(0, $token->getLine()); - } else { - $arg = $this->parseExpression(); - } - - if ($stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { - $slice = true; - } - - if ($slice) { - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { - $length = new ConstantExpression(null, $token->getLine()); - } else { - $length = $this->parseExpression(); - } - - $class = $this->getFilterNodeClass('slice', $token->getLine()); - $arguments = new Node([$arg, $length]); - $filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine()); - - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']'); - - return $filter; - } - - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']'); + if ('.' === $this->parser->getStream()->next()->getValue()) { + return $this->parseSubscriptExpressionDot($node); } - return new GetAttrExpression($node, $arg, $arguments, $type, $lineno); + return $this->parseSubscriptExpressionArray($node); } public function parseFilterExpression($node) @@ -561,23 +566,35 @@ public function parseFilterExpression($node) return $this->parseFilterExpressionRaw($node); } - public function parseFilterExpressionRaw($node, $tag = null) + public function parseFilterExpressionRaw($node) { + if (\func_num_args() > 1) { + trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); + } + while (true) { - $token = $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5); + $token = $this->parser->getStream()->expect(Token::NAME_TYPE); - $name = new ConstantExpression($token->getValue(), $token->getLine()); - if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { - $arguments = new Node(); + if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { + $arguments = new EmptyNode(); } else { - $arguments = $this->parseArguments(true, false, true); + $arguments = $this->parseNamedArguments(); + } + + $filter = $this->getFilter($token->getValue(), $token->getLine()); + + $ready = true; + if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); } - $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine()); + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } - $node = new $class($node, $name, $arguments, $token->getLine(), $tag); + $node = new $class($node, $ready ? $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments, $token->getLine()); - if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '|')) { + if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) { break; } @@ -590,51 +607,70 @@ public function parseFilterExpressionRaw($node, $tag = null) /** * Parses arguments. * - * @param bool $namedArguments Whether to allow named arguments or not - * @param bool $definition Whether we are parsing arguments for a function definition - * * @return Node * * @throws SyntaxError + * + * @deprecated since Twig 3.19 Use parseNamedArguments() instead */ - public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false) + public function parseArguments() { + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "%s::parseNamedArguments()" instead.', __METHOD__, __CLASS__)); + + $namedArguments = false; + $definition = false; + if (\func_num_args() > 1) { + $definition = func_get_arg(1); + } + if (\func_num_args() > 0) { + trigger_deprecation('twig/twig', '3.15', 'Passing arguments to "%s()" is deprecated.', __METHOD__); + $namedArguments = func_get_arg(0); + } + $args = []; $stream = $this->parser->getStream(); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(', 'A list of arguments must begin with an opening parenthesis'); - while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) { - if (!empty($args)) { - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'Arguments must be separated by a comma'); + $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + $hasSpread = false; + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { + if ($args) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); // if the comma above was a trailing comma, early exit the argument parse loop - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) { + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { break; } } if ($definition) { - $token = $stream->expect(/* Token::NAME_TYPE */ 5, null, 'An argument must be a name'); - $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine()); + $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); + $value = new ContextVariable($token->getValue(), $this->parser->getCurrentToken()->getLine()); } else { - $value = $this->parseExpression(0, $allowArrow); + if ($stream->nextIf(Token::SPREAD_TYPE)) { + $hasSpread = true; + $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine()); + } elseif ($hasSpread) { + throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } else { + $value = $this->parseExpression(); + } } $name = null; - if ($namedArguments && $token = $stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) { - if (!$value instanceof NameExpression) { - throw new SyntaxError(sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); + if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || (!$definition && $token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) { + if (!$value instanceof ContextVariable) { + throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); } $name = $value->getAttribute('name'); if ($definition) { - $value = $this->parsePrimaryExpression(); + $value = $this->getPrimary(); if (!$this->checkConstantExpression($value)) { - throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, or an array).', $token->getLine(), $stream->getSourceContext()); + throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); } } else { - $value = $this->parseExpression(0, $allowArrow); + $value = $this->parseExpression(); } } @@ -642,6 +678,7 @@ public function parseArguments($namedArguments = false, $definition = false, $al if (null === $name) { $name = $value->getAttribute('name'); $value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine()); + $value->setAttribute('is_implicit', true); } $args[$name] = $value; } else { @@ -652,9 +689,9 @@ public function parseArguments($namedArguments = false, $definition = false, $al } } } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'A list of arguments must be closed by a parenthesis'); + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); - return new Node($args); + return new Nodes($args); } public function parseAssignmentExpression() @@ -663,24 +700,20 @@ public function parseAssignmentExpression() $targets = []; while (true) { $token = $this->parser->getCurrentToken(); - if ($stream->test(/* Token::OPERATOR_TYPE */ 8) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { + if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { // in this context, string operators are variable names $this->parser->getStream()->next(); } else { - $stream->expect(/* Token::NAME_TYPE */ 5, null, 'Only variables can be assigned to'); - } - $value = $token->getValue(); - if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) { - throw new SyntaxError(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext()); + $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); } - $targets[] = new AssignNameExpression($value, $token->getLine()); + $targets[] = new AssignContextVariable($token->getValue(), $token->getLine()); - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } - return new Node($targets); + return new Nodes($targets); } public function parseMultitargetExpression() @@ -688,12 +721,12 @@ public function parseMultitargetExpression() $targets = []; while (true) { $targets[] = $this->parseExpression(); - if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } - return new Node($targets); + return new Nodes($targets); } private function parseNotTestExpression(Node $node): NotUnary @@ -704,124 +737,127 @@ private function parseNotTestExpression(Node $node): NotUnary private function parseTestExpression(Node $node): TestExpression { $stream = $this->parser->getStream(); - list($name, $test) = $this->getTest($node->getTemplateLine()); + $test = $this->getTest($node->getTemplateLine()); - $class = $this->getTestNodeClass($test); $arguments = null; - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { - $arguments = $this->parseArguments(true); + if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { + $arguments = $this->parseNamedArguments(); } elseif ($test->hasOneMandatoryArgument()) { - $arguments = new Node([0 => $this->parsePrimaryExpression()]); + $arguments = new Nodes([0 => $this->getPrimary()]); + } + + if ('defined' === $test->getName() && $node instanceof ContextVariable && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { + $node = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); + } + + $ready = $test instanceof TwigTest; + if (!isset($this->readyNodes[$class = $test->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); } - if ('defined' === $name && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { - $node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); - $node->setAttribute('safe', true); + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); } - return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine()); + return new $class($node, $ready ? $test : $test->getName(), $arguments, $this->parser->getCurrentToken()->getLine()); } - private function getTest(int $line): array + private function getTest(int $line): TwigTest { $stream = $this->parser->getStream(); - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); - if ($test = $this->env->getTest($name)) { - return [$name, $test]; - } - - if ($stream->test(/* Token::NAME_TYPE */ 5)) { + if ($stream->test(Token::NAME_TYPE)) { // try 2-words tests $name = $name.' '.$this->parser->getCurrentToken()->getValue(); if ($test = $this->env->getTest($name)) { $stream->next(); - - return [$name, $test]; } + } else { + $test = $this->env->getTest($name); } - $e = new SyntaxError(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); - $e->addSuggestions($name, array_keys($this->env->getTests())); + if (!$test) { + if ($this->parser->shouldIgnoreUnknownTwigCallables()) { + return new TwigTest($name, fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getTests())); - throw $e; - } + throw $e; + } - private function getTestNodeClass(TwigTest $test): string - { if ($test->isDeprecated()) { $stream = $this->parser->getStream(); - $message = sprintf('Twig Test "%s" is deprecated', $test->getName()); - - if ($test->getDeprecatedVersion()) { - $message .= sprintf(' since version %s', $test->getDeprecatedVersion()); - } - if ($test->getAlternative()) { - $message .= sprintf('. Use "%s" instead', $test->getAlternative()); - } $src = $stream->getSourceContext(); - $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine()); - - @trigger_error($message, \E_USER_DEPRECATED); + $test->triggerDeprecation($src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine()); } - return $test->getNodeClass(); + return $test; } - private function getFunctionNodeClass(string $name, int $line): string + private function getFunction(string $name, int $line): TwigFunction { - if (!$function = $this->env->getFunction($name)) { - $e = new SyntaxError(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()); + try { + $function = $this->env->getFunction($name); + } catch (SyntaxError $e) { + if (!$this->parser->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $function = null; + } + + if (!$function) { + if ($this->parser->shouldIgnoreUnknownTwigCallables()) { + return new TwigFunction($name, fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()); $e->addSuggestions($name, array_keys($this->env->getFunctions())); throw $e; } if ($function->isDeprecated()) { - $message = sprintf('Twig Function "%s" is deprecated', $function->getName()); - if ($function->getDeprecatedVersion()) { - $message .= sprintf(' since version %s', $function->getDeprecatedVersion()); - } - if ($function->getAlternative()) { - $message .= sprintf('. Use "%s" instead', $function->getAlternative()); - } $src = $this->parser->getStream()->getSourceContext(); - $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); - - @trigger_error($message, \E_USER_DEPRECATED); + $function->triggerDeprecation($src->getPath() ?: $src->getName(), $line); } - return $function->getNodeClass(); + return $function; } - private function getFilterNodeClass(string $name, int $line): string + private function getFilter(string $name, int $line): TwigFilter { - if (!$filter = $this->env->getFilter($name)) { - $e = new SyntaxError(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()); + try { + $filter = $this->env->getFilter($name); + } catch (SyntaxError $e) { + if (!$this->parser->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $filter = null; + } + if (!$filter) { + if ($this->parser->shouldIgnoreUnknownTwigCallables()) { + return new TwigFilter($name, fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()); $e->addSuggestions($name, array_keys($this->env->getFilters())); throw $e; } if ($filter->isDeprecated()) { - $message = sprintf('Twig Filter "%s" is deprecated', $filter->getName()); - if ($filter->getDeprecatedVersion()) { - $message .= sprintf(' since version %s', $filter->getDeprecatedVersion()); - } - if ($filter->getAlternative()) { - $message .= sprintf('. Use "%s" instead', $filter->getAlternative()); - } $src = $this->parser->getStream()->getSourceContext(); - $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); - - @trigger_error($message, \E_USER_DEPRECATED); + $filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line); } - return $filter->getNodeClass(); + return $filter; } // checks that the node only contains "constant" elements + // to be removed in 4.0 private function checkConstantExpression(Node $node): bool { if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression @@ -838,4 +874,160 @@ private function checkConstantExpression(Node $node): bool return true; } + + private function setDeprecationCheck(bool $deprecationCheck): bool + { + $current = $this->deprecationCheck; + $this->deprecationCheck = $deprecationCheck; + + return $current; + } + + private function createArguments(int $line): ArrayExpression + { + $arguments = new ArrayExpression([], $line); + foreach ($this->parseNamedArguments() as $k => $n) { + $arguments->addElement($n, new LocalVariable($k, $line)); + } + + return $arguments; + } + + /** + * @deprecated since Twig 3.19 Use parseNamedArguments() instead + */ + public function parseOnlyArguments() + { + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "%s::parseNamedArguments()" instead.', __METHOD__, __CLASS__)); + + return $this->parseNamedArguments(); + } + + public function parseNamedArguments(): Nodes + { + $args = []; + $stream = $this->parser->getStream(); + $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + $hasSpread = false; + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { + if ($args) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + + // if the comma above was a trailing comma, early exit the argument parse loop + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { + break; + } + } + + if ($stream->nextIf(Token::SPREAD_TYPE)) { + $hasSpread = true; + $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine()); + } elseif ($hasSpread) { + throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } else { + $value = $this->parseExpression(); + } + + $name = null; + if (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) { + if (!$value instanceof ContextVariable) { + throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); + } + $name = $value->getAttribute('name'); + $value = $this->parseExpression(); + } + + if (null === $name) { + $args[] = $value; + } else { + $args[$name] = $value; + } + } + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return new Nodes($args); + } + + private function parseSubscriptExpressionDot(Node $node): AbstractExpression + { + $stream = $this->parser->getStream(); + $token = $stream->getCurrent(); + $lineno = $token->getLine(); + $arguments = new ArrayExpression([], $lineno); + $type = Template::ANY_CALL; + + if ($stream->nextIf(Token::PUNCTUATION_TYPE, '(')) { + $attribute = $this->parseExpression(); + $stream->expect(Token::PUNCTUATION_TYPE, ')'); + } else { + $token = $stream->next(); + if ( + $token->test(Token::NAME_TYPE) + || $token->test(Token::NUMBER_TYPE) + || ($token->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) + ) { + $attribute = new ConstantExpression($token->getValue(), $token->getLine()); + } else { + throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext()); + } + } + + if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { + $type = Template::METHOD_CALL; + $arguments = $this->createArguments($token->getLine()); + } + + if ( + $node instanceof ContextVariable + && ( + null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name')) + || '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression + ) + ) { + return new MacroReferenceExpression(new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $node->getTemplateLine()); + } + + return new GetAttrExpression($node, $attribute, $arguments, $type, $lineno); + } + + private function parseSubscriptExpressionArray(Node $node): AbstractExpression + { + $stream = $this->parser->getStream(); + $token = $stream->getCurrent(); + $lineno = $token->getLine(); + $arguments = new ArrayExpression([], $lineno); + + // slice? + $slice = false; + if ($stream->test(Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + $attribute = new ConstantExpression(0, $token->getLine()); + } else { + $attribute = $this->parseExpression(); + } + + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + } + + if ($slice) { + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { + $length = new ConstantExpression(null, $token->getLine()); + } else { + $length = $this->parseExpression(); + } + + $filter = $this->getFilter('slice', $token->getLine()); + $arguments = new Nodes([$attribute, $length]); + $filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine()); + + $stream->expect(Token::PUNCTUATION_TYPE, ']'); + + return $filter; + } + + $stream->expect(Token::PUNCTUATION_TYPE, ']'); + + return new GetAttrExpression($node, $attribute, $arguments, Template::ARRAY_CALL, $lineno); + } } diff --git a/app/vendor/twig/twig/src/Extension/AbstractExtension.php b/app/vendor/twig/twig/src/Extension/AbstractExtension.php index 422925f31..26c00c680 100644 --- a/app/vendor/twig/twig/src/Extension/AbstractExtension.php +++ b/app/vendor/twig/twig/src/Extension/AbstractExtension.php @@ -11,7 +11,7 @@ namespace Twig\Extension; -abstract class AbstractExtension implements ExtensionInterface +abstract class AbstractExtension implements LastModifiedExtensionInterface { public function getTokenParsers() { @@ -40,6 +40,23 @@ public function getFunctions() public function getOperators() { - return []; + return [[], []]; + } + + public function getLastModified(): int + { + $filename = (new \ReflectionClass($this))->getFileName(); + if (!is_file($filename)) { + return 0; + } + + $lastModified = filemtime($filename); + + // Track modifications of the runtime class if it exists and follows the naming convention + if (str_ends_with($filename, 'Extension.php') && is_file($filename = substr($filename, 0, -13).'Runtime.php')) { + $lastModified = max($lastModified, filemtime($filename)); + } + + return $lastModified; } } diff --git a/app/vendor/twig/twig/src/Extension/CoreExtension.php b/app/vendor/twig/twig/src/Extension/CoreExtension.php index 36aa8f10a..a351f570a 100644 --- a/app/vendor/twig/twig/src/Extension/CoreExtension.php +++ b/app/vendor/twig/twig/src/Extension/CoreExtension.php @@ -9,8 +9,16 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\DeprecatedCallableInfo; +use Twig\Environment; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; use Twig\ExpressionParser; +use Twig\Markup; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\Binary\AddBinary; use Twig\Node\Expression\Binary\AndBinary; use Twig\Node\Expression\Binary\BitwiseAndBinary; @@ -18,6 +26,7 @@ use Twig\Node\Expression\Binary\BitwiseXorBinary; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\Binary\DivBinary; +use Twig\Node\Expression\Binary\ElvisBinary; use Twig\Node\Expression\Binary\EndsWithBinary; use Twig\Node\Expression\Binary\EqualBinary; use Twig\Node\Expression\Binary\FloorDivBinary; @@ -33,14 +42,20 @@ use Twig\Node\Expression\Binary\MulBinary; use Twig\Node\Expression\Binary\NotEqualBinary; use Twig\Node\Expression\Binary\NotInBinary; +use Twig\Node\Expression\Binary\NullCoalesceBinary; use Twig\Node\Expression\Binary\OrBinary; use Twig\Node\Expression\Binary\PowerBinary; use Twig\Node\Expression\Binary\RangeBinary; use Twig\Node\Expression\Binary\SpaceshipBinary; use Twig\Node\Expression\Binary\StartsWithBinary; use Twig\Node\Expression\Binary\SubBinary; +use Twig\Node\Expression\Binary\XorBinary; +use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\Filter\DefaultFilter; -use Twig\Node\Expression\NullCoalesceExpression; +use Twig\Node\Expression\FunctionNode\EnumCasesFunction; +use Twig\Node\Expression\FunctionNode\EnumFunction; +use Twig\Node\Expression\GetAttrExpression; +use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\Test\ConstantTest; use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Expression\Test\DivisiblebyTest; @@ -51,7 +66,14 @@ use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; -use Twig\NodeVisitor\MacroAutoImportNodeVisitor; +use Twig\Node\Node; +use Twig\OperatorPrecedenceChange; +use Twig\Parser; +use Twig\Sandbox\SecurityNotAllowedMethodError; +use Twig\Sandbox\SecurityNotAllowedPropertyError; +use Twig\Source; +use Twig\Template; +use Twig\TemplateWrapper; use Twig\TokenParser\ApplyTokenParser; use Twig\TokenParser\BlockTokenParser; use Twig\TokenParser\DeprecatedTokenParser; @@ -61,19 +83,38 @@ use Twig\TokenParser\FlushTokenParser; use Twig\TokenParser\ForTokenParser; use Twig\TokenParser\FromTokenParser; +use Twig\TokenParser\GuardTokenParser; use Twig\TokenParser\IfTokenParser; use Twig\TokenParser\ImportTokenParser; use Twig\TokenParser\IncludeTokenParser; use Twig\TokenParser\MacroTokenParser; use Twig\TokenParser\SetTokenParser; +use Twig\TokenParser\TypesTokenParser; use Twig\TokenParser\UseTokenParser; use Twig\TokenParser\WithTokenParser; use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; +use Twig\Util\CallableArgumentsExtractor; final class CoreExtension extends AbstractExtension { + public const ARRAY_LIKE_CLASSES = [ + 'ArrayIterator', + 'ArrayObject', + 'CachingIterator', + 'RecursiveArrayIterator', + 'RecursiveCachingIterator', + 'SplDoublyLinkedList', + 'SplFixedArray', + 'SplObjectStorage', + 'SplQueue', + 'SplStack', + 'WeakMap', + ]; + + private const DEFAULT_TRIM_CHARS = " \t\n\r\0\x0B"; + private $dateFormats = ['F j, Y H:i', '%d days']; private $numberFormat = [0, '.', ',']; private $timezone = null; @@ -81,8 +122,8 @@ final class CoreExtension extends AbstractExtension /** * Sets the default format to be used by the date filter. * - * @param string $format The default date format string - * @param string $dateIntervalFormat The default date interval format string + * @param string|null $format The default date format string + * @param string|null $dateIntervalFormat The default date interval format string */ public function setDateFormat($format = null, $dateIntervalFormat = null) { @@ -165,11 +206,13 @@ public function getTokenParsers(): array new ImportTokenParser(), new FromTokenParser(), new SetTokenParser(), + new TypesTokenParser(), new FlushTokenParser(), new DoTokenParser(), new EmbedTokenParser(), new WithTokenParser(), new DeprecatedTokenParser(), + new GuardTokenParser(), ]; } @@ -177,65 +220,73 @@ public function getFilters(): array { return [ // formatting filters - new TwigFilter('date', 'twig_date_format_filter', ['needs_environment' => true]), - new TwigFilter('date_modify', 'twig_date_modify_filter', ['needs_environment' => true]), - new TwigFilter('format', 'twig_sprintf'), - new TwigFilter('replace', 'twig_replace_filter'), - new TwigFilter('number_format', 'twig_number_format_filter', ['needs_environment' => true]), + new TwigFilter('date', [$this, 'formatDate']), + new TwigFilter('date_modify', [$this, 'modifyDate']), + new TwigFilter('format', [self::class, 'sprintf']), + new TwigFilter('replace', [self::class, 'replace']), + new TwigFilter('number_format', [$this, 'formatNumber']), new TwigFilter('abs', 'abs'), - new TwigFilter('round', 'twig_round'), + new TwigFilter('round', [self::class, 'round']), // encoding - new TwigFilter('url_encode', 'twig_urlencode_filter'), + new TwigFilter('url_encode', [self::class, 'urlencode']), new TwigFilter('json_encode', 'json_encode'), - new TwigFilter('convert_encoding', 'twig_convert_encoding'), + new TwigFilter('convert_encoding', [self::class, 'convertEncoding']), // string filters - new TwigFilter('title', 'twig_title_string_filter', ['needs_environment' => true]), - new TwigFilter('capitalize', 'twig_capitalize_string_filter', ['needs_environment' => true]), - new TwigFilter('upper', 'twig_upper_filter', ['needs_environment' => true]), - new TwigFilter('lower', 'twig_lower_filter', ['needs_environment' => true]), - new TwigFilter('striptags', 'twig_striptags'), - new TwigFilter('trim', 'twig_trim_filter'), - new TwigFilter('nl2br', 'twig_nl2br', ['pre_escape' => 'html', 'is_safe' => ['html']]), - new TwigFilter('spaceless', 'twig_spaceless', ['is_safe' => ['html']]), + new TwigFilter('title', [self::class, 'titleCase'], ['needs_charset' => true]), + new TwigFilter('capitalize', [self::class, 'capitalize'], ['needs_charset' => true]), + new TwigFilter('upper', [self::class, 'upper'], ['needs_charset' => true]), + new TwigFilter('lower', [self::class, 'lower'], ['needs_charset' => true]), + new TwigFilter('striptags', [self::class, 'striptags']), + new TwigFilter('trim', [self::class, 'trim']), + new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]), + new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12')]), // array helpers - new TwigFilter('join', 'twig_join_filter'), - new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]), - new TwigFilter('sort', 'twig_sort_filter', ['needs_environment' => true]), - new TwigFilter('merge', 'twig_array_merge'), - new TwigFilter('batch', 'twig_array_batch'), - new TwigFilter('column', 'twig_array_column'), - new TwigFilter('filter', 'twig_array_filter', ['needs_environment' => true]), - new TwigFilter('map', 'twig_array_map', ['needs_environment' => true]), - new TwigFilter('reduce', 'twig_array_reduce', ['needs_environment' => true]), + new TwigFilter('join', [self::class, 'join']), + new TwigFilter('split', [self::class, 'split'], ['needs_charset' => true]), + new TwigFilter('sort', [self::class, 'sort'], ['needs_environment' => true]), + new TwigFilter('merge', [self::class, 'merge']), + new TwigFilter('batch', [self::class, 'batch']), + new TwigFilter('column', [self::class, 'column']), + new TwigFilter('filter', [self::class, 'filter'], ['needs_environment' => true]), + new TwigFilter('map', [self::class, 'map'], ['needs_environment' => true]), + new TwigFilter('reduce', [self::class, 'reduce'], ['needs_environment' => true]), + new TwigFilter('find', [self::class, 'find'], ['needs_environment' => true]), // string/array filters - new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]), - new TwigFilter('length', 'twig_length_filter', ['needs_environment' => true]), - new TwigFilter('slice', 'twig_slice', ['needs_environment' => true]), - new TwigFilter('first', 'twig_first', ['needs_environment' => true]), - new TwigFilter('last', 'twig_last', ['needs_environment' => true]), + new TwigFilter('reverse', [self::class, 'reverse'], ['needs_charset' => true]), + new TwigFilter('shuffle', [self::class, 'shuffle'], ['needs_charset' => true]), + new TwigFilter('length', [self::class, 'length'], ['needs_charset' => true]), + new TwigFilter('slice', [self::class, 'slice'], ['needs_charset' => true]), + new TwigFilter('first', [self::class, 'first'], ['needs_charset' => true]), + new TwigFilter('last', [self::class, 'last'], ['needs_charset' => true]), // iteration and runtime - new TwigFilter('default', '_twig_default_filter', ['node_class' => DefaultFilter::class]), - new TwigFilter('keys', 'twig_get_array_keys_filter'), + new TwigFilter('default', [self::class, 'default'], ['node_class' => DefaultFilter::class]), + new TwigFilter('keys', [self::class, 'keys']), + new TwigFilter('invoke', [self::class, 'invoke']), ]; } public function getFunctions(): array { return [ + new TwigFunction('parent', null, ['parser_callable' => [self::class, 'parseParentFunction']]), + new TwigFunction('block', null, ['parser_callable' => [self::class, 'parseBlockFunction']]), + new TwigFunction('attribute', null, ['parser_callable' => [self::class, 'parseAttributeFunction']]), new TwigFunction('max', 'max'), new TwigFunction('min', 'min'), new TwigFunction('range', 'range'), - new TwigFunction('constant', 'twig_constant'), - new TwigFunction('cycle', 'twig_cycle'), - new TwigFunction('random', 'twig_random', ['needs_environment' => true]), - new TwigFunction('date', 'twig_date_converter', ['needs_environment' => true]), - new TwigFunction('include', 'twig_include', ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), - new TwigFunction('source', 'twig_source', ['needs_environment' => true, 'is_safe' => ['all']]), + new TwigFunction('constant', [self::class, 'constant']), + new TwigFunction('cycle', [self::class, 'cycle']), + new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]), + new TwigFunction('date', [$this, 'convertDate']), + new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), + new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), + new TwigFunction('enum_cases', [self::class, 'enumCases'], ['node_class' => EnumCasesFunction::class]), + new TwigFunction('enum', [self::class, 'enum'], ['node_class' => EnumFunction::class]), ]; } @@ -250,26 +301,32 @@ public function getTests(): array new TwigTest('null', null, ['node_class' => NullTest::class]), new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]), new TwigTest('constant', null, ['node_class' => ConstantTest::class]), - new TwigTest('empty', 'twig_test_empty'), + new TwigTest('empty', [self::class, 'testEmpty']), new TwigTest('iterable', 'is_iterable'), + new TwigTest('sequence', [self::class, 'testSequence']), + new TwigTest('mapping', [self::class, 'testMapping']), ]; } public function getNodeVisitors(): array { - return [new MacroAutoImportNodeVisitor()]; + return []; } public function getOperators(): array { return [ [ - 'not' => ['precedence' => 50, 'class' => NotUnary::class], + 'not' => ['precedence' => 50, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 70), 'class' => NotUnary::class], '-' => ['precedence' => 500, 'class' => NegUnary::class], '+' => ['precedence' => 500, 'class' => PosUnary::class], ], [ + '? :' => ['precedence' => 5, 'class' => ElvisBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], + '?:' => ['precedence' => 5, 'class' => ElvisBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], + '??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 5), 'class' => NullCoalesceBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], 'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + 'xor' => ['precedence' => 12, 'class' => XorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'and' => ['precedence' => 15, 'class' => AndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'b-or' => ['precedence' => 16, 'class' => BitwiseOrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'b-xor' => ['precedence' => 17, 'class' => BitwiseXorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], @@ -291,7 +348,7 @@ public function getOperators(): array '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '~' => ['precedence' => 40, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + '~' => ['precedence' => 40, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 27), 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '//' => ['precedence' => 60, 'class' => FloorDivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], @@ -299,196 +356,229 @@ public function getOperators(): array 'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - '??' => ['precedence' => 300, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], ], ]; } -} -} -namespace { - use Twig\Environment; - use Twig\Error\LoaderError; - use Twig\Error\RuntimeError; - use Twig\Extension\CoreExtension; - use Twig\Extension\SandboxExtension; - use Twig\Markup; - use Twig\Source; - use Twig\Template; - use Twig\TemplateWrapper; - -/** - * Cycles over a value. - * - * @param \ArrayAccess|array $values - * @param int $position The cycle position - * - * @return string The next value in the cycle - */ -function twig_cycle($values, $position) -{ - if (!\is_array($values) && !$values instanceof \ArrayAccess) { - return $values; - } + /** + * Cycles over a sequence. + * + * @param array|\ArrayAccess $values A non-empty sequence of values + * @param int<0, max> $position The position of the value to return in the cycle + * + * @return mixed The value at the given position in the sequence, wrapping around as needed + * + * @internal + */ + public static function cycle($values, $position): mixed + { + if (!\is_array($values)) { + if (!$values instanceof \ArrayAccess) { + throw new RuntimeError('The "cycle" function expects an array or "ArrayAccess" as first argument.'); + } - return $values[$position % \count($values)]; -} + if (!is_countable($values)) { + // To be uncommented in 4.0 + // throw new RuntimeError('The "cycle" function expects a countable sequence as first argument.'); -/** - * Returns a random value depending on the supplied parameter type: - * - a random item from a \Traversable or array - * - a random character from a string - * - a random integer between 0 and the integer parameter. - * - * @param \Traversable|array|int|float|string $values The values to pick a random item from - * @param int|null $max Maximum value used when $values is an int - * - * @return mixed A random value from the given sequence - * - * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) - */ -function twig_random(Environment $env, $values = null, $max = null) -{ - if (null === $values) { - return null === $max ? mt_rand() : mt_rand(0, (int) $max); - } + trigger_deprecation('twig/twig', '3.12', 'Passing a non-countable sequence of values to "%s()" is deprecated.', __METHOD__); - if (\is_int($values) || \is_float($values)) { - if (null === $max) { - if ($values < 0) { - $max = 0; - $min = $values; - } else { - $max = $values; - $min = 0; + return $values; } - } else { - $min = $values; + + $values = self::toArray($values, false); } - return mt_rand((int) $min, (int) $max); + if (!$count = \count($values)) { + throw new RuntimeError('The "cycle" function expects a non-empty sequence.'); + } + + return $values[$position % $count]; } - if (\is_string($values)) { - if ('' === $values) { - return ''; + /** + * Returns a random value depending on the supplied parameter type: + * - a random item from a \Traversable or array + * - a random character from a string + * - a random integer between 0 and the integer parameter. + * + * @param \Traversable|array|int|float|string $values The values to pick a random item from + * @param int|null $max Maximum value used when $values is an int + * + * @return mixed A random value from the given sequence + * + * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) + * + * @internal + */ + public static function random(string $charset, $values = null, $max = null) + { + if (null === $values) { + return null === $max ? mt_rand() : mt_rand(0, (int) $max); } - $charset = $env->getCharset(); + if (\is_int($values) || \is_float($values)) { + if (null === $max) { + if ($values < 0) { + $max = 0; + $min = $values; + } else { + $max = $values; + $min = 0; + } + } else { + $min = $values; + } - if ('UTF-8' !== $charset) { - $values = twig_convert_encoding($values, 'UTF-8', $charset); + return mt_rand((int) $min, (int) $max); } - // unicode version of str_split() - // split at all positions, but not after the start and not before the end - $values = preg_split('/(? $value) { - $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); + if ('UTF-8' !== $charset) { + $values = self::convertEncoding($values, 'UTF-8', $charset); + } + + // unicode version of str_split() + // split at all positions, but not after the start and not before the end + $values = preg_split('/(? $value) { + $values[$i] = self::convertEncoding($value, $charset, 'UTF-8'); + } } } - } - if (!is_iterable($values)) { - return $values; + if (!is_iterable($values)) { + return $values; + } + + $values = self::toArray($values); + + if (0 === \count($values)) { + throw new RuntimeError('The "random" function cannot pick from an empty sequence or mapping.'); + } + + return $values[array_rand($values, 1)]; } - $values = twig_to_array($values); + /** + * Formats a date. + * + * {{ post.published_at|date("m/d/Y") }} + * + * @param \DateTimeInterface|\DateInterval|string|int|null $date A date, a timestamp or null to use the current time + * @param string|null $format The target format, null to use the default + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + */ + public function formatDate($date, $format = null, $timezone = null): string + { + if (null === $format) { + $formats = $this->getDateFormat(); + $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + } + + if ($date instanceof \DateInterval) { + return $date->format($format); + } - if (0 === \count($values)) { - throw new RuntimeError('The random function cannot pick from an empty array.'); + return $this->convertDate($date, $timezone)->format($format); } - return $values[array_rand($values, 1)]; -} + /** + * Returns a new date object modified. + * + * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} + * + * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time + * @param string $modifier A modifier string + * + * @return \DateTime|\DateTimeImmutable + * + * @internal + */ + public function modifyDate($date, $modifier) + { + return $this->convertDate($date, false)->modify($modifier); + } -/** - * Converts a date to the given format. - * - * {{ post.published_at|date("m/d/Y") }} - * - * @param \DateTimeInterface|\DateInterval|string $date A date - * @param string|null $format The target format, null to use the default - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return string The formatted date - */ -function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) -{ - if (null === $format) { - $formats = $env->getExtension(CoreExtension::class)->getDateFormat(); - $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + /** + * Returns a formatted string. + * + * @param string|null $format + * @param ...$values + * + * @internal + */ + public static function sprintf($format, ...$values): string + { + return \sprintf($format ?? '', ...$values); } - if ($date instanceof \DateInterval) { - return $date->format($format); + /** + * @internal + */ + public static function dateConverter(Environment $env, $date, $format = null, $timezone = null): string + { + return $env->getExtension(self::class)->formatDate($date, $format, $timezone); } - return twig_date_converter($env, $date, $timezone)->format($format); -} + /** + * Converts an input to a \DateTime instance. + * + * {% if date(user.created_at) < date('+2days') %} + * {# do something #} + * {% endif %} + * + * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return \DateTime|\DateTimeImmutable + */ + public function convertDate($date = null, $timezone = null) + { + // determine the timezone + if (false !== $timezone) { + if (null === $timezone) { + $timezone = $this->getTimezone(); + } elseif (!$timezone instanceof \DateTimeZone) { + $timezone = new \DateTimeZone($timezone); + } + } -/** - * Returns a new date object modified. - * - * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} - * - * @param \DateTimeInterface|string $date A date - * @param string $modifier A modifier string - * - * @return \DateTimeInterface - */ -function twig_date_modify_filter(Environment $env, $date, $modifier) -{ - $date = twig_date_converter($env, $date, false); + // immutable dates + if ($date instanceof \DateTimeImmutable) { + return false !== $timezone ? $date->setTimezone($timezone) : $date; + } - return $date->modify($modifier); -} + if ($date instanceof \DateTime) { + $date = clone $date; + if (false !== $timezone) { + $date->setTimezone($timezone); + } -/** - * Returns a formatted string. - * - * @param string|null $format - * @param ...$values - * - * @return string - */ -function twig_sprintf($format, ...$values) -{ - return sprintf($format ?? '', ...$values); -} + return $date; + } -/** - * Converts an input to a \DateTime instance. - * - * {% if date(user.created_at) < date('+2days') %} - * {# do something #} - * {% endif %} - * - * @param \DateTimeInterface|string|null $date A date or null to use the current time - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return \DateTimeInterface - */ -function twig_date_converter(Environment $env, $date = null, $timezone = null) -{ - // determine the timezone - if (false !== $timezone) { - if (null === $timezone) { - $timezone = $env->getExtension(CoreExtension::class)->getTimezone(); - } elseif (!$timezone instanceof \DateTimeZone) { - $timezone = new \DateTimeZone($timezone); + if (null === $date || 'now' === $date) { + if (null === $date) { + $date = 'now'; + } + + return new \DateTime($date, false !== $timezone ? $timezone : $this->getTimezone()); } - } - // immutable dates - if ($date instanceof \DateTimeImmutable) { - return false !== $timezone ? $date->setTimezone($timezone) : $date; - } + $asString = (string) $date; + if (ctype_digit($asString) || ('' !== $asString && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + $date = new \DateTime('@'.$date); + } else { + $date = new \DateTime($date); + } - if ($date instanceof \DateTimeInterface) { - $date = clone $date; if (false !== $timezone) { $date->setTimezone($timezone); } @@ -496,1255 +586,1577 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null) return $date; } - if (null === $date || 'now' === $date) { - if (null === $date) { - $date = 'now'; + /** + * Replaces strings within a string. + * + * @param string|null $str String to replace in + * @param array|\Traversable $from Replace values + * + * @internal + */ + public static function replace($str, $from): string + { + if (!is_iterable($from)) { + throw new RuntimeError(\sprintf('The "replace" filter expects a sequence or a mapping, got "%s".', get_debug_type($from))); } - return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone()); + return strtr($str ?? '', self::toArray($from)); } - $asString = (string) $date; - if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { - $date = new \DateTime('@'.$date); - } else { - $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone()); - } + /** + * Rounds a number. + * + * @param int|float|string|null $value The value to round + * @param int|float $precision The rounding precision + * @param 'common'|'ceil'|'floor' $method The method to use for rounding + * + * @return float The rounded number + * + * @internal + */ + public static function round($value, $precision = 0, $method = 'common') + { + $value = (float) $value; - if (false !== $timezone) { - $date->setTimezone($timezone); - } + if ('common' === $method) { + return round($value, $precision); + } - return $date; -} + if ('ceil' !== $method && 'floor' !== $method) { + throw new RuntimeError('The "round" filter only supports the "common", "ceil", and "floor" methods.'); + } -/** - * Replaces strings within a string. - * - * @param string|null $str String to replace in - * @param array|\Traversable $from Replace values - * - * @return string - */ -function twig_replace_filter($str, $from) -{ - if (!is_iterable($from)) { - throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); + return $method($value * 10 ** $precision) / 10 ** $precision; } - return strtr($str ?? '', twig_to_array($from)); -} + /** + * Formats a number. + * + * All of the formatting options can be left null, in that case the defaults will + * be used. Supplying any of the parameters will override the defaults set in the + * environment object. + * + * @param mixed $number A float/int/string of the number to format + * @param int|null $decimal the number of decimal points to display + * @param string|null $decimalPoint the character(s) to use for the decimal point + * @param string|null $thousandSep the character(s) to use for the thousands separator + */ + public function formatNumber($number, $decimal = null, $decimalPoint = null, $thousandSep = null): string + { + $defaults = $this->getNumberFormat(); + if (null === $decimal) { + $decimal = $defaults[0]; + } -/** - * Rounds a number. - * - * @param int|float|string|null $value The value to round - * @param int|float $precision The rounding precision - * @param string $method The method to use for rounding - * - * @return int|float The rounded number - */ -function twig_round($value, $precision = 0, $method = 'common') -{ - $value = (float) $value; + if (null === $decimalPoint) { + $decimalPoint = $defaults[1]; + } + + if (null === $thousandSep) { + $thousandSep = $defaults[2]; + } - if ('common' === $method) { - return round($value, $precision); + return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); } - if ('ceil' !== $method && 'floor' !== $method) { - throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); + /** + * URL encodes (RFC 3986) a string as a path segment or an array as a query string. + * + * @param string|array|null $url A URL or an array of query parameters + * + * @internal + */ + public static function urlencode($url): string + { + if (\is_array($url)) { + return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); + } + + return rawurlencode($url ?? ''); } - return $method($value * 10 ** $precision) / 10 ** $precision; -} + /** + * Merges any number of arrays or Traversable objects. + * + * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} + * + * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %} + * + * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #} + * + * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge + * + * @internal + */ + public static function merge(...$arrays): array + { + $result = []; -/** - * Number format filter. - * - * All of the formatting options can be left null, in that case the defaults will - * be used. Supplying any of the parameters will override the defaults set in the - * environment object. - * - * @param mixed $number A float/int/string of the number to format - * @param int $decimal the number of decimal points to display - * @param string $decimalPoint the character(s) to use for the decimal point - * @param string $thousandSep the character(s) to use for the thousands separator - * - * @return string The formatted number - */ -function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) -{ - $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat(); - if (null === $decimal) { - $decimal = $defaults[0]; + foreach ($arrays as $argNumber => $array) { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "merge" filter expects a sequence or a mapping, got "%s" for argument %d.', get_debug_type($array), $argNumber + 1)); + } + + $result = array_merge($result, self::toArray($array)); + } + + return $result; } - if (null === $decimalPoint) { - $decimalPoint = $defaults[1]; + /** + * Slices a variable. + * + * @param mixed $item A variable + * @param int $start Start of the slice + * @param int $length Size of the slice + * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) + * + * @return mixed The sliced variable + * + * @internal + */ + public static function slice(string $charset, $item, $start, $length = null, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + while ($item instanceof \IteratorAggregate) { + $item = $item->getIterator(); + } + + if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { + try { + return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); + } catch (\OutOfBoundsException $e) { + return []; + } + } + + $item = iterator_to_array($item, $preserveKeys); + } + + if (\is_array($item)) { + return \array_slice($item, $start, $length, $preserveKeys); + } + + return mb_substr((string) $item, $start, $length, $charset); } - if (null === $thousandSep) { - $thousandSep = $defaults[2]; + /** + * Returns the first element of the item. + * + * @param mixed $item A variable + * + * @return mixed The first element of the item + * + * @internal + */ + public static function first(string $charset, $item) + { + $elements = self::slice($charset, $item, 0, 1, false); + + return \is_string($elements) ? $elements : current($elements); } - return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); -} + /** + * Returns the last element of the item. + * + * @param mixed $item A variable + * + * @return mixed The last element of the item + * + * @internal + */ + public static function last(string $charset, $item) + { + $elements = self::slice($charset, $item, -1, 1, false); -/** - * URL encodes (RFC 3986) a string as a path segment or an array as a query string. - * - * @param string|array|null $url A URL or an array of query parameters - * - * @return string The URL encoded value - */ -function twig_urlencode_filter($url) -{ - if (\is_array($url)) { - return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); + return \is_string($elements) ? $elements : current($elements); } - return rawurlencode($url ?? ''); -} + /** + * Joins the values to a string. + * + * The separators between elements are empty strings per default, you can define them with the optional parameters. + * + * {{ [1, 2, 3]|join(', ', ' and ') }} + * {# returns 1, 2 and 3 #} + * + * {{ [1, 2, 3]|join('|') }} + * {# returns 1|2|3 #} + * + * {{ [1, 2, 3]|join }} + * {# returns 123 #} + * + * @param iterable|array|string|float|int|bool|null $value An array + * @param string $glue The separator + * @param string|null $and The separator for the last pair + * + * @internal + */ + public static function join($value, $glue = '', $and = null): string + { + if (!is_iterable($value)) { + $value = (array) $value; + } -/** - * Merges any number of arrays or Traversable objects. - * - * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} - * - * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %} - * - * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #} - * - * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge - * - * @return array The merged array - */ -function twig_array_merge(...$arrays) -{ - $result = []; + $value = self::toArray($value, false); - foreach ($arrays as $argNumber => $array) { - if (!is_iterable($array)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); + if (0 === \count($value)) { + return ''; + } + + if (null === $and || $and === $glue) { + return implode($glue, $value); + } + + if (1 === \count($value)) { + return $value[0]; } - $result = array_merge($result, twig_to_array($array)); + return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; } - return $result; -} + /** + * Splits the string into an array. + * + * {{ "one,two,three"|split(',') }} + * {# returns [one, two, three] #} + * + * {{ "one,two,three,four,five"|split(',', 3) }} + * {# returns [one, two, "three,four,five"] #} + * + * {{ "123"|split('') }} + * {# returns [1, 2, 3] #} + * + * {{ "aabbcc"|split('', 2) }} + * {# returns [aa, bb, cc] #} + * + * @param string|null $value A string + * @param string $delimiter The delimiter + * @param int|null $limit The limit + * + * @internal + */ + public static function split(string $charset, $value, $delimiter, $limit = null): array + { + $value = $value ?? ''; + if ('' !== $delimiter) { + return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); + } -/** - * Slices a variable. - * - * @param mixed $item A variable - * @param int $start Start of the slice - * @param int $length Size of the slice - * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) - * - * @return mixed The sliced variable - */ -function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - while ($item instanceof \IteratorAggregate) { - $item = $item->getIterator(); + if ($limit <= 1) { + return preg_split('/(?= 0 && $length >= 0 && $item instanceof \Iterator) { - try { - return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); - } catch (\OutOfBoundsException $e) { - return []; - } + $length = mb_strlen($value, $charset); + if ($length < $limit) { + return [$value]; } - $item = iterator_to_array($item, $preserveKeys); - } + $r = []; + for ($i = 0; $i < $length; $i += $limit) { + $r[] = mb_substr($value, $i, $limit, $charset); + } - if (\is_array($item)) { - return \array_slice($item, $start, $length, $preserveKeys); + return $r; } - return mb_substr((string) $item, $start, $length, $env->getCharset()); -} + /** + * @internal + */ + public static function default($value, $default = '') + { + if (self::testEmpty($value)) { + return $default; + } -/** - * Returns the first element of the item. - * - * @param mixed $item A variable - * - * @return mixed The first element of the item - */ -function twig_first(Environment $env, $item) -{ - $elements = twig_slice($env, $item, 0, 1, false); + return $value; + } - return \is_string($elements) ? $elements : current($elements); -} + /** + * Returns the keys for the given array. + * + * It is useful when you want to iterate over the keys of an array: + * + * {% for key in array|keys %} + * {# ... #} + * {% endfor %} + * + * @internal + */ + public static function keys($array): array + { + if ($array instanceof \Traversable) { + while ($array instanceof \IteratorAggregate) { + $array = $array->getIterator(); + } -/** - * Returns the last element of the item. - * - * @param mixed $item A variable - * - * @return mixed The last element of the item - */ -function twig_last(Environment $env, $item) -{ - $elements = twig_slice($env, $item, -1, 1, false); + $keys = []; + if ($array instanceof \Iterator) { + $array->rewind(); + while ($array->valid()) { + $keys[] = $array->key(); + $array->next(); + } - return \is_string($elements) ? $elements : current($elements); -} + return $keys; + } -/** - * Joins the values to a string. - * - * The separators between elements are empty strings per default, you can define them with the optional parameters. - * - * {{ [1, 2, 3]|join(', ', ' and ') }} - * {# returns 1, 2 and 3 #} - * - * {{ [1, 2, 3]|join('|') }} - * {# returns 1|2|3 #} - * - * {{ [1, 2, 3]|join }} - * {# returns 123 #} - * - * @param array $value An array - * @param string $glue The separator - * @param string|null $and The separator for the last pair - * - * @return string The concatenated string - */ -function twig_join_filter($value, $glue = '', $and = null) -{ - if (!is_iterable($value)) { - $value = (array) $value; - } + foreach ($array as $key => $item) { + $keys[] = $key; + } - $value = twig_to_array($value, false); + return $keys; + } - if (0 === \count($value)) { - return ''; - } + if (!\is_array($array)) { + return []; + } - if (null === $and || $and === $glue) { - return implode($glue, $value); + return array_keys($array); } - if (1 === \count($value)) { - return $value[0]; + /** + * Invokes a callable. + * + * @internal + */ + public static function invoke(\Closure $arrow, ...$arguments): mixed + { + return $arrow(...$arguments); } - return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; -} + /** + * Reverses a variable. + * + * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string + * @param bool $preserveKeys Whether to preserve key or not + * + * @return mixed The reversed input + * + * @internal + */ + public static function reverse(string $charset, $item, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + return array_reverse(iterator_to_array($item), $preserveKeys); + } -/** - * Splits the string into an array. - * - * {{ "one,two,three"|split(',') }} - * {# returns [one, two, three] #} - * - * {{ "one,two,three,four,five"|split(',', 3) }} - * {# returns [one, two, "three,four,five"] #} - * - * {{ "123"|split('') }} - * {# returns [1, 2, 3] #} - * - * {{ "aabbcc"|split('', 2) }} - * {# returns [aa, bb, cc] #} - * - * @param string|null $value A string - * @param string $delimiter The delimiter - * @param int $limit The limit - * - * @return array The split string as an array - */ -function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) -{ - $value = $value ?? ''; + if (\is_array($item)) { + return array_reverse($item, $preserveKeys); + } - if ('' !== $delimiter) { - return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); - } + $string = (string) $item; - if ($limit <= 1) { - return preg_split('/(?getCharset()); - if ($length < $limit) { - return [$value]; + $string = implode('', array_reverse($matches[0])); + + if ('UTF-8' !== $charset) { + $string = self::convertEncoding($string, $charset, 'UTF-8'); + } + + return $string; } - $r = []; - for ($i = 0; $i < $length; $i += $limit) { - $r[] = mb_substr($value, $i, $limit, $env->getCharset()); + /** + * Shuffles an array, a \Traversable instance, or a string. + * The function does not preserve keys. + * + * @param array|\Traversable|string|null $item + * + * @return mixed + * + * @internal + */ + public static function shuffle(string $charset, $item) + { + if (\is_string($item)) { + if ('UTF-8' !== $charset) { + $item = self::convertEncoding($item, 'UTF-8', $charset); + } + + $item = preg_split('/(?getIterator(); + if (\is_string($compare)) { + if (\is_string($value) || \is_int($value) || \is_float($value)) { + return '' === $value || str_contains($compare, (string) $value); + } + + return false; } - $keys = []; - if ($array instanceof \Iterator) { - $array->rewind(); - while ($array->valid()) { - $keys[] = $array->key(); - $array->next(); + if (!is_iterable($compare)) { + return false; + } + + if (\is_object($value) || \is_resource($value)) { + if (!\is_array($compare)) { + foreach ($compare as $item) { + if ($item === $value) { + return true; + } + } + + return false; } - return $keys; + return \in_array($value, $compare, true); } - foreach ($array as $key => $item) { - $keys[] = $key; + foreach ($compare as $item) { + if (0 === self::compare($value, $item)) { + return true; + } } - return $keys; + return false; } - if (!\is_array($array)) { - return []; - } + /** + * Compares two values using a more strict version of the PHP non-strict comparison operator. + * + * @see https://wiki.php.net/rfc/string_to_number_comparison + * @see https://wiki.php.net/rfc/trailing_whitespace_numerics + * + * @internal + */ + public static function compare($a, $b) + { + // int <=> string + if (\is_int($a) && \is_string($b)) { + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + if ((int) $bTrim == $bTrim) { + return $a <=> (int) $bTrim; + } else { + return (float) $a <=> (float) $bTrim; + } + } + if (\is_string($a) && \is_int($b)) { + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + if ((int) $aTrim == $aTrim) { + return (int) $aTrim <=> $b; + } else { + return (float) $aTrim <=> (float) $b; + } + } - return array_keys($array); -} + // float <=> string + if (\is_float($a) && \is_string($b)) { + if (is_nan($a)) { + return 1; + } + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } -/** - * Reverses a variable. - * - * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string - * @param bool $preserveKeys Whether to preserve key or not - * - * @return mixed The reversed input - */ -function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - return array_reverse(iterator_to_array($item), $preserveKeys); + return $a <=> (float) $bTrim; + } + if (\is_string($a) && \is_float($b)) { + if (is_nan($b)) { + return 1; + } + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + + return (float) $aTrim <=> $b; + } + + // fallback to <=> + return $a <=> $b; } - if (\is_array($item)) { - return array_reverse($item, $preserveKeys); + /** + * @throws RuntimeError When an invalid pattern is used + * + * @internal + */ + public static function matches(string $regexp, ?string $str): int + { + set_error_handler(function ($t, $m) use ($regexp) { + throw new RuntimeError(\sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); + }); + try { + return preg_match($regexp, $str ?? ''); + } finally { + restore_error_handler(); + } } - $string = (string) $item; + /** + * Returns a trimmed string. + * + * @param string|\Stringable|null $string + * @param string|null $characterMask + * @param string $side left, right, or both + * + * @throws RuntimeError When an invalid trimming side is used + * + * @internal + */ + public static function trim($string, $characterMask = null, $side = 'both'): string|\Stringable + { + if (null === $characterMask) { + $characterMask = self::DEFAULT_TRIM_CHARS; + } - $charset = $env->getCharset(); + $trimmed = match ($side) { + 'both' => trim($string ?? '', $characterMask), + 'left' => ltrim($string ?? '', $characterMask), + 'right' => rtrim($string ?? '', $characterMask), + default => throw new RuntimeError('Trimming side must be "left", "right" or "both".'), + }; - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); + // trimming a safe string with the default character mask always returns a safe string (independently of the context) + return $string instanceof Markup && self::DEFAULT_TRIM_CHARS === $characterMask ? new Markup($trimmed, $string->getCharset()) : $trimmed; } - preg_match_all('/./us', $string, $matches); - - $string = implode('', array_reverse($matches[0])); + /** + * Inserts HTML line breaks before all newlines in a string. + * + * @param string|null $string + * + * @internal + */ + public static function nl2br($string): string + { + return nl2br($string ?? ''); + } - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, $charset, 'UTF-8'); + /** + * Removes whitespaces between HTML tags. + * + * @param string|null $content + * + * @internal + */ + public static function spaceless($content): string + { + return trim(preg_replace('/>\s+<', $content ?? '')); } - return $string; -} + /** + * @param string|null $string + * @param string $to + * @param string $from + * + * @internal + */ + public static function convertEncoding($string, $to, $from): string + { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + } -/** - * Sorts an array. - * - * @param array|\Traversable $array - * - * @return array - */ -function twig_sort_filter(Environment $env, $array, $arrow = null) -{ - if ($array instanceof \Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); + return iconv($from, $to, $string ?? ''); } - if (null !== $arrow) { - twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter'); + /** + * Returns the length of a variable. + * + * @param mixed $thing A variable + * + * @internal + */ + public static function length(string $charset, $thing): int + { + if (null === $thing) { + return 0; + } - uasort($array, $arrow); - } else { - asort($array); - } + if (\is_scalar($thing)) { + return mb_strlen($thing, $charset); + } - return $array; -} + if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { + return \count($thing); + } -/** - * @internal - */ -function twig_in_filter($value, $compare) -{ - if ($value instanceof Markup) { - $value = (string) $value; + if ($thing instanceof \Traversable) { + return iterator_count($thing); + } + + if ($thing instanceof \Stringable) { + return mb_strlen((string) $thing, $charset); + } + + return 1; } - if ($compare instanceof Markup) { - $compare = (string) $compare; + + /** + * Converts a string to uppercase. + * + * @param string|null $string A string + * + * @internal + */ + public static function upper(string $charset, $string): string + { + return mb_strtoupper($string ?? '', $charset); } - if (\is_string($compare)) { - if (\is_string($value) || \is_int($value) || \is_float($value)) { - return '' === $value || str_contains($compare, (string) $value); - } + /** + * Converts a string to lowercase. + * + * @param string|null $string A string + * + * @internal + */ + public static function lower(string $charset, $string): string + { + return mb_strtolower($string ?? '', $charset); + } - return false; + /** + * Strips HTML and PHP tags from a string. + * + * @param string|null $string + * @param string[]|string|null $allowable_tags + * + * @internal + */ + public static function striptags($string, $allowable_tags = null): string + { + return strip_tags($string ?? '', $allowable_tags); } - if (!is_iterable($compare)) { - return false; + /** + * Returns a titlecased string. + * + * @param string|null $string A string + * + * @internal + */ + public static function titleCase(string $charset, $string): string + { + return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); } - if (\is_object($value) || \is_resource($value)) { - if (!\is_array($compare)) { - foreach ($compare as $item) { - if ($item === $value) { - return true; + /** + * Returns a capitalized string. + * + * @param string|null $string A string + * + * @internal + */ + public static function capitalize(string $charset, $string): string + { + return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); + } + + /** + * @internal + * + * to be removed in 4.0 + */ + public static function callMacro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) + { + if (!method_exists($template, $method)) { + $parent = $template; + while ($parent = $parent->getParent($context)) { + if (method_exists($parent, $method)) { + return $parent->$method(...$args); } } - return false; + throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); } - return \in_array($value, $compare, true); + return $template->$method(...$args); } - foreach ($compare as $item) { - if (0 === twig_compare($value, $item)) { - return true; + /** + * @template TSequence + * + * @param TSequence $seq + * + * @return ($seq is iterable ? TSequence : array{}) + * + * @internal + */ + public static function ensureTraversable($seq) + { + if (is_iterable($seq)) { + return $seq; } + + return []; } - return false; -} + /** + * @internal + */ + public static function toArray($seq, $preserveKeys = true) + { + if ($seq instanceof \Traversable) { + return iterator_to_array($seq, $preserveKeys); + } -/** - * Compares two values using a more strict version of the PHP non-strict comparison operator. - * - * @see https://wiki.php.net/rfc/string_to_number_comparison - * @see https://wiki.php.net/rfc/trailing_whitespace_numerics - * - * @internal - */ -function twig_compare($a, $b) -{ - // int <=> string - if (\is_int($a) && \is_string($b)) { - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; - } - if ((int) $bTrim == $bTrim) { - return $a <=> (int) $bTrim; - } else { - return (float) $a <=> (float) $bTrim; + if (!\is_array($seq)) { + return $seq; } + + return $preserveKeys ? $seq : array_values($seq); } - if (\is_string($a) && \is_int($b)) { - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; - } - if ((int) $aTrim == $aTrim) { - return (int) $aTrim <=> $b; - } else { - return (float) $aTrim <=> (float) $b; + + /** + * Checks if a variable is empty. + * + * {# evaluates to true if the foo variable is null, false, or the empty string #} + * {% if foo is empty %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @internal + */ + public static function testEmpty($value): bool + { + if ($value instanceof \Countable) { + return 0 === \count($value); } - } - // float <=> string - if (\is_float($a) && \is_string($b)) { - if (is_nan($a)) { - return 1; + if ($value instanceof \Traversable) { + return !iterator_count($value); } - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; + + if ($value instanceof \Stringable) { + return '' === (string) $value; } - return $a <=> (float) $bTrim; + return '' === $value || false === $value || null === $value || [] === $value; } - if (\is_string($a) && \is_float($b)) { - if (is_nan($b)) { - return 1; + + /** + * Checks if a variable is a sequence. + * + * {# evaluates to true if the foo variable is a sequence #} + * {% if foo is sequence %} + * {# ... #} + * {% endif %} + * + * @param mixed $value + * + * @internal + */ + public static function testSequence($value): bool + { + if ($value instanceof \ArrayObject) { + $value = $value->getArrayCopy(); } - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; + + if ($value instanceof \Traversable) { + $value = iterator_to_array($value); } - return (float) $aTrim <=> $b; + return \is_array($value) && array_is_list($value); } - // fallback to <=> - return $a <=> $b; -} + /** + * Checks if a variable is a mapping. + * + * {# evaluates to true if the foo variable is a mapping #} + * {% if foo is mapping %} + * {# ... #} + * {% endif %} + * + * @param mixed $value + * + * @internal + */ + public static function testMapping($value): bool + { + if ($value instanceof \ArrayObject) { + $value = $value->getArrayCopy(); + } -/** - * @return int - * - * @throws RuntimeError When an invalid pattern is used - */ -function twig_matches(string $regexp, ?string $str) -{ - set_error_handler(function ($t, $m) use ($regexp) { - throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); - }); - try { - return preg_match($regexp, $str ?? ''); - } finally { - restore_error_handler(); - } -} + if ($value instanceof \Traversable) { + $value = iterator_to_array($value); + } -/** - * Returns a trimmed string. - * - * @param string|null $string - * @param string|null $characterMask - * @param string $side - * - * @return string - * - * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') - */ -function twig_trim_filter($string, $characterMask = null, $side = 'both') -{ - if (null === $characterMask) { - $characterMask = " \t\n\r\0\x0B"; + return (\is_array($value) && !array_is_list($value)) || \is_object($value); } - switch ($side) { - case 'both': - return trim($string ?? '', $characterMask); - case 'left': - return ltrim($string ?? '', $characterMask); - case 'right': - return rtrim($string ?? '', $characterMask); - default: - throw new RuntimeError('Trimming side must be "left", "right" or "both".'); - } -} + /** + * Renders a template. + * + * @param array $context + * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively + * @param array $variables The variables to pass to the template + * @param bool $withContext + * @param bool $ignoreMissing Whether to ignore missing templates or not + * @param bool $sandboxed Whether to sandbox the template or not + * + * @internal + */ + public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false): string + { + $alreadySandboxed = false; + $sandbox = null; + if ($withContext) { + $variables = array_merge($context, $variables); + } -/** - * Inserts HTML line breaks before all newlines in a string. - * - * @param string|null $string - * - * @return string - */ -function twig_nl2br($string) -{ - return nl2br($string ?? ''); -} + if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { + $sandbox = $env->getExtension(SandboxExtension::class); + if (!$alreadySandboxed = $sandbox->isSandboxed()) { + $sandbox->enableSandbox(); + } + } -/** - * Removes whitespaces between HTML tags. - * - * @param string|null $string - * - * @return string - */ -function twig_spaceless($content) -{ - return trim(preg_replace('/>\s+<', $content ?? '')); -} + try { + $loaded = null; + try { + $loaded = $env->resolveTemplate($template); + } catch (LoaderError $e) { + if (!$ignoreMissing) { + throw $e; + } -/** - * @param string|null $string - * @param string $to - * @param string $from - * - * @return string - */ -function twig_convert_encoding($string, $to, $from) -{ - if (!\function_exists('iconv')) { - throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); - } + return ''; + } - return iconv($from, $to, $string ?? ''); -} + if ($isSandboxed) { + $loaded->unwrap()->checkSecurity(); + } -/** - * Returns the length of a variable. - * - * @param mixed $thing A variable - * - * @return int The length of the value - */ -function twig_length_filter(Environment $env, $thing) -{ - if (null === $thing) { - return 0; + return $loaded->render($variables); + } finally { + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); + } + } } - if (\is_scalar($thing)) { - return mb_strlen($thing, $env->getCharset()); - } + /** + * Returns a template content without rendering it. + * + * @param string $name The template name + * @param bool $ignoreMissing Whether to ignore missing templates or not + * + * @internal + */ + public static function source(Environment $env, $name, $ignoreMissing = false): string + { + $loader = $env->getLoader(); + try { + return $loader->getSourceContext($name)->getCode(); + } catch (LoaderError $e) { + if (!$ignoreMissing) { + throw $e; + } - if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { - return \count($thing); + return ''; + } } - if ($thing instanceof \Traversable) { - return iterator_count($thing); - } + /** + * Returns the list of cases of the enum. + * + * @template T of \UnitEnum + * + * @param class-string $enum + * + * @return list + * + * @internal + */ + public static function enumCases(string $enum): array + { + if (!enum_exists($enum)) { + throw new RuntimeError(\sprintf('Enum "%s" does not exist.', $enum)); + } - if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { - return mb_strlen((string) $thing, $env->getCharset()); + return $enum::cases(); } - return 1; -} - -/** - * Converts a string to uppercase. - * - * @param string|null $string A string - * - * @return string The uppercased string - */ -function twig_upper_filter(Environment $env, $string) -{ - return mb_strtoupper($string ?? '', $env->getCharset()); -} - -/** - * Converts a string to lowercase. - * - * @param string|null $string A string - * - * @return string The lowercased string - */ -function twig_lower_filter(Environment $env, $string) -{ - return mb_strtolower($string ?? '', $env->getCharset()); -} + /** + * Provides the ability to access enums by their class names. + * + * @template T of \UnitEnum + * + * @param class-string $enum + * + * @return T + * + * @internal + */ + public static function enum(string $enum): \UnitEnum + { + if (!enum_exists($enum)) { + throw new RuntimeError(\sprintf('"%s" is not an enum.', $enum)); + } -/** - * Strips HTML and PHP tags from a string. - * - * @param string|null $string - * @param string[]|string|null $string - * - * @return string - */ -function twig_striptags($string, $allowable_tags = null) -{ - return strip_tags($string ?? '', $allowable_tags); -} + if (!$cases = $enum::cases()) { + throw new RuntimeError(\sprintf('"%s" is an empty enum.', $enum)); + } -/** - * Returns a titlecased string. - * - * @param string|null $string A string - * - * @return string The titlecased string - */ -function twig_title_string_filter(Environment $env, $string) -{ - if (null !== $charset = $env->getCharset()) { - return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); + return $cases[0]; } - return ucwords(strtolower($string ?? '')); -} - -/** - * Returns a capitalized string. - * - * @param string|null $string A string - * - * @return string The capitalized string - */ -function twig_capitalize_string_filter(Environment $env, $string) -{ - $charset = $env->getCharset(); + /** + * Provides the ability to get constants from instances as well as class/global constants. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * @param bool $checkDefined Whether to check if the constant is defined or not + * + * @return mixed Class constants can return many types like scalars, arrays, and + * objects depending on the PHP version (\BackedEnum, \UnitEnum, etc.) + * When $checkDefined is true, returns true when the constant is defined, false otherwise + * + * @internal + */ + public static function constant($constant, $object = null, bool $checkDefined = false) + { + if (null !== $object) { + if ('class' === $constant) { + return $checkDefined ? true : \get_class($object); + } - return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); -} + $constant = \get_class($object).'::'.$constant; + } -/** - * @internal - */ -function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) -{ - if (!method_exists($template, $method)) { - $parent = $template; - while ($parent = $parent->getParent($context)) { - if (method_exists($parent, $method)) { - return $parent->$method(...$args); + if (!\defined($constant)) { + if ($checkDefined) { + return false; } - } - throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); - } + if ('::class' === strtolower(substr($constant, -7))) { + throw new RuntimeError(\sprintf('You cannot use the Twig function "constant" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant)); + } - return $template->$method(...$args); -} + throw new RuntimeError(\sprintf('Constant "%s" is undefined.', $constant)); + } -/** - * @internal - */ -function twig_ensure_traversable($seq) -{ - if (is_iterable($seq)) { - return $seq; + return $checkDefined ? true : \constant($constant); } - return []; -} + /** + * Batches item. + * + * @param array $items An array of items + * @param int $size The size of the batch + * @param mixed $fill A value used to fill missing items + * + * @internal + */ + public static function batch($items, $size, $fill = null, $preserveKeys = true): array + { + if (!is_iterable($items)) { + throw new RuntimeError(\sprintf('The "batch" filter expects a sequence or a mapping, got "%s".', get_debug_type($items))); + } -/** - * @internal - */ -function twig_to_array($seq, $preserveKeys = true) -{ - if ($seq instanceof \Traversable) { - return iterator_to_array($seq, $preserveKeys); - } + $size = (int) ceil($size); - if (!\is_array($seq)) { - return $seq; - } + $result = array_chunk(self::toArray($items, $preserveKeys), $size, $preserveKeys); - return $preserveKeys ? $seq : array_values($seq); -} + if (null !== $fill && $result) { + $last = \count($result) - 1; + if ($fillCount = $size - \count($result[$last])) { + for ($i = 0; $i < $fillCount; ++$i) { + $result[$last][] = $fill; + } + } + } -/** - * Checks if a variable is empty. - * - * {# evaluates to true if the foo variable is null, false, or the empty string #} - * {% if foo is empty %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is empty, false otherwise - */ -function twig_test_empty($value) -{ - if ($value instanceof \Countable) { - return 0 === \count($value); + return $result; } - if ($value instanceof \Traversable) { - return !iterator_count($value); - } + /** + * Returns the attribute value for a given array/object. + * + * @param mixed $object The object or array from where to get the item + * @param mixed $item The item to get from the array or object + * @param array $arguments An array of arguments to pass if the item is an object method + * @param string $type The type of attribute (@see \Twig\Template constants) + * @param bool $isDefinedTest Whether this is only a defined check + * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not + * @param int $lineno The template line where the attribute was called + * + * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true + * + * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false + * + * @internal + */ + public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) + { + $propertyNotAllowedError = null; - if (\is_object($value) && method_exists($value, '__toString')) { - return '' === (string) $value; - } + // array + if (Template::METHOD_CALL !== $type) { + $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; - return '' === $value || false === $value || null === $value || [] === $value; -} + if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + goto methodCheck; + } + } -/** - * Checks if a variable is traversable. - * - * {# evaluates to true if the foo variable is an array or a traversable object #} - * {% if foo is iterable %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is traversable - * - * @deprecated since Twig 3.8, to be removed in 4.0 (use the native "is_iterable" function instead) - */ -function twig_test_iterable($value) -{ - return is_iterable($value); -} + if (match (true) { + \is_array($object) => \array_key_exists($arrayItem, $object), + $object instanceof \ArrayAccess => $object->offsetExists($arrayItem), + default => false, + }) { + if ($isDefinedTest) { + return true; + } -/** - * Renders a template. - * - * @param array $context - * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively - * @param array $variables The variables to pass to the template - * @param bool $withContext - * @param bool $ignoreMissing Whether to ignore missing templates or not - * @param bool $sandboxed Whether to sandbox the template or not - * - * @return string The rendered template - */ -function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) -{ - $alreadySandboxed = false; - $sandbox = null; - if ($withContext) { - $variables = array_merge($context, $variables); - } + return $object[$arrayItem]; + } - if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { - $sandbox = $env->getExtension(SandboxExtension::class); - if (!$alreadySandboxed = $sandbox->isSandboxed()) { - $sandbox->enableSandbox(); - } + if (Template::ARRAY_CALL === $type || !\is_object($object)) { + if ($isDefinedTest) { + return false; + } - foreach ((\is_array($template) ? $template : [$template]) as $name) { - // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security - if ($name instanceof TemplateWrapper || $name instanceof Template) { - $name->unwrap()->checkSecurity(); + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + if ($object instanceof \ArrayAccess) { + $message = \sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); + } elseif (\is_object($object)) { + $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + } elseif (\is_array($object)) { + if (!$object) { + $message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem); + } else { + $message = \sprintf('Key "%s" for sequence/mapping with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + } + } elseif (Template::ARRAY_CALL === $type) { + if (null === $object) { + $message = \sprintf('Impossible to access a key ("%s") on a null variable.', $item); + } else { + $message = \sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object); + } + } elseif (null === $object) { + $message = \sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + } else { + $message = \sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object); + } + + throw new RuntimeError($message, $lineno, $source); } } - } - try { - $loaded = null; - try { - $loaded = $env->resolveTemplate($template); - } catch (LoaderError $e) { - if (!$ignoreMissing) { - throw $e; + $item = (string) $item; + + if (!\is_object($object)) { + if ($isDefinedTest) { + return false; } - } - return $loaded ? $loaded->render($variables) : ''; - } finally { - if ($isSandboxed && !$alreadySandboxed) { - $sandbox->disableSandbox(); - } - } -} + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } -/** - * Returns a template content without rendering it. - * - * @param string $name The template name - * @param bool $ignoreMissing Whether to ignore missing templates or not - * - * @return string The template source - */ -function twig_source(Environment $env, $name, $ignoreMissing = false) -{ - $loader = $env->getLoader(); - try { - return $loader->getSourceContext($name)->getCode(); - } catch (LoaderError $e) { - if (!$ignoreMissing) { - throw $e; - } - } -} + if (null === $object) { + $message = \sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); + } elseif (\is_array($object)) { + $message = \sprintf('Impossible to invoke a method ("%s") on a sequence/mapping.', $item); + } else { + $message = \sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object); + } -/** - * Provides the ability to get constants from instances as well as class/global constants. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return string - */ -function twig_constant($constant, $object = null) -{ - if (null !== $object) { - if ('class' === $constant) { - return \get_class($object); + throw new RuntimeError($message, $lineno, $source); } - $constant = \get_class($object).'::'.$constant; - } + if ($object instanceof Template) { + throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); + } - if (!\defined($constant)) { - throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant)); - } + // object property + if (Template::METHOD_CALL !== $type) { + if ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + goto methodCheck; + } + } - return \constant($constant); -} + static $propertyCheckers = []; -/** - * Checks if a constant exists. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return bool - */ -function twig_constant_is_defined($constant, $object = null) -{ - if (null !== $object) { - if ('class' === $constant) { - return true; - } + if ($object instanceof \Closure && '__invoke' === $item) { + return $isDefinedTest ? true : $object(); + } - $constant = \get_class($object).'::'.$constant; - } + if (isset($object->$item) + || ($propertyCheckers[$object::class][$item] ??= self::getPropertyChecker($object::class, $item))($object, $item) + ) { + if ($isDefinedTest) { + return true; + } - return \defined($constant); -} + return $object->$item; + } -/** - * Batches item. - * - * @param array $items An array of items - * @param int $size The size of the batch - * @param mixed $fill A value used to fill missing items - * - * @return array - */ -function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) -{ - if (!is_iterable($items)) { - throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); - } + if ($object instanceof \DateTimeInterface && \in_array($item, ['date', 'timezone', 'timezone_type'], true)) { + if ($isDefinedTest) { + return true; + } - $size = ceil($size); + return ((array) $object)[$item]; + } - $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys); + if (\defined($object::class.'::'.$item)) { + if ($isDefinedTest) { + return true; + } - if (null !== $fill && $result) { - $last = \count($result) - 1; - if ($fillCount = $size - \count($result[$last])) { - for ($i = 0; $i < $fillCount; ++$i) { - $result[$last][] = $fill; + return \constant($object::class.'::'.$item); } } - } - return $result; -} + methodCheck: -/** - * Returns the attribute value for a given array/object. - * - * @param mixed $object The object or array from where to get the item - * @param mixed $item The item to get from the array or object - * @param array $arguments An array of arguments to pass if the item is an object method - * @param string $type The type of attribute (@see \Twig\Template constants) - * @param bool $isDefinedTest Whether this is only a defined check - * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not - * @param int $lineno The template line where the attribute was called - * - * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true - * - * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false - * - * @internal - */ -function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) -{ - // array - if (/* Template::METHOD_CALL */ 'method' !== $type) { - $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + static $cache = []; - if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) - || ($object instanceof ArrayAccess && isset($object[$arrayItem])) - ) { - if ($isDefinedTest) { - return true; + $class = \get_class($object); + + // object method + // precedence: getXxx() > isXxx() > hasXxx() + if (!isset($cache[$class])) { + $methods = get_class_methods($object); + if ($object instanceof \Closure) { + $methods[] = '__invoke'; } + sort($methods); + $lcMethods = array_map('strtolower', $methods); + $classCache = []; + foreach ($methods as $i => $method) { + $classCache[$method] = $method; + $classCache[$lcName = $lcMethods[$i]] = $method; + + if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) { + $name = substr($method, 2); + $lcName = substr($lcName, 2); + } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + if (\in_array('is'.$lcName, $lcMethods)) { + continue; + } + } else { + continue; + } + + // skip get() and is() methods (in which case, $name is empty) + if ($name) { + if (!isset($classCache[$name])) { + $classCache[$name] = $method; + } - return $object[$arrayItem]; + if (!isset($classCache[$lcName])) { + $classCache[$lcName] = $method; + } + } + } + $cache[$class] = $classCache; } - if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { + $call = false; + if (isset($cache[$class][$item])) { + $method = $cache[$class][$item]; + } elseif (isset($cache[$class][$lcItem = strtolower($item)])) { + $method = $cache[$class][$lcItem]; + } elseif (isset($cache[$class]['__call'])) { + $method = $item; + $call = true; + } else { if ($isDefinedTest) { return false; } + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; + } + if ($ignoreStrictCheck || !$env->isStrictVariables()) { return; } - if ($object instanceof ArrayAccess) { - $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); - } elseif (\is_object($object)) { - $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); - } elseif (\is_array($object)) { - if (empty($object)) { - $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); - } else { - $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + 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 ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + } catch (SecurityNotAllowedMethodError $e) { + if ($isDefinedTest) { + return false; } - } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { - if (null === $object) { - $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); - } else { - $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; } - } elseif (null === $object) { - $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); - } else { - $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); - } - throw new RuntimeError($message, $lineno, $source); + throw $e; + } } - } - if (!\is_object($object)) { if ($isDefinedTest) { - return false; + return true; } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; + // Some objects throw exceptions when they have __call, and the method we try + // to call is not supported. If ignoreStrictCheck is true, we should return null. + try { + $ret = $object->$method(...$arguments); + } catch (\BadMethodCallException $e) { + if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { + return; + } + throw $e; } - if (null === $object) { - $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); - } elseif (\is_array($object)) { - $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); - } else { - $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + return $ret; + } + + /** + * Returns the values from a single column in the input array. + * + *
+     *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
+     *
+     *  {% set fruits = items|column('fruit') %}
+     *
+     *  {# fruits now contains ['apple', 'orange'] #}
+     * 
+ * + * @param array|\Traversable $array An array + * @param int|string $name The column name + * @param int|string|null $index The column to use as the index/keys for the returned array + * + * @return array The array of values + * + * @internal + */ + public static function column($array, $name, $index = null): array + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "column" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); } - throw new RuntimeError($message, $lineno, $source); - } + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } - if ($object instanceof Template) { - throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); + return array_column($array, $name, $index); } - // object property - if (/* Template::METHOD_CALL */ 'method' !== $type) { - if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { - if ($isDefinedTest) { - return true; - } + /** + * @param \Closure $arrow + * + * @internal + */ + public static function filter(Environment $env, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array))); + } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); - } + self::checkArrow($env, $arrow, 'filter', 'filter'); - return $object->$item; + if (\is_array($array)) { + return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); } - } - static $cache = []; + // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator + return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); + } - $class = \get_class($object); + /** + * @param \Closure $arrow + * + * @internal + */ + public static function find(Environment $env, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "find" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); + } - // object method - // precedence: getXxx() > isXxx() > hasXxx() - if (!isset($cache[$class])) { - $methods = get_class_methods($object); - sort($methods); - $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods); - $classCache = []; - foreach ($methods as $i => $method) { - $classCache[$method] = $method; - $classCache[$lcName = $lcMethods[$i]] = $method; + self::checkArrow($env, $arrow, 'find', 'filter'); - if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) { - $name = substr($method, 2); - $lcName = substr($lcName, 2); - } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - if (\in_array('is'.$lcName, $lcMethods)) { - continue; - } - } else { - continue; + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return $v; } + } - // skip get() and is() methods (in which case, $name is empty) - if ($name) { - if (!isset($classCache[$name])) { - $classCache[$name] = $method; - } + return null; + } - if (!isset($classCache[$lcName])) { - $classCache[$lcName] = $method; - } - } + /** + * @param \Closure $arrow + * + * @internal + */ + public static function map(Environment $env, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "map" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); } - $cache[$class] = $classCache; + + self::checkArrow($env, $arrow, 'map', 'filter'); + + $r = []; + foreach ($array as $k => $v) { + $r[$k] = $arrow($v, $k); + } + + return $r; } - $call = false; - if (isset($cache[$class][$item])) { - $method = $cache[$class][$item]; - } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { - $method = $cache[$class][$lcItem]; - } elseif (isset($cache[$class]['__call'])) { - $method = $item; - $call = true; - } else { - if ($isDefinedTest) { - return false; + /** + * @param \Closure $arrow + * + * @internal + */ + public static function reduce(Environment $env, $array, $arrow, $initial = null) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "reduce" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; + self::checkArrow($env, $arrow, 'reduce', 'filter'); + + $accumulator = $initial; + foreach ($array as $key => $value) { + $accumulator = $arrow($accumulator, $value, $key); } - 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); + return $accumulator; } - if ($isDefinedTest) { - return true; - } + /** + * @param \Closure $arrow + * + * @internal + */ + public static function arraySome(Environment $env, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "has some" test expects a sequence or a mapping, got "%s".', get_debug_type($array))); + } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); - } + self::checkArrow($env, $arrow, 'has some', 'operator'); - // Some objects throw exceptions when they have __call, and the method we try - // to call is not supported. If ignoreStrictCheck is true, we should return null. - try { - $ret = $object->$method(...$arguments); - } catch (\BadMethodCallException $e) { - if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { - return; + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return true; + } } - throw $e; + + return false; } - return $ret; -} + /** + * @param \Closure $arrow + * + * @internal + */ + public static function arrayEvery(Environment $env, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "has every" test expects a sequence or a mapping, got "%s".', get_debug_type($array))); + } -/** - * Returns the values from a single column in the input array. - * - *
- *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
- *
- *  {% set fruits = items|column('fruit') %}
- *
- *  {# fruits now contains ['apple', 'orange'] #}
- * 
- * - * @param array|Traversable $array An array - * @param mixed $name The column name - * @param mixed $index The column to use as the index/keys for the returned array - * - * @return array The array of values - */ -function twig_array_column($array, $name, $index = null): array -{ - if ($array instanceof Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); - } + self::checkArrow($env, $arrow, 'has every', 'operator'); - return array_column($array, $name, $index); -} + foreach ($array as $k => $v) { + if (!$arrow($v, $k)) { + return false; + } + } -function twig_array_filter(Environment $env, $array, $arrow) -{ - if (!is_iterable($array)) { - throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); + return true; } - twig_check_arrow_in_sandbox($env, $arrow, 'filter', 'filter'); + /** + * @internal + */ + public static function checkArrow(Environment $env, $arrow, $thing, $type) + { + if ($arrow instanceof \Closure) { + return; + } + + if ($env->hasExtension(SandboxExtension::class) && $env->getExtension(SandboxExtension::class)->isSandboxed()) { + throw new RuntimeError(\sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); + } - if (\is_array($array)) { - return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); + trigger_deprecation('twig/twig', '3.15', 'Passing a callable that is not a PHP \Closure as an argument to the "%s" %s is deprecated.', $thing, $type); } - // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator - return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); -} + /** + * @internal to be removed in Twig 4 + */ + public static function captureOutput(iterable $body): string + { + $level = ob_get_level(); + ob_start(); -function twig_array_map(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'map', 'filter'); + try { + foreach ($body as $data) { + echo $data; + } + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } - $r = []; - foreach ($array as $k => $v) { - $r[$k] = $arrow($v, $k); + return ob_get_clean(); } - return $r; -} + /** + * @internal + */ + public static function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + if (!$blockName = $parser->peekBlockStack()) { + throw new SyntaxError('Calling the "parent" function outside of a block is forbidden.', $line, $parser->getStream()->getSourceContext()); + } -function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'reduce', 'filter'); + if (!$parser->hasInheritance()) { + throw new SyntaxError('Calling the "parent" function on a template that does not call "extends" or "use" is forbidden.', $line, $parser->getStream()->getSourceContext()); + } - if (!\is_array($array) && !$array instanceof \Traversable) { - throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + return new ParentExpression($blockName, $line); } - $accumulator = $initial; - foreach ($array as $key => $value) { - $accumulator = $arrow($accumulator, $value, $key); + /** + * @internal + */ + public static function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + $fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); + + return new BlockReferenceExpression($args[0], $args[1] ?? null, $line); } - return $accumulator; -} + /** + * @internal + */ + public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); -function twig_array_some(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'has some', 'operator'); + /* + Deprecation to uncomment sometimes during the lifetime of the 4.x branch + $src = $parser->getStream()->getSourceContext(); + $dep = new DeprecatedCallableInfo('twig/twig', '3.15', 'The "attribute" function is deprecated, use the "." notation instead.'); + $dep->setName('attribute'); + $dep->setType('function'); + $dep->triggerDeprecation($src->getPath() ?: $src->getName(), $line); + */ - foreach ($array as $k => $v) { - if ($arrow($v, $k)) { - return true; - } + return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); } - return false; -} + private static function getPropertyChecker(string $class, string $property): \Closure + { + static $classReflectors = []; -function twig_array_every(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'has every', 'operator'); + $class = $classReflectors[$class] ??= new \ReflectionClass($class); - foreach ($array as $k => $v) { - if (!$arrow($v, $k)) { - return false; + if (!$class->hasProperty($property)) { + static $propertyExists; + + return $propertyExists ??= \Closure::fromCallable('property_exists'); } - } - return true; -} + $property = $class->getProperty($property); -function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) -{ - if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { - throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); + if (!$property->isPublic() || $property->isStatic()) { + static $false; + + return $false ??= static fn () => false; + } + + return static fn ($object) => $property->isInitialized($object); } } -} diff --git a/app/vendor/twig/twig/src/Extension/DebugExtension.php b/app/vendor/twig/twig/src/Extension/DebugExtension.php index c0f10d5a3..dac21c317 100644 --- a/app/vendor/twig/twig/src/Extension/DebugExtension.php +++ b/app/vendor/twig/twig/src/Extension/DebugExtension.php @@ -9,7 +9,11 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\Template; +use Twig\TemplateWrapper; use Twig\TwigFunction; final class DebugExtension extends AbstractExtension @@ -18,47 +22,41 @@ public function getFunctions(): array { // dump is safe if var_dump is overridden by xdebug $isDumpOutputHtmlSafe = \extension_loaded('xdebug') - // false means that it was not set (and the default is on) or it explicitly enabled - && (false === \ini_get('xdebug.overload_var_dump') || \ini_get('xdebug.overload_var_dump')) - // false means that it was not set (and the default is on) or it explicitly enabled - // xdebug.overload_var_dump produces HTML only when html_errors is also enabled + // Xdebug overloads var_dump in develop mode when html_errors is enabled + && str_contains(\ini_get('xdebug.mode'), 'develop') && (false === \ini_get('html_errors') || \ini_get('html_errors')) || 'cli' === \PHP_SAPI ; return [ - new TwigFunction('dump', 'twig_var_dump', ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]), + new TwigFunction('dump', [self::class, 'dump'], ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]), ]; } -} -} -namespace { -use Twig\Environment; -use Twig\Template; -use Twig\TemplateWrapper; - -function twig_var_dump(Environment $env, $context, ...$vars) -{ - if (!$env->isDebug()) { - return; - } + /** + * @internal + */ + public static function dump(Environment $env, $context, ...$vars) + { + if (!$env->isDebug()) { + return; + } - ob_start(); + ob_start(); - if (!$vars) { - $vars = []; - foreach ($context as $key => $value) { - if (!$value instanceof Template && !$value instanceof TemplateWrapper) { - $vars[$key] = $value; + if (!$vars) { + $vars = []; + foreach ($context as $key => $value) { + if (!$value instanceof Template && !$value instanceof TemplateWrapper) { + $vars[$key] = $value; + } } + + var_dump($vars); + } else { + var_dump(...$vars); } - var_dump($vars); - } else { - var_dump(...$vars); + return ob_get_clean(); } - - return ob_get_clean(); -} } diff --git a/app/vendor/twig/twig/src/Extension/EscaperExtension.php b/app/vendor/twig/twig/src/Extension/EscaperExtension.php index ef8879dbd..c5625fa6a 100644 --- a/app/vendor/twig/twig/src/Extension/EscaperExtension.php +++ b/app/vendor/twig/twig/src/Extension/EscaperExtension.php @@ -9,22 +9,24 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; use Twig\FileExtensionEscapingStrategy; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\Filter\RawFilter; +use Twig\Node\Node; use Twig\NodeVisitor\EscaperNodeVisitor; +use Twig\Runtime\EscaperRuntime; use Twig\TokenParser\AutoEscapeTokenParser; use Twig\TwigFilter; final class EscaperExtension extends AbstractExtension { - private $defaultStrategy; + private $environment; private $escapers = []; - - /** @internal */ - public $safeClasses = []; - - /** @internal */ - public $safeLookup = []; + private $escaper; + private $defaultStrategy; /** * @param string|false|callable $defaultStrategy An escaping strategy @@ -49,19 +51,53 @@ public function getNodeVisitors(): array public function getFilters(): array { return [ - new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), - new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), - new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]), + new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]), ]; } + public function getLastModified(): int + { + return max( + parent::getLastModified(), + filemtime((new \ReflectionClass(EscaperRuntime::class))->getFileName()), + ); + } + + /** + * @deprecated since Twig 3.10 + */ + public function setEnvironment(Environment $environment): void + { + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; + if ($triggerDeprecation) { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); + } + + $this->environment = $environment; + $this->escaper = $environment->getRuntime(EscaperRuntime::class); + } + + /** + * @return void + * + * @deprecated since Twig 3.10 + */ + public function setEscaperRuntime(EscaperRuntime $escaper) + { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); + + $this->escaper = $escaper; + } + /** * Sets the default strategy to use when not defined by the user. * * The strategy can be a valid PHP callback that takes the template * name as an argument and returns the strategy to use. * - * @param string|false|callable $defaultStrategy An escaping strategy + * @param string|false|callable(string $templateName): string $defaultStrategy An escaping strategy */ public function setDefaultStrategy($defaultStrategy): void { @@ -93,324 +129,90 @@ public function getDefaultStrategy(string $name) /** * Defines a new escaper to be used via the escape filter. * - * @param string $strategy The strategy name that should be used as a strategy in the escape call - * @param callable $callable A valid PHP callable + * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param callable(Environment, string, string): string $callable A valid PHP callable + * + * @return void + * + * @deprecated since Twig 3.10 */ public function setEscaper($strategy, callable $callable) { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__); + + if (!isset($this->environment)) { + throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); + } + $this->escapers[$strategy] = $callable; + $callable = function ($string, $charset) use ($callable) { + return $callable($this->environment, $string, $charset); + }; + + $this->escaper->setEscaper($strategy, $callable); } /** * Gets all defined escapers. * - * @return callable[] An array of escapers + * @return array An array of escapers + * + * @deprecated since Twig 3.10 */ public function getEscapers() { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::getEscaper()" method instead.', __METHOD__); + return $this->escapers; } + /** + * @return void + * + * @deprecated since Twig 3.10 + */ public function setSafeClasses(array $safeClasses = []) { - $this->safeClasses = []; - $this->safeLookup = []; - foreach ($safeClasses as $class => $strategies) { - $this->addSafeClass($class, $strategies); - } - } + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__); - public function addSafeClass(string $class, array $strategies) - { - $class = ltrim($class, '\\'); - if (!isset($this->safeClasses[$class])) { - $this->safeClasses[$class] = []; + if (!isset($this->escaper)) { + throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); } - $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies); - foreach ($strategies as $strategy) { - $this->safeLookup[$strategy][$class] = true; - } + $this->escaper->setSafeClasses($safeClasses); } -} -} - -namespace { -use Twig\Environment; -use Twig\Error\RuntimeError; -use Twig\Extension\EscaperExtension; -use Twig\Markup; -use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Node; - -/** - * Marks a variable as being safe. - * - * @param string $string A PHP variable - */ -function twig_raw_filter($string) -{ - return $string; -} -/** - * Escapes a string. - * - * @param mixed $string The value to be escaped - * @param string $strategy The escaping strategy - * @param string $charset The charset - * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) - * - * @return string - */ -function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) -{ - if ($autoescape && $string instanceof Markup) { - return $string; - } - - if (!\is_string($string)) { - if (\is_object($string) && method_exists($string, '__toString')) { - if ($autoescape) { - $c = \get_class($string); - $ext = $env->getExtension(EscaperExtension::class); - if (!isset($ext->safeClasses[$c])) { - $ext->safeClasses[$c] = []; - foreach (class_parents($string) + class_implements($string) as $class) { - if (isset($ext->safeClasses[$class])) { - $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class])); - foreach ($ext->safeClasses[$class] as $s) { - $ext->safeLookup[$s][$c] = true; - } - } - } - } - if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) { - return (string) $string; - } - } + /** + * @return void + * + * @deprecated since Twig 3.10 + */ + public function addSafeClass(string $class, array $strategies) + { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__); - $string = (string) $string; - } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { - return $string; + if (!isset($this->escaper)) { + throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); } - } - if ('' === $string) { - return ''; + $this->escaper->addSafeClass($class, $strategies); } - if (null === $charset) { - $charset = $env->getCharset(); - } - - switch ($strategy) { - case 'html': - // see https://www.php.net/htmlspecialchars - - // Using a static variable to avoid initializing the array - // each time the function is called. Moving the declaration on the - // top of the function slow downs other escaping strategies. - static $htmlspecialcharsCharsets = [ - 'ISO-8859-1' => true, 'ISO8859-1' => true, - 'ISO-8859-15' => true, 'ISO8859-15' => true, - 'utf-8' => true, 'UTF-8' => true, - 'CP866' => true, 'IBM866' => true, '866' => true, - 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, - '1251' => true, - 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, - 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, - 'BIG5' => true, '950' => true, - 'GB2312' => true, '936' => true, - 'BIG5-HKSCS' => true, - 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, - 'EUC-JP' => true, 'EUCJP' => true, - 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, - ]; - - if (isset($htmlspecialcharsCharsets[$charset])) { - return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); - } - - if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { - // cache the lowercase variant for future iterations - $htmlspecialcharsCharsets[$charset] = true; - - return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); - } - - $string = twig_convert_encoding($string, 'UTF-8', $charset); - $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); - - return iconv('UTF-8', $charset, $string); - - case 'js': - // escape all non-alphanumeric characters - // into their \x or \uHHHH representations - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } - - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } - - $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) { - $char = $matches[0]; - - /* - * A few characters have short escape sequences in JSON and JavaScript. - * Escape sequences supported only by JavaScript, not JSON, are omitted. - * \" is also supported but omitted, because the resulting string is not HTML safe. - */ - static $shortMap = [ - '\\' => '\\\\', - '/' => '\\/', - "\x08" => '\b', - "\x0C" => '\f', - "\x0A" => '\n', - "\x0D" => '\r', - "\x09" => '\t', - ]; - - if (isset($shortMap[$char])) { - return $shortMap[$char]; - } - - $codepoint = mb_ord($char, 'UTF-8'); - if (0x10000 > $codepoint) { - return sprintf('\u%04X', $codepoint); - } - - // Split characters outside the BMP into surrogate pairs - // https://tools.ietf.org/html/rfc2781.html#section-2.1 - $u = $codepoint - 0x10000; - $high = 0xD800 | ($u >> 10); - $low = 0xDC00 | ($u & 0x3FF); - - return sprintf('\u%04X\u%04X', $high, $low); - }, $string); - - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } - - return $string; - - case 'css': - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } - - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } - - $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) { - $char = $matches[0]; - - return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); - }, $string); - - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } - - return $string; - - case 'html_attr': - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } - - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } - - $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) { - /** - * This function is adapted from code coming from Zend Framework. - * - * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) - * @license https://framework.zend.com/license/new-bsd New BSD License - */ - $chr = $matches[0]; - $ord = \ord($chr); - - /* - * The following replaces characters undefined in HTML with the - * hex entity for the Unicode replacement character. - */ - if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) { - return '�'; - } - - /* - * Check if the current character to escape has a name entity we should - * replace it with while grabbing the hex value of the character. - */ - if (1 === \strlen($chr)) { - /* - * While HTML supports far more named entities, the lowest common denominator - * has become HTML5's XML Serialisation which is restricted to the those named - * entities that XML supports. Using HTML entities would result in this error: - * XML Parsing Error: undefined entity - */ - static $entityMap = [ - 34 => '"', /* quotation mark */ - 38 => '&', /* ampersand */ - 60 => '<', /* less-than sign */ - 62 => '>', /* greater-than sign */ - ]; - - if (isset($entityMap[$ord])) { - return $entityMap[$ord]; - } - - return sprintf('&#x%02X;', $ord); - } - - /* - * Per OWASP recommendations, we'll use hex entities for any other - * characters where a named entity does not exist. - */ - return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); - }, $string); - - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } - - return $string; - - case 'url': - return rawurlencode($string); - - default: - $escapers = $env->getExtension(EscaperExtension::class)->getEscapers(); - if (\array_key_exists($strategy, $escapers)) { - return $escapers[$strategy]($env, $string, $charset); + /** + * @internal + * + * @return array + */ + public static function escapeFilterIsSafe(Node $filterArgs) + { + foreach ($filterArgs as $arg) { + if ($arg instanceof ConstantExpression) { + return [$arg->getAttribute('value')]; } - $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); - - throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies)); - } -} - -/** - * @internal - */ -function twig_escape_filter_is_safe(Node $filterArgs) -{ - foreach ($filterArgs as $arg) { - if ($arg instanceof ConstantExpression) { - return [$arg->getAttribute('value')]; + return []; } - return []; + return ['html']; } - - return ['html']; -} } diff --git a/app/vendor/twig/twig/src/Extension/ExtensionInterface.php b/app/vendor/twig/twig/src/Extension/ExtensionInterface.php index ab9c2c37c..d51cd3ee2 100644 --- a/app/vendor/twig/twig/src/Extension/ExtensionInterface.php +++ b/app/vendor/twig/twig/src/Extension/ExtensionInterface.php @@ -15,6 +15,7 @@ use Twig\Node\Expression\Binary\AbstractBinary; use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\OperatorPrecedenceChange; use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; use Twig\TwigFunction; @@ -68,8 +69,8 @@ public function getFunctions(); * @return array First array of unary operators, second array of binary operators * * @psalm-return array{ - * array}>, - * array, associativity: ExpressionParser::OPERATOR_*}> + * array}>, + * array, associativity: ExpressionParser::OPERATOR_*}> * } */ public function getOperators(); diff --git a/app/vendor/twig/twig/src/Extension/GlobalsInterface.php b/app/vendor/twig/twig/src/Extension/GlobalsInterface.php index 6f1dfe8a7..d52cd107e 100644 --- a/app/vendor/twig/twig/src/Extension/GlobalsInterface.php +++ b/app/vendor/twig/twig/src/Extension/GlobalsInterface.php @@ -12,10 +12,7 @@ namespace Twig\Extension; /** - * Enables usage of the deprecated Twig\Extension\AbstractExtension::getGlobals() method. - * - * Explicitly implement this interface if you really need to implement the - * deprecated getGlobals() method in your extensions. + * Allows Twig extensions to add globals to the context. * * @author Fabien Potencier */ diff --git a/app/vendor/twig/twig/src/Extension/LastModifiedExtensionInterface.php b/app/vendor/twig/twig/src/Extension/LastModifiedExtensionInterface.php new file mode 100644 index 000000000..4bab0c07c --- /dev/null +++ b/app/vendor/twig/twig/src/Extension/LastModifiedExtensionInterface.php @@ -0,0 +1,23 @@ +optimizers = $optimizers; + public function __construct( + private int $optimizers = -1, + ) { } public function getNodeVisitors(): array diff --git a/app/vendor/twig/twig/src/Extension/SandboxExtension.php b/app/vendor/twig/twig/src/Extension/SandboxExtension.php index c861159b6..a9681c8d6 100644 --- a/app/vendor/twig/twig/src/Extension/SandboxExtension.php +++ b/app/vendor/twig/twig/src/Extension/SandboxExtension.php @@ -15,6 +15,7 @@ use Twig\Sandbox\SecurityNotAllowedMethodError; use Twig\Sandbox\SecurityNotAllowedPropertyError; use Twig\Sandbox\SecurityPolicyInterface; +use Twig\Sandbox\SourcePolicyInterface; use Twig\Source; use Twig\TokenParser\SandboxTokenParser; @@ -23,11 +24,13 @@ final class SandboxExtension extends AbstractExtension private $sandboxedGlobally; private $sandboxed; private $policy; + private $sourcePolicy; - public function __construct(SecurityPolicyInterface $policy, $sandboxed = false) + public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null) { $this->policy = $policy; $this->sandboxedGlobally = $sandboxed; + $this->sourcePolicy = $sourcePolicy; } public function getTokenParsers(): array @@ -50,9 +53,9 @@ public function disableSandbox(): void $this->sandboxed = false; } - public function isSandboxed(): bool + public function isSandboxed(?Source $source = null): bool { - return $this->sandboxedGlobally || $this->sandboxed; + return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source); } public function isSandboxedGlobally(): bool @@ -60,7 +63,16 @@ public function isSandboxedGlobally(): bool return $this->sandboxedGlobally; } - public function setSecurityPolicy(SecurityPolicyInterface $policy) + private function isSourceSandboxed(?Source $source): bool + { + if (null === $source || null === $this->sourcePolicy) { + return false; + } + + return $this->sourcePolicy->enableSandbox($source); + } + + public function setSecurityPolicy(SecurityPolicyInterface $policy): void { $this->policy = $policy; } @@ -70,16 +82,16 @@ public function getSecurityPolicy(): SecurityPolicyInterface return $this->policy; } - public function checkSecurity($tags, $filters, $functions): void + public function checkSecurity($tags, $filters, $functions, ?Source $source = null): void { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { $this->policy->checkSecurity($tags, $filters, $functions); } } - public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null): void + public function checkMethodAllowed($obj, $method, int $lineno = -1, ?Source $source = null): void { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { try { $this->policy->checkMethodAllowed($obj, $method); } catch (SecurityNotAllowedMethodError $e) { @@ -91,9 +103,9 @@ public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $sour } } - public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null): void + public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source $source = null): void { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { try { $this->policy->checkPropertyAllowed($obj, $property); } catch (SecurityNotAllowedPropertyError $e) { @@ -105,9 +117,22 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $ } } - public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null) + /** + * @param mixed $obj + * + * @return mixed + * + * @throws SecurityNotAllowedMethodError + */ + public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) { - if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) { + if (\is_array($obj)) { + $this->ensureToStringAllowedForArray($obj, $lineno, $source); + + return $obj; + } + + if ($obj instanceof \Stringable && $this->isSandboxed($source)) { try { $this->policy->checkMethodAllowed($obj, '__toString'); } catch (SecurityNotAllowedMethodError $e) { @@ -120,4 +145,28 @@ public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = n 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 ($r = \ReflectionReference::fromArrayElement($obj, $k)) { + if (isset($stack[$r->getId()])) { + continue; + } + + $stack[$r->getId()] = true; + } + + $this->ensureToStringAllowedForArray($v, $lineno, $source, $stack); + } + } } diff --git a/app/vendor/twig/twig/src/Extension/StagingExtension.php b/app/vendor/twig/twig/src/Extension/StagingExtension.php index 0ea47f90c..59db2ca7d 100644 --- a/app/vendor/twig/twig/src/Extension/StagingExtension.php +++ b/app/vendor/twig/twig/src/Extension/StagingExtension.php @@ -35,7 +35,7 @@ final class StagingExtension extends AbstractExtension public function addFunction(TwigFunction $function): void { if (isset($this->functions[$function->getName()])) { - throw new \LogicException(sprintf('Function "%s" is already registered.', $function->getName())); + throw new \LogicException(\sprintf('Function "%s" is already registered.', $function->getName())); } $this->functions[$function->getName()] = $function; @@ -49,7 +49,7 @@ public function getFunctions(): array public function addFilter(TwigFilter $filter): void { if (isset($this->filters[$filter->getName()])) { - throw new \LogicException(sprintf('Filter "%s" is already registered.', $filter->getName())); + throw new \LogicException(\sprintf('Filter "%s" is already registered.', $filter->getName())); } $this->filters[$filter->getName()] = $filter; @@ -73,7 +73,7 @@ public function getNodeVisitors(): array public function addTokenParser(TokenParserInterface $parser): void { if (isset($this->tokenParsers[$parser->getTag()])) { - throw new \LogicException(sprintf('Tag "%s" is already registered.', $parser->getTag())); + throw new \LogicException(\sprintf('Tag "%s" is already registered.', $parser->getTag())); } $this->tokenParsers[$parser->getTag()] = $parser; @@ -87,7 +87,7 @@ public function getTokenParsers(): array public function addTest(TwigTest $test): void { if (isset($this->tests[$test->getName()])) { - throw new \LogicException(sprintf('Test "%s" is already registered.', $test->getName())); + throw new \LogicException(\sprintf('Test "%s" is already registered.', $test->getName())); } $this->tests[$test->getName()] = $test; diff --git a/app/vendor/twig/twig/src/Extension/StringLoaderExtension.php b/app/vendor/twig/twig/src/Extension/StringLoaderExtension.php index 7b4514710..698d181f1 100644 --- a/app/vendor/twig/twig/src/Extension/StringLoaderExtension.php +++ b/app/vendor/twig/twig/src/Extension/StringLoaderExtension.php @@ -9,7 +9,10 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\TemplateWrapper; use Twig\TwigFunction; final class StringLoaderExtension extends AbstractExtension @@ -17,26 +20,21 @@ final class StringLoaderExtension extends AbstractExtension public function getFunctions(): array { return [ - new TwigFunction('template_from_string', 'twig_template_from_string', ['needs_environment' => true]), + new TwigFunction('template_from_string', [self::class, 'templateFromString'], ['needs_environment' => true]), ]; } -} -} - -namespace { -use Twig\Environment; -use Twig\TemplateWrapper; -/** - * Loads a template from a string. - * - * {{ include(template_from_string("Hello {{ name }}")) }} - * - * @param string $template A template as a string or object implementing __toString() - * @param string $name An optional name of the template to be used in error messages - */ -function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper -{ - return $env->createTemplate((string) $template, $name); -} + /** + * Loads a template from a string. + * + * {{ include(template_from_string("Hello {{ name }}")) }} + * + * @param string|null $name An optional name of the template to be used in error messages + * + * @internal + */ + public static function templateFromString(Environment $env, string|\Stringable $template, ?string $name = null): TemplateWrapper + { + return $env->createTemplate((string) $template, $name); + } } diff --git a/app/vendor/twig/twig/src/Extension/YieldNotReadyExtension.php b/app/vendor/twig/twig/src/Extension/YieldNotReadyExtension.php new file mode 100644 index 000000000..49dfb8085 --- /dev/null +++ b/app/vendor/twig/twig/src/Extension/YieldNotReadyExtension.php @@ -0,0 +1,30 @@ +useYield)]; + } +} diff --git a/app/vendor/twig/twig/src/ExtensionSet.php b/app/vendor/twig/twig/src/ExtensionSet.php index d32200ceb..b069232b4 100644 --- a/app/vendor/twig/twig/src/ExtensionSet.php +++ b/app/vendor/twig/twig/src/ExtensionSet.php @@ -14,6 +14,7 @@ use Twig\Error\RuntimeError; use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; +use Twig\Extension\LastModifiedExtensionInterface; use Twig\Extension\StagingExtension; use Twig\Node\Expression\Binary\AbstractBinary; use Twig\Node\Expression\Unary\AbstractUnary; @@ -35,18 +36,27 @@ final class ExtensionSet private $visitors; /** @var array */ private $filters; + /** @var array */ + private $dynamicFilters; /** @var array */ private $tests; + /** @var array */ + private $dynamicTests; /** @var array */ private $functions; - /** @var array}> */ + /** @var array */ + private $dynamicFunctions; + /** @var array}> */ private $unaryOperators; - /** @var array, associativity: ExpressionParser::OPERATOR_*}> */ + /** @var array, associativity: ExpressionParser::OPERATOR_*}> */ private $binaryOperators; - /** @var array */ + /** @var array|null */ private $globals; + /** @var array */ private $functionCallbacks = []; + /** @var array */ private $filterCallbacks = []; + /** @var array */ private $parserCallbacks = []; private $lastModified = 0; @@ -55,6 +65,9 @@ public function __construct() $this->staging = new StagingExtension(); } + /** + * @return void + */ public function initRuntime() { $this->runtimeInitialized = true; @@ -70,7 +83,7 @@ public function getExtension(string $class): ExtensionInterface $class = ltrim($class, '\\'); if (!isset($this->extensions[$class])) { - throw new RuntimeError(sprintf('The "%s" extension is not enabled.', $class)); + throw new RuntimeError(\sprintf('The "%s" extension is not enabled.', $class)); } return $this->extensions[$class]; @@ -110,14 +123,19 @@ public function getLastModified(): int return $this->lastModified; } + $lastModified = 0; foreach ($this->extensions as $extension) { - $r = new \ReflectionObject($extension); - if (is_file($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModified) { - $this->lastModified = $extensionTime; + if ($extension instanceof LastModifiedExtensionInterface) { + $lastModified = max($extension->getLastModified(), $lastModified); + } else { + $r = new \ReflectionObject($extension); + if (is_file($r->getFileName())) { + $lastModified = max(filemtime($r->getFileName()), $lastModified); + } } } - return $this->lastModified; + return $this->lastModified = $lastModified; } public function addExtension(ExtensionInterface $extension): void @@ -125,11 +143,11 @@ public function addExtension(ExtensionInterface $extension): void $class = \get_class($extension); if ($this->initialized) { - throw new \LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class)); + throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class)); } if (isset($this->extensions[$class])) { - throw new \LogicException(sprintf('Unable to register extension "%s" as it is already registered.', $class)); + throw new \LogicException(\sprintf('Unable to register extension "%s" as it is already registered.', $class)); } $this->extensions[$class] = $extension; @@ -138,7 +156,7 @@ public function addExtension(ExtensionInterface $extension): void public function addFunction(TwigFunction $function): void { if ($this->initialized) { - throw new \LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName())); + throw new \LogicException(\sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName())); } $this->staging->addFunction($function); @@ -166,14 +184,11 @@ public function getFunction(string $name): ?TwigFunction return $this->functions[$name]; } - foreach ($this->functions as $pattern => $function) { - $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); - - if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) { + foreach ($this->dynamicFunctions as $pattern => $function) { + if (preg_match($pattern, $name, $matches)) { array_shift($matches); - $function->setArguments($matches); - return $function; + return $function->withDynamicArguments($name, $function->getName(), $matches); } } @@ -186,6 +201,9 @@ public function getFunction(string $name): ?TwigFunction return null; } + /** + * @param callable(string): (TwigFunction|false) $callable + */ public function registerUndefinedFunctionCallback(callable $callable): void { $this->functionCallbacks[] = $callable; @@ -194,7 +212,7 @@ public function registerUndefinedFunctionCallback(callable $callable): void public function addFilter(TwigFilter $filter): void { if ($this->initialized) { - throw new \LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName())); + throw new \LogicException(\sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName())); } $this->staging->addFilter($filter); @@ -222,14 +240,11 @@ public function getFilter(string $name): ?TwigFilter return $this->filters[$name]; } - foreach ($this->filters as $pattern => $filter) { - $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); - - if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) { + foreach ($this->dynamicFilters as $pattern => $filter) { + if (preg_match($pattern, $name, $matches)) { array_shift($matches); - $filter->setArguments($matches); - return $filter; + return $filter->withDynamicArguments($name, $filter->getName(), $matches); } } @@ -242,6 +257,9 @@ public function getFilter(string $name): ?TwigFilter return null; } + /** + * @param callable(string): (TwigFilter|false) $callable + */ public function registerUndefinedFilterCallback(callable $callable): void { $this->filterCallbacks[] = $callable; @@ -308,6 +326,9 @@ public function getTokenParser(string $name): ?TokenParserInterface return null; } + /** + * @param callable(string): (TokenParserInterface|false) $callable + */ public function registerUndefinedTokenParserCallback(callable $callable): void { $this->parserCallbacks[] = $callable; @@ -328,12 +349,7 @@ public function getGlobals(): array continue; } - $extGlobals = $extension->getGlobals(); - if (!\is_array($extGlobals)) { - throw new \UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension))); - } - - $globals = array_merge($globals, $extGlobals); + $globals = array_merge($globals, $extension->getGlobals()); } if ($this->initialized) { @@ -343,10 +359,15 @@ public function getGlobals(): array return $globals; } + public function resetGlobals(): void + { + $this->globals = null; + } + public function addTest(TwigTest $test): void { if ($this->initialized) { - throw new \LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName())); + throw new \LogicException(\sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName())); } $this->staging->addTest($test); @@ -374,16 +395,11 @@ public function getTest(string $name): ?TwigTest return $this->tests[$name]; } - foreach ($this->tests as $pattern => $test) { - $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); - - if ($count) { - if (preg_match('#^'.$pattern.'$#', $name, $matches)) { - array_shift($matches); - $test->setArguments($matches); + foreach ($this->dynamicTests as $pattern => $test) { + if (preg_match($pattern, $name, $matches)) { + array_shift($matches); - return $test; - } + return $test->withDynamicArguments($name, $test->getName(), $matches); } } @@ -391,7 +407,7 @@ public function getTest(string $name): ?TwigTest } /** - * @return array}> + * @return array}> */ public function getUnaryOperators(): array { @@ -403,7 +419,7 @@ public function getUnaryOperators(): array } /** - * @return array, associativity: ExpressionParser::OPERATOR_*}> + * @return array, associativity: ExpressionParser::OPERATOR_*}> */ public function getBinaryOperators(): array { @@ -420,6 +436,9 @@ private function initExtensions(): void $this->filters = []; $this->functions = []; $this->tests = []; + $this->dynamicFilters = []; + $this->dynamicFunctions = []; + $this->dynamicTests = []; $this->visitors = []; $this->unaryOperators = []; $this->binaryOperators = []; @@ -436,17 +455,26 @@ private function initExtension(ExtensionInterface $extension): void { // filters foreach ($extension->getFilters() as $filter) { - $this->filters[$filter->getName()] = $filter; + $this->filters[$name = $filter->getName()] = $filter; + if (str_contains($name, '*')) { + $this->dynamicFilters['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $filter; + } } // functions foreach ($extension->getFunctions() as $function) { - $this->functions[$function->getName()] = $function; + $this->functions[$name = $function->getName()] = $function; + if (str_contains($name, '*')) { + $this->dynamicFunctions['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $function; + } } // tests foreach ($extension->getTests() as $test) { - $this->tests[$test->getName()] = $test; + $this->tests[$name = $test->getName()] = $test; + if (str_contains($name, '*')) { + $this->dynamicTests['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $test; + } } // token parsers @@ -466,11 +494,11 @@ private function initExtension(ExtensionInterface $extension): void // operators if ($operators = $extension->getOperators()) { if (!\is_array($operators)) { - throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators))); + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators))); } if (2 !== \count($operators)) { - throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); } $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]); diff --git a/app/vendor/twig/twig/src/FileExtensionEscapingStrategy.php b/app/vendor/twig/twig/src/FileExtensionEscapingStrategy.php index 812071bf9..5308158d3 100644 --- a/app/vendor/twig/twig/src/FileExtensionEscapingStrategy.php +++ b/app/vendor/twig/twig/src/FileExtensionEscapingStrategy.php @@ -45,6 +45,7 @@ public static function guess(string $name) switch ($extension) { case 'js': + case 'json': return 'js'; case 'css': diff --git a/app/vendor/twig/twig/src/Lexer.php b/app/vendor/twig/twig/src/Lexer.php index b23080f58..929673c60 100644 --- a/app/vendor/twig/twig/src/Lexer.php +++ b/app/vendor/twig/twig/src/Lexer.php @@ -44,12 +44,29 @@ class Lexer public const STATE_INTERPOLATION = 4; public const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A'; - public const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?([Ee][\+\-][0-9]+)?/A'; public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'; + + public const REGEX_NUMBER = '/(?(DEFINE) + (?[0-9]+(_[0-9]+)*) # Integers (with underscores) 123_456 + (?\.(?&LNUM)) # Fractional part .456 + (?[eE][+-]?(?&LNUM)) # Exponent part E+10 + (?(?&LNUM)(?:(?&FRAC))?) # Decimal number 123_456.456 + )(?:(?&DNUM)(?:(?&EXPONENT))?) # 123_456.456E+10 + /Ax'; + public const REGEX_DQ_STRING_DELIM = '/"/A'; public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; + public const REGEX_INLINE_COMMENT = '/#[^\n]*/A'; public const PUNCTUATION = '()[]{}?:.,|'; + private const SPECIAL_CHARS = [ + 'f' => "\f", + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'v' => "\v", + ]; + public function __construct(Environment $env, array $options = []) { $this->env = $env; @@ -65,14 +82,12 @@ public function __construct(Environment $env, array $options = []) ], $options); } - private function initialize() + private function initialize(): void { if ($this->isInitialized) { return; } - $this->isInitialized = true; - // when PHP 7.3 is the min version, we will be able to remove the '#' part in preg_quote as it's part of the default $this->regexes = [ // }} @@ -160,6 +175,8 @@ private function initialize() 'interpolation_start' => '{'.preg_quote($this->options['interpolation'][0], '#').'\s*}A', 'interpolation_end' => '{\s*'.preg_quote($this->options['interpolation'][1], '#').'}A', ]; + + $this->isInitialized = true; } public function tokenize(Source $source): TokenStream @@ -207,11 +224,11 @@ public function tokenize(Source $source): TokenStream } } - $this->pushToken(/* Token::EOF_TYPE */ -1); + $this->pushToken(Token::EOF_TYPE); - if (!empty($this->brackets)) { - list($expect, $lineno) = array_pop($this->brackets); - throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + if ($this->brackets) { + [$expect, $lineno] = array_pop($this->brackets); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } return new TokenStream($this->tokens, $this->source); @@ -221,7 +238,7 @@ private function lexData(): void { // if no matches are left we return the rest of the template as simple text token if ($this->position == \count($this->positions[0]) - 1) { - $this->pushToken(/* Token::TEXT_TYPE */ 0, substr($this->code, $this->cursor)); + $this->pushToken(Token::TEXT_TYPE, substr($this->code, $this->cursor)); $this->cursor = $this->end; return; @@ -250,7 +267,7 @@ private function lexData(): void $text = rtrim($text, " \t\0\x0B"); } } - $this->pushToken(/* Token::TEXT_TYPE */ 0, $text); + $this->pushToken(Token::TEXT_TYPE, $text); $this->moveCursor($textContent.$position[0]); switch ($this->positions[1][$this->position][0]) { @@ -268,14 +285,14 @@ private function lexData(): void $this->moveCursor($match[0]); $this->lineno = (int) $match[1]; } else { - $this->pushToken(/* Token::BLOCK_START_TYPE */ 1); + $this->pushToken(Token::BLOCK_START_TYPE); $this->pushState(self::STATE_BLOCK); $this->currentVarBlockLine = $this->lineno; } break; case $this->options['tag_variable'][0]: - $this->pushToken(/* Token::VAR_START_TYPE */ 2); + $this->pushToken(Token::VAR_START_TYPE); $this->pushState(self::STATE_VAR); $this->currentVarBlockLine = $this->lineno; break; @@ -284,8 +301,8 @@ private function lexData(): void private function lexBlock(): void { - if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::BLOCK_END_TYPE */ 3); + if (!$this->brackets && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) { + $this->pushToken(Token::BLOCK_END_TYPE); $this->moveCursor($match[0]); $this->popState(); } else { @@ -295,8 +312,8 @@ private function lexBlock(): void private function lexVar(): void { - if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::VAR_END_TYPE */ 4); + if (!$this->brackets && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) { + $this->pushToken(Token::VAR_END_TYPE); $this->moveCursor($match[0]); $this->popState(); } else { @@ -311,7 +328,7 @@ private function lexExpression(): void $this->moveCursor($match[0]); if ($this->cursor >= $this->end) { - throw new SyntaxError(sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source); + throw new SyntaxError(\sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source); } } @@ -321,27 +338,23 @@ private function lexExpression(): void $this->moveCursor('...'); } // arrow function - elseif ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) { + elseif ('=' === $this->code[$this->cursor] && ($this->cursor + 1 < $this->end) && '>' === $this->code[$this->cursor + 1]) { $this->pushToken(Token::ARROW_TYPE, '=>'); $this->moveCursor('=>'); } // operators elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::OPERATOR_TYPE */ 8, preg_replace('/\s+/', ' ', $match[0])); + $this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0])); $this->moveCursor($match[0]); } // names elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::NAME_TYPE */ 5, $match[0]); + $this->pushToken(Token::NAME_TYPE, $match[0]); $this->moveCursor($match[0]); } // numbers elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, 0, $this->cursor)) { - $number = (float) $match[0]; // floats - if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) { - $number = (int) $match[0]; // integers lower than the maximum - } - $this->pushToken(/* Token::NUMBER_TYPE */ 6, $number); + $this->pushToken(Token::NUMBER_TYPE, 0 + str_replace('_', '', $match[0])); $this->moveCursor($match[0]); } // punctuation @@ -352,22 +365,22 @@ private function lexExpression(): void } // closing bracket elseif (str_contains(')]}', $this->code[$this->cursor])) { - if (empty($this->brackets)) { - throw new SyntaxError(sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); + if (!$this->brackets) { + throw new SyntaxError(\sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } - list($expect, $lineno) = array_pop($this->brackets); + [$expect, $lineno] = array_pop($this->brackets); if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) { - throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } } - $this->pushToken(/* Token::PUNCTUATION_TYPE */ 9, $this->code[$this->cursor]); + $this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]); ++$this->cursor; } // strings elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes(substr($match[0], 1, -1))); + $this->pushToken(Token::STRING_TYPE, $this->stripcslashes(substr($match[0], 1, -1), substr($match[0], 0, 1))); $this->moveCursor($match[0]); } // opening double quoted string @@ -376,10 +389,71 @@ private function lexExpression(): void $this->pushState(self::STATE_STRING); $this->moveCursor($match[0]); } + // inline comment + elseif (preg_match(self::REGEX_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) { + $this->moveCursor($match[0]); + } // unlexable else { - throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); + throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); + } + } + + private function stripcslashes(string $str, string $quoteType): string + { + $result = ''; + $length = \strlen($str); + + $i = 0; + while ($i < $length) { + if (false === $pos = strpos($str, '\\', $i)) { + $result .= substr($str, $i); + break; + } + + $result .= substr($str, $i, $pos - $i); + $i = $pos + 1; + + if ($i >= $length) { + $result .= '\\'; + break; + } + + $nextChar = $str[$i]; + + if (isset(self::SPECIAL_CHARS[$nextChar])) { + $result .= self::SPECIAL_CHARS[$nextChar]; + } elseif ('\\' === $nextChar) { + $result .= $nextChar; + } elseif ("'" === $nextChar || '"' === $nextChar) { + if ($nextChar !== $quoteType) { + trigger_deprecation('twig/twig', '3.12', 'Character "%s" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position %d in "%s" at line %d.', $nextChar, $i + 1, $this->source->getName(), $this->lineno); + } + $result .= $nextChar; + } elseif ('#' === $nextChar && $i + 1 < $length && '{' === $str[$i + 1]) { + $result .= '#{'; + ++$i; + } elseif ('x' === $nextChar && $i + 1 < $length && ctype_xdigit($str[$i + 1])) { + $hex = $str[++$i]; + if ($i + 1 < $length && ctype_xdigit($str[$i + 1])) { + $hex .= $str[++$i]; + } + $result .= \chr(hexdec($hex)); + } elseif (ctype_digit($nextChar) && $nextChar < '8') { + $octal = $nextChar; + while ($i + 1 < $length && ctype_digit($str[$i + 1]) && $str[$i + 1] < '8' && \strlen($octal) < 3) { + $octal .= $str[++$i]; + } + $result .= \chr(octdec($octal)); + } else { + trigger_deprecation('twig/twig', '3.12', 'Character "%s" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position %d in "%s" at line %d.', $nextChar, $i + 1, $this->source->getName(), $this->lineno); + $result .= $nextChar; + } + + ++$i; } + + return $result; } private function lexRawData(): void @@ -403,7 +477,7 @@ private function lexRawData(): void } } - $this->pushToken(/* Token::TEXT_TYPE */ 0, $text); + $this->pushToken(Token::TEXT_TYPE, $text); } private function lexComment(): void @@ -419,23 +493,23 @@ private function lexString(): void { if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) { $this->brackets[] = [$this->options['interpolation'][0], $this->lineno]; - $this->pushToken(/* Token::INTERPOLATION_START_TYPE */ 10); + $this->pushToken(Token::INTERPOLATION_START_TYPE); $this->moveCursor($match[0]); $this->pushState(self::STATE_INTERPOLATION); } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) { - $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($match[0])); + $this->pushToken(Token::STRING_TYPE, $this->stripcslashes($match[0], '"')); $this->moveCursor($match[0]); } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) { - list($expect, $lineno) = array_pop($this->brackets); + [$expect, $lineno] = array_pop($this->brackets); if ('"' != $this->code[$this->cursor]) { - throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } $this->popState(); ++$this->cursor; } else { // unlexable - throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); + throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } } @@ -444,7 +518,7 @@ private function lexInterpolation(): void $bracket = end($this->brackets); if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) { array_pop($this->brackets); - $this->pushToken(/* Token::INTERPOLATION_END_TYPE */ 11); + $this->pushToken(Token::INTERPOLATION_END_TYPE); $this->moveCursor($match[0]); $this->popState(); } else { @@ -455,7 +529,7 @@ private function lexInterpolation(): void private function pushToken($type, $value = ''): void { // do not push empty text tokens - if (/* Token::TEXT_TYPE */ 0 === $type && '' === $value) { + if (Token::TEXT_TYPE === $type && '' === $value) { return; } diff --git a/app/vendor/twig/twig/src/Loader/ArrayLoader.php b/app/vendor/twig/twig/src/Loader/ArrayLoader.php index 5d726c35a..2bb54b7a8 100644 --- a/app/vendor/twig/twig/src/Loader/ArrayLoader.php +++ b/app/vendor/twig/twig/src/Loader/ArrayLoader.php @@ -28,14 +28,12 @@ */ final class ArrayLoader implements LoaderInterface { - private $templates = []; - /** * @param array $templates An array of templates (keys are the names, and values are the source code) */ - public function __construct(array $templates = []) - { - $this->templates = $templates; + public function __construct( + private array $templates = [], + ) { } public function setTemplate(string $name, string $template): void @@ -46,7 +44,7 @@ public function setTemplate(string $name, string $template): void public function getSourceContext(string $name): Source { if (!isset($this->templates[$name])) { - throw new LoaderError(sprintf('Template "%s" is not defined.', $name)); + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); } return new Source($this->templates[$name], $name); @@ -60,7 +58,7 @@ public function exists(string $name): bool public function getCacheKey(string $name): string { if (!isset($this->templates[$name])) { - throw new LoaderError(sprintf('Template "%s" is not defined.', $name)); + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); } return $name.':'.$this->templates[$name]; @@ -69,7 +67,7 @@ public function getCacheKey(string $name): string public function isFresh(string $name, int $time): bool { if (!isset($this->templates[$name])) { - throw new LoaderError(sprintf('Template "%s" is not defined.', $name)); + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); } return true; diff --git a/app/vendor/twig/twig/src/Loader/ChainLoader.php b/app/vendor/twig/twig/src/Loader/ChainLoader.php index fbf4f3a06..6e4f9511c 100644 --- a/app/vendor/twig/twig/src/Loader/ChainLoader.php +++ b/app/vendor/twig/twig/src/Loader/ChainLoader.php @@ -21,22 +21,28 @@ */ final class ChainLoader implements LoaderInterface { + /** + * @var array + */ private $hasSourceCache = []; - private $loaders = []; /** - * @param LoaderInterface[] $loaders + * @param iterable $loaders */ - public function __construct(array $loaders = []) - { - foreach ($loaders as $loader) { - $this->addLoader($loader); - } + public function __construct( + private iterable $loaders = [], + ) { } public function addLoader(LoaderInterface $loader): void { - $this->loaders[] = $loader; + $current = $this->loaders; + + $this->loaders = (static function () use ($current, $loader): \Generator { + yield from $current; + yield $loader; + })(); + $this->hasSourceCache = []; } @@ -45,13 +51,18 @@ public function addLoader(LoaderInterface $loader): void */ public function getLoaders(): array { + if (!\is_array($this->loaders)) { + $this->loaders = iterator_to_array($this->loaders, false); + } + return $this->loaders; } public function getSourceContext(string $name): Source { $exceptions = []; - foreach ($this->loaders as $loader) { + + foreach ($this->getLoaders() as $loader) { if (!$loader->exists($name)) { continue; } @@ -63,7 +74,7 @@ public function getSourceContext(string $name): Source } } - throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); } public function exists(string $name): bool @@ -72,7 +83,7 @@ public function exists(string $name): bool return $this->hasSourceCache[$name]; } - foreach ($this->loaders as $loader) { + foreach ($this->getLoaders() as $loader) { if ($loader->exists($name)) { return $this->hasSourceCache[$name] = true; } @@ -84,7 +95,8 @@ public function exists(string $name): bool public function getCacheKey(string $name): string { $exceptions = []; - foreach ($this->loaders as $loader) { + + foreach ($this->getLoaders() as $loader) { if (!$loader->exists($name)) { continue; } @@ -96,13 +108,14 @@ public function getCacheKey(string $name): string } } - throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); } public function isFresh(string $name, int $time): bool { $exceptions = []; - foreach ($this->loaders as $loader) { + + foreach ($this->getLoaders() as $loader) { if (!$loader->exists($name)) { continue; } @@ -114,6 +127,6 @@ public function isFresh(string $name, int $time): bool } } - throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); } } diff --git a/app/vendor/twig/twig/src/Loader/FilesystemLoader.php b/app/vendor/twig/twig/src/Loader/FilesystemLoader.php index 1073a406a..49f2b8915 100644 --- a/app/vendor/twig/twig/src/Loader/FilesystemLoader.php +++ b/app/vendor/twig/twig/src/Loader/FilesystemLoader.php @@ -24,6 +24,9 @@ class FilesystemLoader implements LoaderInterface /** Identifier of the main namespace. */ public const MAIN_NAMESPACE = '__main__'; + /** + * @var array> + */ protected $paths = []; protected $cache = []; protected $errorCache = []; @@ -31,10 +34,10 @@ class FilesystemLoader implements LoaderInterface private $rootPath; /** - * @param string|array $paths A path or an array of paths where to look for templates - * @param string|null $rootPath The root path common to all relative paths (null for getcwd()) + * @param string|string[] $paths A path or an array of paths where to look for templates + * @param string|null $rootPath The root path common to all relative paths (null for getcwd()) */ - public function __construct($paths = [], string $rootPath = null) + public function __construct($paths = [], ?string $rootPath = null) { $this->rootPath = ($rootPath ?? getcwd()).\DIRECTORY_SEPARATOR; if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) { @@ -48,6 +51,8 @@ public function __construct($paths = [], string $rootPath = null) /** * Returns the paths to the templates. + * + * @return list */ public function getPaths(string $namespace = self::MAIN_NAMESPACE): array { @@ -58,6 +63,8 @@ public function getPaths(string $namespace = self::MAIN_NAMESPACE): array * Returns the path namespaces. * * The main namespace is always defined. + * + * @return list */ public function getNamespaces(): array { @@ -65,7 +72,7 @@ public function getNamespaces(): array } /** - * @param string|array $paths A path or an array of paths where to look for templates + * @param string|string[] $paths A path or an array of paths where to look for templates */ public function setPaths($paths, string $namespace = self::MAIN_NAMESPACE): void { @@ -89,7 +96,7 @@ public function addPath(string $path, string $namespace = self::MAIN_NAMESPACE): $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; if (!is_dir($checkPath)) { - throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); + throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); } $this->paths[$namespace][] = rtrim($path, '/\\'); @@ -105,7 +112,7 @@ public function prependPath(string $path, string $namespace = self::MAIN_NAMESPA $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; if (!is_dir($checkPath)) { - throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); + throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); } $path = rtrim($path, '/\\'); @@ -183,7 +190,7 @@ protected function findTemplate(string $name, bool $throw = true) } try { - list($namespace, $shortname) = $this->parseName($name); + [$namespace, $shortname] = $this->parseName($name); $this->validateName($shortname); } catch (LoaderError $e) { @@ -195,7 +202,7 @@ protected function findTemplate(string $name, bool $throw = true) } if (!isset($this->paths[$namespace])) { - $this->errorCache[$name] = sprintf('There are no registered paths for namespace "%s".', $namespace); + $this->errorCache[$name] = \sprintf('There are no registered paths for namespace "%s".', $namespace); if (!$throw) { return null; @@ -218,7 +225,7 @@ protected function findTemplate(string $name, bool $throw = true) } } - $this->errorCache[$name] = sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace])); + $this->errorCache[$name] = \sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace])); if (!$throw) { return null; @@ -236,7 +243,7 @@ private function parseName(string $name, string $default = self::MAIN_NAMESPACE) { if (isset($name[0]) && '@' == $name[0]) { if (false === $pos = strpos($name, '/')) { - throw new LoaderError(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name)); + throw new LoaderError(\sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name)); } $namespace = substr($name, 1, $pos - 1); @@ -265,7 +272,7 @@ private function validateName(string $name): void } if ($level < 0) { - throw new LoaderError(sprintf('Looks like you try to load a template outside configured directories (%s).', $name)); + throw new LoaderError(\sprintf('Looks like you try to load a template outside configured directories (%s).', $name)); } } } diff --git a/app/vendor/twig/twig/src/Markup.php b/app/vendor/twig/twig/src/Markup.php index 1788acc4f..a933b69d3 100644 --- a/app/vendor/twig/twig/src/Markup.php +++ b/app/vendor/twig/twig/src/Markup.php @@ -16,10 +16,10 @@ * * @author Fabien Potencier */ -class Markup implements \Countable, \JsonSerializable +class Markup implements \Countable, \JsonSerializable, \Stringable { private $content; - private $charset; + private ?string $charset; public function __construct($content, $charset) { @@ -27,11 +27,16 @@ public function __construct($content, $charset) $this->charset = $charset; } - public function __toString() + public function __toString(): string { return $this->content; } + public function getCharset(): string + { + return $this->charset; + } + /** * @return int */ diff --git a/app/vendor/twig/twig/src/Node/AutoEscapeNode.php b/app/vendor/twig/twig/src/Node/AutoEscapeNode.php index cd970411b..ee806396e 100644 --- a/app/vendor/twig/twig/src/Node/AutoEscapeNode.php +++ b/app/vendor/twig/twig/src/Node/AutoEscapeNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -24,11 +25,12 @@ * * @author Fabien Potencier */ +#[YieldReady] class AutoEscapeNode extends Node { - public function __construct($value, Node $body, int $lineno, string $tag = 'autoescape') + public function __construct($value, Node $body, int $lineno) { - parent::__construct(['body' => $body], ['value' => $value], $lineno, $tag); + parent::__construct(['body' => $body], ['value' => $value], $lineno); } public function compile(Compiler $compiler): void diff --git a/app/vendor/twig/twig/src/Node/BlockNode.php b/app/vendor/twig/twig/src/Node/BlockNode.php index 0632ba747..b4f939cf6 100644 --- a/app/vendor/twig/twig/src/Node/BlockNode.php +++ b/app/vendor/twig/twig/src/Node/BlockNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,24 +20,29 @@ * * @author Fabien Potencier */ +#[YieldReady] class BlockNode extends Node { - public function __construct(string $name, Node $body, int $lineno, string $tag = null) + public function __construct(string $name, Node $body, int $lineno) { - parent::__construct(['body' => $body], ['name' => $name], $lineno, $tag); + parent::__construct(['body' => $body], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write(sprintf("public function block_%s(\$context, array \$blocks = [])\n", $this->getAttribute('name')), "{\n") + ->write("/**\n") + ->write(" * @return iterable\n") + ->write(" */\n") + ->write(\sprintf("public function block_%s(array \$context, array \$blocks = []): iterable\n", $this->getAttribute('name')), "{\n") ->indent() ->write("\$macros = \$this->macros;\n") ; $compiler ->subcompile($this->getNode('body')) + ->write("yield from [];\n") ->outdent() ->write("}\n\n") ; diff --git a/app/vendor/twig/twig/src/Node/BlockReferenceNode.php b/app/vendor/twig/twig/src/Node/BlockReferenceNode.php index cc8af5b52..7c313a04c 100644 --- a/app/vendor/twig/twig/src/Node/BlockReferenceNode.php +++ b/app/vendor/twig/twig/src/Node/BlockReferenceNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,18 +20,19 @@ * * @author Fabien Potencier */ +#[YieldReady] class BlockReferenceNode extends Node implements NodeOutputInterface { - public function __construct(string $name, int $lineno, string $tag = null) + public function __construct(string $name, int $lineno) { - parent::__construct([], ['name' => $name], $lineno, $tag); + parent::__construct([], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) + ->write(\sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) ; } } diff --git a/app/vendor/twig/twig/src/Node/BodyNode.php b/app/vendor/twig/twig/src/Node/BodyNode.php index 041cbf685..08115b3bd 100644 --- a/app/vendor/twig/twig/src/Node/BodyNode.php +++ b/app/vendor/twig/twig/src/Node/BodyNode.php @@ -11,11 +11,14 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; + /** * Represents a body node. * * @author Fabien Potencier */ +#[YieldReady] class BodyNode extends Node { } diff --git a/app/vendor/twig/twig/src/Node/CaptureNode.php b/app/vendor/twig/twig/src/Node/CaptureNode.php new file mode 100644 index 000000000..3b7f0b6d8 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/CaptureNode.php @@ -0,0 +1,57 @@ + + */ +#[YieldReady] +class CaptureNode extends Node +{ + public function __construct(Node $body, int $lineno) + { + parent::__construct(['body' => $body], ['raw' => false], $lineno); + } + + public function compile(Compiler $compiler): void + { + $useYield = $compiler->getEnvironment()->useYield(); + + if (!$this->getAttribute('raw')) { + $compiler->raw("('' === \$tmp = "); + } + $compiler + ->raw($useYield ? "implode('', iterator_to_array(" : '\\Twig\\Extension\\CoreExtension::captureOutput(') + ->raw("(function () use (&\$context, \$macros, \$blocks) {\n") + ->indent() + ->subcompile($this->getNode('body')) + ->write("yield from [];\n") + ->outdent() + ->write('})()') + ; + if ($useYield) { + $compiler->raw(', false))'); + } else { + $compiler->raw(')'); + } + if (!$this->getAttribute('raw')) { + $compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset());"); + } else { + $compiler->raw(';'); + } + } +} diff --git a/app/vendor/twig/twig/src/Node/CheckSecurityCallNode.php b/app/vendor/twig/twig/src/Node/CheckSecurityCallNode.php index a78a38d80..bb8783bc3 100644 --- a/app/vendor/twig/twig/src/Node/CheckSecurityCallNode.php +++ b/app/vendor/twig/twig/src/Node/CheckSecurityCallNode.php @@ -11,17 +11,22 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** * @author Fabien Potencier */ +#[YieldReady] class CheckSecurityCallNode extends Node { + /** + * @return void + */ public function compile(Compiler $compiler) { $compiler - ->write("\$this->sandbox = \$this->env->getExtension('\Twig\Extension\SandboxExtension');\n") + ->write("\$this->sandbox = \$this->extensions[SandboxExtension::class];\n") ->write("\$this->checkSecurity();\n") ; } diff --git a/app/vendor/twig/twig/src/Node/CheckSecurityNode.php b/app/vendor/twig/twig/src/Node/CheckSecurityNode.php index 472732796..6e591aad4 100644 --- a/app/vendor/twig/twig/src/Node/CheckSecurityNode.php +++ b/app/vendor/twig/twig/src/Node/CheckSecurityNode.php @@ -11,17 +11,24 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** * @author Fabien Potencier */ +#[YieldReady] class CheckSecurityNode extends Node { private $usedFilters; private $usedTags; private $usedFunctions; + /** + * @param array $usedFilters + * @param array $usedTags + * @param array $usedFunctions + */ public function __construct(array $usedFilters, array $usedTags, array $usedFunctions) { $this->usedFilters = $usedFilters; @@ -33,32 +40,22 @@ public function __construct(array $usedFilters, array $usedTags, array $usedFunc public function compile(Compiler $compiler): void { - $tags = $filters = $functions = []; - foreach (['tags', 'filters', 'functions'] as $type) { - foreach ($this->{'used'.ucfirst($type)} as $name => $node) { - if ($node instanceof Node) { - ${$type}[$name] = $node->getTemplateLine(); - } else { - ${$type}[$node] = null; - } - } - } - $compiler ->write("\n") ->write("public function checkSecurity()\n") ->write("{\n") ->indent() - ->write('static $tags = ')->repr(array_filter($tags))->raw(";\n") - ->write('static $filters = ')->repr(array_filter($filters))->raw(";\n") - ->write('static $functions = ')->repr(array_filter($functions))->raw(";\n\n") + ->write('static $tags = ')->repr(array_filter($this->usedTags))->raw(";\n") + ->write('static $filters = ')->repr(array_filter($this->usedFilters))->raw(";\n") + ->write('static $functions = ')->repr(array_filter($this->usedFunctions))->raw(";\n\n") ->write("try {\n") ->indent() ->write("\$this->sandbox->checkSecurity(\n") ->indent() - ->write(!$tags ? "[],\n" : "['".implode("', '", array_keys($tags))."'],\n") - ->write(!$filters ? "[],\n" : "['".implode("', '", array_keys($filters))."'],\n") - ->write(!$functions ? "[]\n" : "['".implode("', '", array_keys($functions))."']\n") + ->write(!$this->usedTags ? "[],\n" : "['".implode("', '", array_keys($this->usedTags))."'],\n") + ->write(!$this->usedFilters ? "[],\n" : "['".implode("', '", array_keys($this->usedFilters))."'],\n") + ->write(!$this->usedFunctions ? "[],\n" : "['".implode("', '", array_keys($this->usedFunctions))."'],\n") + ->write("\$this->source\n") ->outdent() ->write(");\n") ->outdent() diff --git a/app/vendor/twig/twig/src/Node/CheckToStringNode.php b/app/vendor/twig/twig/src/Node/CheckToStringNode.php index c7a9d6984..937240c1d 100644 --- a/app/vendor/twig/twig/src/Node/CheckToStringNode.php +++ b/app/vendor/twig/twig/src/Node/CheckToStringNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -24,11 +25,12 @@ * * @author Fabien Potencier */ +#[YieldReady] class CheckToStringNode extends AbstractExpression { public function __construct(AbstractExpression $expr) { - parent::__construct(['expr' => $expr], [], $expr->getTemplateLine(), $expr->getNodeTag()); + parent::__construct(['expr' => $expr], [], $expr->getTemplateLine()); } public function compile(Compiler $compiler): void diff --git a/app/vendor/twig/twig/src/Node/DeprecatedNode.php b/app/vendor/twig/twig/src/Node/DeprecatedNode.php index 5ff44307f..0772adfc3 100644 --- a/app/vendor/twig/twig/src/Node/DeprecatedNode.php +++ b/app/vendor/twig/twig/src/Node/DeprecatedNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -20,11 +21,12 @@ * * @author Yonel Ceruto */ +#[YieldReady] class DeprecatedNode extends Node { - public function __construct(AbstractExpression $expr, int $lineno, string $tag = null) + public function __construct(AbstractExpression $expr, int $lineno) { - parent::__construct(['expr' => $expr], [], $lineno, $tag); + parent::__construct(['expr' => $expr], [], $lineno); } public function compile(Compiler $compiler): void @@ -33,21 +35,39 @@ public function compile(Compiler $compiler): void $expr = $this->getNode('expr'); - if ($expr instanceof ConstantExpression) { - $compiler->write('@trigger_error(') - ->subcompile($expr); - } else { + if (!$expr instanceof ConstantExpression) { $varName = $compiler->getVarName(); - $compiler->write(sprintf('$%s = ', $varName)) + $compiler + ->write(\sprintf('$%s = ', $varName)) ->subcompile($expr) ->raw(";\n") - ->write(sprintf('@trigger_error($%s', $varName)); + ; + } + + $compiler->write('trigger_deprecation('); + if ($this->hasNode('package')) { + $compiler->subcompile($this->getNode('package')); + } else { + $compiler->raw("''"); + } + $compiler->raw(', '); + if ($this->hasNode('version')) { + $compiler->subcompile($this->getNode('version')); + } else { + $compiler->raw("''"); + } + $compiler->raw(', '); + + if ($expr instanceof ConstantExpression) { + $compiler->subcompile($expr); + } else { + $compiler->write(\sprintf('$%s', $varName)); } $compiler ->raw('.') - ->string(sprintf(' ("%s" at line %d).', $this->getTemplateName(), $this->getTemplateLine())) - ->raw(", E_USER_DEPRECATED);\n") + ->string(\sprintf(' in "%s" at line %d.', $this->getTemplateName(), $this->getTemplateLine())) + ->raw(");\n") ; } } diff --git a/app/vendor/twig/twig/src/Node/DoNode.php b/app/vendor/twig/twig/src/Node/DoNode.php index f7783d19f..1593fd050 100644 --- a/app/vendor/twig/twig/src/Node/DoNode.php +++ b/app/vendor/twig/twig/src/Node/DoNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -19,11 +20,12 @@ * * @author Fabien Potencier */ +#[YieldReady] class DoNode extends Node { - public function __construct(AbstractExpression $expr, int $lineno, string $tag = null) + public function __construct(AbstractExpression $expr, int $lineno) { - parent::__construct(['expr' => $expr], [], $lineno, $tag); + parent::__construct(['expr' => $expr], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/app/vendor/twig/twig/src/Node/EmbedNode.php b/app/vendor/twig/twig/src/Node/EmbedNode.php index 903c3f6c7..597f95e44 100644 --- a/app/vendor/twig/twig/src/Node/EmbedNode.php +++ b/app/vendor/twig/twig/src/Node/EmbedNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -20,21 +21,22 @@ * * @author Fabien Potencier */ +#[YieldReady] class EmbedNode extends IncludeNode { // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module) - public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null) + public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno) { - parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag); + parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno); $this->setAttribute('name', $name); $this->setAttribute('index', $index); } - protected function addGetTemplate(Compiler $compiler): void + protected function addGetTemplate(Compiler $compiler, string $template = ''): void { $compiler - ->write('$this->loadTemplate(') + ->raw('$this->loadTemplate(') ->string($this->getAttribute('name')) ->raw(', ') ->repr($this->getTemplateName()) @@ -44,5 +46,11 @@ protected function addGetTemplate(Compiler $compiler): void ->string($this->getAttribute('index')) ->raw(')') ; + if ($this->getAttribute('ignore_missing')) { + $compiler + ->raw(";\n") + ->write(\sprintf("\$%s->getParent(\$context);\n", $template)) + ; + } } } diff --git a/app/vendor/twig/twig/src/Node/EmptyNode.php b/app/vendor/twig/twig/src/Node/EmptyNode.php new file mode 100644 index 000000000..fd4717ff4 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/EmptyNode.php @@ -0,0 +1,33 @@ + + */ +#[YieldReady] +final class EmptyNode extends Node +{ + public function __construct(int $lineno = 0) + { + parent::__construct([], [], $lineno); + } + + public function setNode(string $name, Node $node): void + { + throw new \LogicException('EmptyNode cannot have children.'); + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/AbstractExpression.php b/app/vendor/twig/twig/src/Node/Expression/AbstractExpression.php index 42da0559d..22d8617cd 100644 --- a/app/vendor/twig/twig/src/Node/Expression/AbstractExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/AbstractExpression.php @@ -21,4 +21,23 @@ */ abstract class AbstractExpression extends Node { + public function isGenerator(): bool + { + return $this->hasAttribute('is_generator') && $this->getAttribute('is_generator'); + } + + /** + * @return static + */ + public function setExplicitParentheses(): self + { + $this->setAttribute('with_parentheses', true); + + return $this; + } + + public function hasExplicitParentheses(): bool + { + return $this->hasAttribute('with_parentheses') && $this->getAttribute('with_parentheses'); + } } diff --git a/app/vendor/twig/twig/src/Node/Expression/ArrayExpression.php b/app/vendor/twig/twig/src/Node/Expression/ArrayExpression.php index 444283802..61a5063f3 100644 --- a/app/vendor/twig/twig/src/Node/Expression/ArrayExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/ArrayExpression.php @@ -12,6 +12,8 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Node\Expression\Unary\StringCastUnary; +use Twig\Node\Expression\Variable\ContextVariable; class ArrayExpression extends AbstractExpression { @@ -55,7 +57,7 @@ public function hasElement(AbstractExpression $key): bool return false; } - public function addElement(AbstractExpression $value, AbstractExpression $key = null): void + public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void { if (null === $key) { $key = new ConstantExpression(++$this->index, $value->getTemplateLine()); @@ -70,7 +72,7 @@ public function compile(Compiler $compiler): void $needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs); if ($needsArrayMergeSpread) { - $compiler->raw('twig_array_merge('); + $compiler->raw('CoreExtension::merge('); } $compiler->raw('['); $first = true; @@ -97,19 +99,25 @@ public function compile(Compiler $compiler): void $compiler->raw('...')->subcompile($pair['value']); ++$nextIndex; } else { - $key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null; + $key = null; + if ($pair['key'] instanceof ContextVariable) { + $pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine()); + } + if ($pair['key'] instanceof TempNameExpression) { + $key = $pair['key']->getAttribute('name'); + $pair['key'] = new ConstantExpression($key, $pair['key']->getTemplateLine()); + } + if ($pair['key'] instanceof ConstantExpression) { + $key = $pair['key']->getAttribute('value'); + } if ($nextIndex !== $key) { - if (\is_int($key)) { - $nextIndex = $key + 1; - } $compiler ->subcompile($pair['key']) ->raw(' => ') ; - } else { - ++$nextIndex; } + ++$nextIndex; $compiler->subcompile($pair['value']); } diff --git a/app/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php b/app/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php index eaad03c9c..2bae4edd7 100644 --- a/app/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php @@ -21,9 +21,9 @@ */ class ArrowFunctionExpression extends AbstractExpression { - public function __construct(AbstractExpression $expr, Node $names, $lineno, $tag = null) + public function __construct(AbstractExpression $expr, Node $names, $lineno) { - parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno, $tag); + parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/app/vendor/twig/twig/src/Node/Expression/AssignNameExpression.php b/app/vendor/twig/twig/src/Node/Expression/AssignNameExpression.php index 7dd1bc4a3..c194660da 100644 --- a/app/vendor/twig/twig/src/Node/Expression/AssignNameExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/AssignNameExpression.php @@ -13,9 +13,26 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Error\SyntaxError; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\ContextVariable; -class AssignNameExpression extends NameExpression +class AssignNameExpression extends ContextVariable { + public function __construct(string $name, int $lineno) + { + if (self::class === static::class) { + trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, AssignContextVariable::class); + } + + // All names supported by ExpressionParser::parsePrimaryExpression() should be excluded + if (\in_array(strtolower($name), ['true', 'false', 'none', 'null'])) { + throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno); + } + + parent::__construct($name, $lineno); + } + public function compile(Compiler $compiler): void { $compiler diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/AbstractBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/AbstractBinary.php index c424e5cc5..bd6cc6c02 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/AbstractBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/AbstractBinary.php @@ -16,10 +16,21 @@ use Twig\Node\Expression\AbstractExpression; use Twig\Node\Node; -abstract class AbstractBinary extends AbstractExpression +abstract class AbstractBinary extends AbstractExpression implements BinaryInterface { + /** + * @param AbstractExpression $left + * @param AbstractExpression $right + */ public function __construct(Node $left, Node $right, int $lineno) { + if (!$left instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($left)); + } + if (!$right instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($right)); + } + parent::__construct(['left' => $left, 'right' => $right], [], $lineno); } diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/BinaryInterface.php b/app/vendor/twig/twig/src/Node/Expression/Binary/BinaryInterface.php new file mode 100644 index 000000000..eeeb2eb99 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/BinaryInterface.php @@ -0,0 +1,22 @@ +setNode('test', clone $left); + $left->setAttribute('always_defined', true); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('((') + ->subcompile($this->getNode('test')) + ->raw(') ? (') + ->subcompile($this->getNode('left')) + ->raw(') : (') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('?:'); + } + + public function getOperandNamesToEscape(): array + { + return ['left', 'right']; + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php index 73fa20b1f..a73a5608d 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php @@ -20,11 +20,11 @@ public function compile(Compiler $compiler): void $left = $compiler->getVarName(); $right = $compiler->getVarName(); $compiler - ->raw(sprintf('(is_string($%s = ', $left)) + ->raw(\sprintf('(is_string($%s = ', $left)) ->subcompile($this->getNode('left')) - ->raw(sprintf(') && is_string($%s = ', $right)) + ->raw(\sprintf(') && is_string($%s = ', $right)) ->subcompile($this->getNode('right')) - ->raw(sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right)) + ->raw(\sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right)) ; } diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/EqualBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/EqualBinary.php index 6b48549ef..5f423196f 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/EqualBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/EqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 === twig_compare(') + ->raw('(0 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/GreaterBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/GreaterBinary.php index e1dd06780..f42de3f86 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/GreaterBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/GreaterBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(1 === twig_compare(') + ->raw('(1 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php index df9bfcfbf..0c4f43fd9 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 <= twig_compare(') + ->raw('(0 <= CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php index adfabd44c..c57bb20e9 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php @@ -18,7 +18,7 @@ class HasEveryBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_array_every($this->env, ') + ->raw('CoreExtension::arrayEvery($this->env, ') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php index 270da3692..12293f84c 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php @@ -18,7 +18,7 @@ class HasSomeBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_array_some($this->env, ') + ->raw('CoreExtension::arraySome($this->env, ') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/InBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/InBinary.php index 6dbfa97f0..68a98fe15 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/InBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/InBinary.php @@ -18,7 +18,7 @@ class InBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_in_filter(') + ->raw('CoreExtension::inFilter(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/LessBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/LessBinary.php index 598e62913..fb3264a2d 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/LessBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/LessBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(-1 === twig_compare(') + ->raw('(-1 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php index e3c4af58d..8f3653892 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 >= twig_compare(') + ->raw('(0 >= CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/MatchesBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/MatchesBinary.php index a8bce6f4e..0a523c216 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/MatchesBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/MatchesBinary.php @@ -12,13 +12,31 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Error\SyntaxError; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Node; class MatchesBinary extends AbstractBinary { + public function __construct(Node $left, Node $right, int $lineno) + { + if ($right instanceof ConstantExpression) { + $regexp = $right->getAttribute('value'); + set_error_handler(static fn ($t, $m) => throw new SyntaxError(\sprintf('Regexp "%s" passed to "matches" is not valid: %s.', $regexp, substr($m, 14)), $lineno)); + try { + preg_match($regexp, ''); + } finally { + restore_error_handler(); + } + } + + parent::__construct($left, $right, $lineno); + } + public function compile(Compiler $compiler): void { $compiler - ->raw('twig_matches(') + ->raw('CoreExtension::matches(') ->subcompile($this->getNode('right')) ->raw(', ') ->subcompile($this->getNode('left')) diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php index db47a2890..d137ef627 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 !== twig_compare(') + ->raw('(0 !== CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/NotInBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/NotInBinary.php index fcba6cca1..80c8755d8 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/NotInBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/NotInBinary.php @@ -18,7 +18,7 @@ class NotInBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('!twig_in_filter(') + ->raw('!CoreExtension::inFilter(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/NullCoalesceBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/NullCoalesceBinary.php new file mode 100644 index 000000000..a047b6030 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/NullCoalesceBinary.php @@ -0,0 +1,71 @@ +getTemplateLine()); + // for "block()", we don't need the null test as the return value is always a string + if (!$left instanceof BlockReferenceExpression) { + $test = new AndBinary( + $test, + new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()), + $left->getTemplateLine(), + ); + } + + $left->setAttribute('always_defined', true); + $this->setNode('test', $test); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('((') + ->subcompile($this->getNode('test')) + ->raw(') ? (') + ->subcompile($this->getNode('left')) + ->raw(') : (') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('??'); + } + + public function getOperandNamesToEscape(): array + { + return ['left', 'right']; + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php index 22eff92a7..4519f30d9 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php @@ -20,11 +20,11 @@ public function compile(Compiler $compiler): void $left = $compiler->getVarName(); $right = $compiler->getVarName(); $compiler - ->raw(sprintf('(is_string($%s = ', $left)) + ->raw(\sprintf('(is_string($%s = ', $left)) ->subcompile($this->getNode('left')) - ->raw(sprintf(') && is_string($%s = ', $right)) + ->raw(\sprintf(') && is_string($%s = ', $right)) ->subcompile($this->getNode('right')) - ->raw(sprintf(') && str_starts_with($%1$s, $%2$s))', $left, $right)) + ->raw(\sprintf(') && str_starts_with($%1$s, $%2$s))', $left, $right)) ; } diff --git a/app/vendor/twig/twig/src/Node/Expression/Binary/XorBinary.php b/app/vendor/twig/twig/src/Node/Expression/Binary/XorBinary.php new file mode 100644 index 000000000..d8ccd7853 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/Binary/XorBinary.php @@ -0,0 +1,23 @@ +raw('xor'); + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/BlockReferenceExpression.php b/app/vendor/twig/twig/src/Node/Expression/BlockReferenceExpression.php index b1e2a8f7b..a5a3cee3f 100644 --- a/app/vendor/twig/twig/src/Node/Expression/BlockReferenceExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/BlockReferenceExpression.php @@ -22,14 +22,21 @@ */ class BlockReferenceExpression extends AbstractExpression { - public function __construct(Node $name, ?Node $template, int $lineno, string $tag = null) + /** + * @param AbstractExpression $name + */ + public function __construct(Node $name, ?Node $template, int $lineno) { + if (!$name instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($name)); + } + $nodes = ['name' => $name]; if (null !== $template) { $nodes['template'] = $template; } - parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno, $tag); + parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno); } public function compile(Compiler $compiler): void @@ -40,8 +47,9 @@ public function compile(Compiler $compiler): void if ($this->getAttribute('output')) { $compiler->addDebugInfo($this); + $compiler->write('yield from '); $this - ->compileTemplateCall($compiler, 'displayBlock') + ->compileTemplateCall($compiler, 'yieldBlock') ->raw(";\n"); } else { $this->compileTemplateCall($compiler, 'renderBlock'); @@ -65,7 +73,7 @@ private function compileTemplateCall(Compiler $compiler, string $method): Compil ; } - $compiler->raw(sprintf('->%s', $method)); + $compiler->raw(\sprintf('->unwrap()->%s', $method)); return $this->compileBlockArguments($compiler); } diff --git a/app/vendor/twig/twig/src/Node/Expression/CallExpression.php b/app/vendor/twig/twig/src/Node/Expression/CallExpression.php index 3a2d7a4fc..330d82535 100644 --- a/app/vendor/twig/twig/src/Node/Expression/CallExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/CallExpression.php @@ -15,40 +15,52 @@ use Twig\Error\SyntaxError; use Twig\Extension\ExtensionInterface; use Twig\Node\Node; +use Twig\TwigCallableInterface; +use Twig\TwigFilter; +use Twig\TwigFunction; +use Twig\TwigTest; +use Twig\Util\CallableArgumentsExtractor; +use Twig\Util\ReflectionCallable; abstract class CallExpression extends AbstractExpression { - private $reflector; + private $reflector = null; + /** + * @return void + */ protected function compileCallable(Compiler $compiler) { - $callable = $this->getAttribute('callable'); + $twigCallable = $this->getTwigCallable(); + $callable = $twigCallable->getCallable(); if (\is_string($callable) && !str_contains($callable, '::')) { $compiler->raw($callable); } else { - [$r, $callable] = $this->reflectCallable($callable); + $rc = $this->reflectCallable($twigCallable); + $r = $rc->getReflector(); + $callable = $rc->getCallable(); if (\is_string($callable)) { $compiler->raw($callable); } elseif (\is_array($callable) && \is_string($callable[0])) { if (!$r instanceof \ReflectionMethod || $r->isStatic()) { - $compiler->raw(sprintf('%s::%s', $callable[0], $callable[1])); + $compiler->raw(\sprintf('%s::%s', $callable[0], $callable[1])); } else { - $compiler->raw(sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1])); + $compiler->raw(\sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1])); } } elseif (\is_array($callable) && $callable[0] instanceof ExtensionInterface) { $class = \get_class($callable[0]); if (!$compiler->getEnvironment()->hasExtension($class)) { // Compile a non-optimized call to trigger a \Twig\Error\RuntimeError, which cannot be a compile-time error - $compiler->raw(sprintf('$this->env->getExtension(\'%s\')', $class)); + $compiler->raw(\sprintf('$this->env->getExtension(\'%s\')', $class)); } else { - $compiler->raw(sprintf('$this->extensions[\'%s\']', ltrim($class, '\\'))); + $compiler->raw(\sprintf('$this->extensions[\'%s\']', ltrim($class, '\\'))); } - $compiler->raw(sprintf('->%s', $callable[1])); + $compiler->raw(\sprintf('->%s', $callable[1])); } else { - $compiler->raw(sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('name'))); + $compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $twigCallable->getDynamicName())); } } @@ -57,16 +69,30 @@ protected function compileCallable(Compiler $compiler) protected function compileArguments(Compiler $compiler, $isArray = false): void { + if (\func_num_args() >= 2) { + trigger_deprecation('twig/twig', '3.11', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); + } + $compiler->raw($isArray ? '[' : '('); $first = true; - if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + $twigCallable = $this->getAttribute('twig_callable'); + + if ($twigCallable->needsCharset()) { + $compiler->raw('$this->env->getCharset()'); + $first = false; + } + + if ($twigCallable->needsEnvironment()) { + if (!$first) { + $compiler->raw(', '); + } $compiler->raw('$this->env'); $first = false; } - if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + if ($twigCallable->needsContext()) { if (!$first) { $compiler->raw(', '); } @@ -74,14 +100,12 @@ protected function compileArguments(Compiler $compiler, $isArray = false): void $first = false; } - if ($this->hasAttribute('arguments')) { - foreach ($this->getAttribute('arguments') as $argument) { - if (!$first) { - $compiler->raw(', '); - } - $compiler->string($argument); - $first = false; + foreach ($twigCallable->getArguments() as $argument) { + if (!$first) { + $compiler->raw(', '); } + $compiler->string($argument); + $first = false; } if ($this->hasNode('node')) { @@ -93,8 +117,7 @@ protected function compileArguments(Compiler $compiler, $isArray = false): void } if ($this->hasNode('arguments')) { - $callable = $this->getAttribute('callable'); - $arguments = $this->getArguments($callable, $this->getNode('arguments')); + $arguments = (new CallableArgumentsExtractor($this, $this->getTwigCallable()))->extractArguments($this->getNode('arguments')); foreach ($arguments as $node) { if (!$first) { $compiler->raw(', '); @@ -107,8 +130,13 @@ protected function compileArguments(Compiler $compiler, $isArray = false): void $compiler->raw($isArray ? ']' : ')'); } + /** + * @deprecated since Twig 3.12, use Twig\Util\CallableArgumentsExtractor::getArguments() instead + */ protected function getArguments($callable, $arguments) { + trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated, use Twig\Util\CallableArgumentsExtractor::getArguments() instead.', __METHOD__); + $callType = $this->getAttribute('type'); $callName = $this->getAttribute('name'); @@ -119,28 +147,28 @@ protected function getArguments($callable, $arguments) $named = true; $name = $this->normalizeName($name); } elseif ($named) { - throw new SyntaxError(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); + throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); } $parameters[$name] = $node; } - $isVariadic = $this->hasAttribute('is_variadic') && $this->getAttribute('is_variadic'); + $isVariadic = $this->getAttribute('twig_callable')->isVariadic(); if (!$named && !$isVariadic) { return $parameters; } if (!$callable) { if ($named) { - $message = sprintf('Named arguments are not supported for %s "%s".', $callType, $callName); + $message = \sprintf('Named arguments are not supported for %s "%s".', $callType, $callName); } else { - $message = sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName); + $message = \sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName); } throw new \LogicException($message); } - list($callableParameters, $isPhpVariadic) = $this->getCallableParameters($callable, $isVariadic); + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters($callable, $isVariadic); $arguments = []; $names = []; $missingArguments = []; @@ -160,11 +188,11 @@ protected function getArguments($callable, $arguments) if (\array_key_exists($name, $parameters)) { if (\array_key_exists($pos, $parameters)) { - throw new SyntaxError(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); + throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); } if (\count($missingArguments)) { - throw new SyntaxError(sprintf( + throw new SyntaxError(\sprintf( 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', $name, $callType, $callName, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) ), $this->getTemplateLine(), $this->getSourceContext()); @@ -183,13 +211,13 @@ protected function getArguments($callable, $arguments) } elseif ($callableParameter->isDefaultValueAvailable()) { $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), -1); } elseif ($callableParameter->isOptional()) { - if (empty($parameters)) { + if (!$parameters) { break; } else { $missingArguments[] = $name; } } else { - throw new SyntaxError(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); } } @@ -210,7 +238,7 @@ protected function getArguments($callable, $arguments) } } - if (!empty($parameters)) { + if ($parameters) { $unknownParameter = null; foreach ($parameters as $parameter) { if ($parameter instanceof Node) { @@ -220,7 +248,7 @@ protected function getArguments($callable, $arguments) } throw new SyntaxError( - sprintf( + \sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', \count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names) ), @@ -232,90 +260,106 @@ protected function getArguments($callable, $arguments) return $arguments; } + /** + * @deprecated since Twig 3.12 + */ protected function normalizeName(string $name): string { + trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated.', __METHOD__); + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); } + // To be removed in 4.0 private function getCallableParameters($callable, bool $isVariadic): array { - [$r, , $callableName] = $this->reflectCallable($callable); + $twigCallable = $this->getAttribute('twig_callable'); + $rc = $this->reflectCallable($twigCallable); + $r = $rc->getReflector(); + $callableName = $rc->getName(); $parameters = $r->getParameters(); if ($this->hasNode('node')) { array_shift($parameters); } - if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + if ($twigCallable->needsCharset()) { array_shift($parameters); } - if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + if ($twigCallable->needsEnvironment()) { array_shift($parameters); } - if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) { - foreach ($this->getAttribute('arguments') as $argument) { - array_shift($parameters); - } + if ($twigCallable->needsContext()) { + array_shift($parameters); + } + foreach ($twigCallable->getArguments() as $argument) { + array_shift($parameters); } + $isPhpVariadic = false; if ($isVariadic) { $argument = end($parameters); - $isArray = $argument && $argument->hasType() && 'array' === $argument->getType()->getName(); + $isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName(); if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) { array_pop($parameters); } elseif ($argument && $argument->isVariadic()) { array_pop($parameters); $isPhpVariadic = true; } else { - throw new \LogicException(sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $this->getAttribute('name'))); + throw new \LogicException(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $twigCallable->getName())); } } return [$parameters, $isPhpVariadic]; } - private function reflectCallable($callable) + private function reflectCallable(TwigCallableInterface $callable): ReflectionCallable { - if (null !== $this->reflector) { - return $this->reflector; - } - - if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { - $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; + if (!$this->reflector) { + $this->reflector = new ReflectionCallable($callable); } - if (\is_array($callable) && method_exists($callable[0], $callable[1])) { - $r = new \ReflectionMethod($callable[0], $callable[1]); - - return $this->reflector = [$r, $callable, $r->class.'::'.$r->name]; - } - - $checkVisibility = $callable instanceof \Closure; - try { - $closure = \Closure::fromCallable($callable); - } catch (\TypeError $e) { - throw new \LogicException(sprintf('Callback for %s "%s" is not callable in the current scope.', $this->getAttribute('type'), $this->getAttribute('name')), 0, $e); - } - $r = new \ReflectionFunction($closure); - - if (str_contains($r->name, '{closure}')) { - return $this->reflector = [$r, $callable, 'Closure']; - } - - if ($object = $r->getClosureThis()) { - $callable = [$object, $r->name]; - $callableName = get_debug_type($object).'::'.$r->name; - } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { - $callableName = $class->name.'::'.$r->name; - } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { - $callableName = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name; - } else { - $callable = $callableName = $r->name; - } - - if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) { - $callable = $r->getClosure(); - } + return $this->reflector; + } - return $this->reflector = [$r, $callable, $callableName]; + /** + * Overrides the Twig callable based on attributes (as potentially, attributes changed between the creation and the compilation of the node). + * + * To be removed in 4.0 and replace by $this->getAttribute('twig_callable'). + */ + private function getTwigCallable(): TwigCallableInterface + { + $current = $this->getAttribute('twig_callable'); + + $this->setAttribute('twig_callable', match ($this->getAttribute('type')) { + 'test' => (new TwigTest( + $this->getAttribute('name'), + $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), + [ + 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), + ], + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), + 'function' => (new TwigFunction( + $this->hasAttribute('name') ? $this->getAttribute('name') : $current->getName(), + $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), + [ + 'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(), + 'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(), + 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), + 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), + ], + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), + 'filter' => (new TwigFilter( + $this->getAttribute('name'), + $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), + [ + 'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(), + 'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(), + 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), + 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), + ], + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), + }); + + return $this->getAttribute('twig_callable'); } } diff --git a/app/vendor/twig/twig/src/Node/Expression/ConditionalExpression.php b/app/vendor/twig/twig/src/Node/Expression/ConditionalExpression.php index d7db99357..7fe309cf3 100644 --- a/app/vendor/twig/twig/src/Node/Expression/ConditionalExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/ConditionalExpression.php @@ -13,11 +13,14 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Node\Expression\Ternary\ConditionalTernary; -class ConditionalExpression extends AbstractExpression +class ConditionalExpression extends AbstractExpression implements OperatorEscapeInterface { public function __construct(AbstractExpression $expr1, AbstractExpression $expr2, AbstractExpression $expr3, int $lineno) { + trigger_deprecation('twig/twig', '3.17', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, ConditionalTernary::class)); + parent::__construct(['expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3], [], $lineno); } @@ -42,4 +45,9 @@ public function compile(Compiler $compiler): void ->raw('))'); } } + + public function getOperandNamesToEscape(): array + { + return ['expr2', 'expr3']; + } } diff --git a/app/vendor/twig/twig/src/Node/Expression/ConstantExpression.php b/app/vendor/twig/twig/src/Node/Expression/ConstantExpression.php index 7ddbcc6fa..2a8909d54 100644 --- a/app/vendor/twig/twig/src/Node/Expression/ConstantExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/ConstantExpression.php @@ -14,6 +14,9 @@ use Twig\Compiler; +/** + * @final + */ class ConstantExpression extends AbstractExpression { public function __construct($value, int $lineno) diff --git a/app/vendor/twig/twig/src/Node/Expression/Filter/DefaultFilter.php b/app/vendor/twig/twig/src/Node/Expression/Filter/DefaultFilter.php index 6a572d488..bccd7f0a4 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Filter/DefaultFilter.php +++ b/app/vendor/twig/twig/src/Node/Expression/Filter/DefaultFilter.php @@ -11,14 +11,20 @@ namespace Twig\Node\Expression\Filter; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; -use Twig\Node\Expression\ConditionalExpression; +use Twig\Extension\CoreExtension; +use Twig\Node\EmptyNode; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Expression\Test\DefinedTest; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; +use Twig\TwigFilter; +use Twig\TwigTest; /** * Returns the value or the default value when it is undefined or empty. @@ -29,20 +35,34 @@ */ class DefaultFilter extends FilterExpression { - public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null) + /** + * @param AbstractExpression $node + */ + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { - $default = new FilterExpression($node, new ConstantExpression('default', $node->getTemplateLine()), $arguments, $node->getTemplateLine()); + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + } + + if ($filter instanceof TwigFilter) { + $name = $filter->getName(); + $default = new FilterExpression($node, $filter, $arguments, $node->getTemplateLine()); + } else { + $name = $filter->getAttribute('value'); + $default = new FilterExpression($node, new TwigFilter('default', [CoreExtension::class, 'default']), $arguments, $node->getTemplateLine()); + } - if ('default' === $filterName->getAttribute('value') && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) { - $test = new DefinedTest(clone $node, 'defined', new Node(), $node->getTemplateLine()); - $false = \count($arguments) ? $arguments->getNode(0) : new ConstantExpression('', $node->getTemplateLine()); + if ('default' === $name && ($node instanceof ContextVariable || $node instanceof GetAttrExpression)) { + $test = new DefinedTest(clone $node, new TwigTest('defined'), new EmptyNode(), $node->getTemplateLine()); + $false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine()); - $node = new ConditionalExpression($test, $default, $false, $node->getTemplateLine()); + $node = new ConditionalTernary($test, $default, $false, $node->getTemplateLine()); } else { $node = $default; } - parent::__construct($node, $filterName, $arguments, $lineno, $tag); + parent::__construct($node, $filter, $arguments, $lineno); } public function compile(Compiler $compiler): void diff --git a/app/vendor/twig/twig/src/Node/Expression/Filter/RawFilter.php b/app/vendor/twig/twig/src/Node/Expression/Filter/RawFilter.php new file mode 100644 index 000000000..0a49e7c4f --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/Filter/RawFilter.php @@ -0,0 +1,45 @@ + + */ +class RawFilter extends FilterExpression +{ + /** + * @param AbstractExpression $node + */ + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0) + { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + } + + parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new EmptyNode(), $lineno ?: $node->getTemplateLine()); + } + + public function compile(Compiler $compiler): void + { + $compiler->subcompile($this->getNode('node')); + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/FilterExpression.php b/app/vendor/twig/twig/src/Node/Expression/FilterExpression.php index 0fc158869..6e0c486ab 100644 --- a/app/vendor/twig/twig/src/Node/Expression/FilterExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/FilterExpression.php @@ -12,28 +12,68 @@ namespace Twig\Node\Expression; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Node\NameDeprecation; use Twig\Node\Node; +use Twig\TwigFilter; class FilterExpression extends CallExpression { - public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null) + /** + * @param AbstractExpression $node + */ + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { - parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], [], $lineno, $tag); + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + } + + if ($filter instanceof TwigFilter) { + $name = $filter->getName(); + $filterName = new ConstantExpression($name, $lineno); + } else { + $name = $filter->getAttribute('value'); + $filterName = $filter; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFilter" when creating a "%s" filter of type "%s" is deprecated.', $name, static::class); + } + + parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], ['name' => $name, 'type' => 'filter'], $lineno); + + if ($filter instanceof TwigFilter) { + $this->setAttribute('twig_callable', $filter); + } + + $this->deprecateNode('filter', new NameDeprecation('twig/twig', '3.12')); + + $this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); } public function compile(Compiler $compiler): void { - $name = $this->getNode('filter')->getAttribute('value'); - $filter = $compiler->getEnvironment()->getFilter($name); - - $this->setAttribute('name', $name); - $this->setAttribute('type', 'filter'); - $this->setAttribute('needs_environment', $filter->needsEnvironment()); - $this->setAttribute('needs_context', $filter->needsContext()); - $this->setAttribute('arguments', $filter->getArguments()); - $this->setAttribute('callable', $filter->getCallable()); - $this->setAttribute('is_variadic', $filter->isVariadic()); + $name = $this->getNode('filter', false)->getAttribute('value'); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.11', 'Changing the value of a "filter" node in a NodeVisitor class is not supported anymore.'); + $this->removeAttribute('twig_callable'); + } + if ('raw' === $name) { + trigger_deprecation('twig/twig', '3.11', 'Creating the "raw" filter via "FilterExpression" is deprecated; use "RawFilter" instead.'); + + $compiler->subcompile($this->getNode('node')); + + return; + } + + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getFilter($name)); + } $this->compileCallable($compiler); } diff --git a/app/vendor/twig/twig/src/Node/Expression/FunctionExpression.php b/app/vendor/twig/twig/src/Node/Expression/FunctionExpression.php index 71269775c..5e22e73e8 100644 --- a/app/vendor/twig/twig/src/Node/Expression/FunctionExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/FunctionExpression.php @@ -11,32 +11,60 @@ namespace Twig\Node\Expression; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Node\NameDeprecation; use Twig\Node\Node; +use Twig\TwigFunction; class FunctionExpression extends CallExpression { - public function __construct(string $name, Node $arguments, int $lineno) + #[FirstClassTwigCallableReady] + public function __construct(TwigFunction|string $function, Node $arguments, int $lineno) { - parent::__construct(['arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno); + if ($function instanceof TwigFunction) { + $name = $function->getName(); + } else { + $name = $function; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFunction" when creating a "%s" function of type "%s" is deprecated.', $name, static::class); + } + + parent::__construct(['arguments' => $arguments], ['name' => $name, 'type' => 'function', 'is_defined_test' => false], $lineno); + + if ($function instanceof TwigFunction) { + $this->setAttribute('twig_callable', $function); + } + + $this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); } + /** + * @return void + */ public function compile(Compiler $compiler) { $name = $this->getAttribute('name'); - $function = $compiler->getEnvironment()->getFunction($name); - - $this->setAttribute('name', $name); - $this->setAttribute('type', 'function'); - $this->setAttribute('needs_environment', $function->needsEnvironment()); - $this->setAttribute('needs_context', $function->needsContext()); - $this->setAttribute('arguments', $function->getArguments()); - $callable = $function->getCallable(); + if ($this->hasAttribute('twig_callable')) { + $name = $this->getAttribute('twig_callable')->getName(); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.12', 'Changing the value of a "function" node in a NodeVisitor class is not supported anymore.'); + $this->removeAttribute('twig_callable'); + } + } + + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getFunction($name)); + } + if ('constant' === $name && $this->getAttribute('is_defined_test')) { - $callable = 'twig_constant_is_defined'; + $this->getNode('arguments')->setNode('checkDefined', new ConstantExpression(true, $this->getTemplateLine())); } - $this->setAttribute('callable', $callable); - $this->setAttribute('is_variadic', $function->isVariadic()); $this->compileCallable($compiler); } diff --git a/app/vendor/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php b/app/vendor/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php new file mode 100644 index 000000000..7e5c25ff4 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php @@ -0,0 +1,41 @@ +getNode('arguments'); + if ($arguments->hasNode('enum')) { + $firstArgument = $arguments->getNode('enum'); + } elseif ($arguments->hasNode('0')) { + $firstArgument = $arguments->getNode('0'); + } else { + $firstArgument = null; + } + + if (!$firstArgument instanceof ConstantExpression || 1 !== \count($arguments)) { + parent::compile($compiler); + + return; + } + + $value = $firstArgument->getAttribute('value'); + + if (!\is_string($value)) { + throw new SyntaxError('The first argument of the "enum_cases" function must be a string.', $this->getTemplateLine(), $this->getSourceContext()); + } + + if (!enum_exists($value)) { + throw new SyntaxError(\sprintf('The first argument of the "enum_cases" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext()); + } + + $compiler->raw(\sprintf('%s::cases()', $value)); + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/FunctionNode/EnumFunction.php b/app/vendor/twig/twig/src/Node/Expression/FunctionNode/EnumFunction.php new file mode 100644 index 000000000..1f8b0ecf1 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/FunctionNode/EnumFunction.php @@ -0,0 +1,45 @@ +getNode('arguments'); + if ($arguments->hasNode('enum')) { + $firstArgument = $arguments->getNode('enum'); + } elseif ($arguments->hasNode('0')) { + $firstArgument = $arguments->getNode('0'); + } else { + $firstArgument = null; + } + + if (!$firstArgument instanceof ConstantExpression || 1 !== \count($arguments)) { + parent::compile($compiler); + + return; + } + + $value = $firstArgument->getAttribute('value'); + + if (!\is_string($value)) { + throw new SyntaxError('The first argument of the "enum" function must be a string.', $this->getTemplateLine(), $this->getSourceContext()); + } + + if (!enum_exists($value)) { + throw new SyntaxError(\sprintf('The first argument of the "enum" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext()); + } + + if (!$cases = $value::cases()) { + throw new SyntaxError(\sprintf('The first argument of the "enum" function must be a non-empty enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext()); + } + + $compiler->raw(\sprintf('%s::%s', $value, $cases[0]->name)); + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php b/app/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php index e6a75ce94..e072f2a0b 100644 --- a/app/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php @@ -14,10 +14,14 @@ use Twig\Compiler; use Twig\Extension\SandboxExtension; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Template; class GetAttrExpression extends AbstractExpression { + /** + * @param ArrayExpression|NameExpression|null $arguments + */ public function __construct(AbstractExpression $node, AbstractExpression $attribute, ?AbstractExpression $arguments, string $type, int $lineno) { $nodes = ['node' => $node, 'attribute' => $attribute]; @@ -25,12 +29,17 @@ public function __construct(AbstractExpression $node, AbstractExpression $attrib $nodes['arguments'] = $arguments; } + if ($arguments && !$arguments instanceof ArrayExpression && !$arguments instanceof ContextVariable) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class)); + } + parent::__construct($nodes, ['type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'optimizable' => true], $lineno); } public function compile(Compiler $compiler): void { $env = $compiler->getEnvironment(); + $arrayAccessSandbox = false; // optimize array calls if ( @@ -44,20 +53,38 @@ public function compile(Compiler $compiler): void ->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($var.'::class') + ->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (') ->raw($var) ->raw('[') ->subcompile($this->getNode('attribute')) - ->raw('] ?? null) : null)') + ->raw('] ?? null) : ') ; - - return; } - $compiler->raw('twig_get_attribute($this->env, $this->source, '); + $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); if ($this->getAttribute('ignore_strict_check')) { $this->getNode('node')->setAttribute('ignore_strict_check', true); @@ -83,5 +110,9 @@ public function compile(Compiler $compiler): void ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) ->raw(')') ; + + if ($arrayAccessSandbox) { + $compiler->raw(')'); + } } } diff --git a/app/vendor/twig/twig/src/Node/Expression/InlinePrint.php b/app/vendor/twig/twig/src/Node/Expression/InlinePrint.php index 1ad4751e4..5509f7942 100644 --- a/app/vendor/twig/twig/src/Node/Expression/InlinePrint.php +++ b/app/vendor/twig/twig/src/Node/Expression/InlinePrint.php @@ -19,17 +19,21 @@ */ final class InlinePrint extends AbstractExpression { + /** + * @param AbstractExpression $node + */ public function __construct(Node $node, int $lineno) { + trigger_deprecation('twig/twig', '3.16', \sprintf('The "%s" class is deprecated with no replacement.', static::class)); + parent::__construct(['node' => $node], [], $lineno); } public function compile(Compiler $compiler): void { $compiler - ->raw('print (') + ->raw('yield ') ->subcompile($this->getNode('node')) - ->raw(')') ; } } diff --git a/app/vendor/twig/twig/src/Node/Expression/MacroReferenceExpression.php b/app/vendor/twig/twig/src/Node/Expression/MacroReferenceExpression.php new file mode 100644 index 000000000..abe99aa35 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/MacroReferenceExpression.php @@ -0,0 +1,56 @@ + + */ +class MacroReferenceExpression extends AbstractExpression +{ + public function __construct(TemplateVariable $template, string $name, AbstractExpression $arguments, int $lineno) + { + parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno); + } + + public function compile(Compiler $compiler): void + { + if ($this->getAttribute('is_defined_test')) { + $compiler + ->subcompile($this->getNode('template')) + ->raw('->hasMacro(') + ->repr($this->getAttribute('name')) + ->raw(', $context') + ->raw(')') + ; + + return; + } + + $compiler + ->subcompile($this->getNode('template')) + ->raw('->getTemplateForMacro(') + ->repr($this->getAttribute('name')) + ->raw(', $context, ') + ->repr($this->getTemplateLine()) + ->raw(', $this->getSourceContext())') + ->raw(\sprintf('->%s', $this->getAttribute('name'))) + ->raw('(...') + ->subcompile($this->getNode('arguments')) + ->raw(')') + ; + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/MethodCallExpression.php b/app/vendor/twig/twig/src/Node/Expression/MethodCallExpression.php index d5ec0b6ef..922b98b10 100644 --- a/app/vendor/twig/twig/src/Node/Expression/MethodCallExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/MethodCallExpression.php @@ -12,14 +12,17 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Node\Expression\Variable\ContextVariable; class MethodCallExpression extends AbstractExpression { public function __construct(AbstractExpression $node, string $method, ArrayExpression $arguments, int $lineno) { + trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', __CLASS__, MacroReferenceExpression::class); + parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false, 'is_defined_test' => false], $lineno); - if ($node instanceof NameExpression) { + if ($node instanceof ContextVariable) { $node->setAttribute('always_defined', true); } } @@ -39,23 +42,13 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('twig_call_macro($macros[') + ->raw('CoreExtension::callMacro($macros[') ->repr($this->getNode('node')->getAttribute('name')) ->raw('], ') ->repr($this->getAttribute('method')) - ->raw(', [') - ; - $first = true; - foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) { - if (!$first) { - $compiler->raw(', '); - } - $first = false; - - $compiler->subcompile($pair['value']); - } - $compiler - ->raw('], ') + ->raw(', ') + ->subcompile($this->getNode('arguments')) + ->raw(', ') ->repr($this->getTemplateLine()) ->raw(', $context, $this->getSourceContext())'); } diff --git a/app/vendor/twig/twig/src/Node/Expression/NameExpression.php b/app/vendor/twig/twig/src/Node/Expression/NameExpression.php index c3563f012..2872ba413 100644 --- a/app/vendor/twig/twig/src/Node/Expression/NameExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/NameExpression.php @@ -13,6 +13,7 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Node\Expression\Variable\ContextVariable; class NameExpression extends AbstractExpression { @@ -24,6 +25,10 @@ class NameExpression extends AbstractExpression public function __construct(string $name, int $lineno) { + if (self::class === static::class) { + trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, ContextVariable::class); + } + parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false], $lineno); } @@ -34,7 +39,7 @@ public function compile(Compiler $compiler): void $compiler->addDebugInfo($this); if ($this->getAttribute('is_defined_test')) { - if ($this->isSpecial()) { + if (isset($this->specialVars[$name]) || $this->getAttribute('always_defined')) { $compiler->repr(true); } elseif (\PHP_VERSION_ID >= 70400) { $compiler @@ -51,7 +56,7 @@ public function compile(Compiler $compiler): void ->raw(', $context))') ; } - } elseif ($this->isSpecial()) { + } elseif (isset($this->specialVars[$name])) { $compiler->raw($this->specialVars[$name]); } elseif ($this->getAttribute('always_defined')) { $compiler @@ -85,13 +90,23 @@ public function compile(Compiler $compiler): void } } + /** + * @deprecated since Twig 3.11 (to be removed in 4.0) + */ public function isSpecial() { + trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__); + return isset($this->specialVars[$this->getAttribute('name')]); } + /** + * @deprecated since Twig 3.11 (to be removed in 4.0) + */ public function isSimple() { + trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__); + return !$this->isSpecial() && !$this->getAttribute('is_defined_test'); } } diff --git a/app/vendor/twig/twig/src/Node/Expression/NullCoalesceExpression.php b/app/vendor/twig/twig/src/Node/Expression/NullCoalesceExpression.php index a72bc4fc6..74ddaf791 100644 --- a/app/vendor/twig/twig/src/Node/Expression/NullCoalesceExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/NullCoalesceExpression.php @@ -12,22 +12,39 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Node\EmptyNode; use Twig\Node\Expression\Binary\AndBinary; +use Twig\Node\Expression\Binary\NullCoalesceBinary; use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Unary\NotUnary; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; +use Twig\TwigTest; class NullCoalesceExpression extends ConditionalExpression { + /** + * @param AbstractExpression $left + * @param AbstractExpression $right + */ public function __construct(Node $left, Node $right, int $lineno) { - $test = new DefinedTest(clone $left, 'defined', new Node(), $left->getTemplateLine()); + trigger_deprecation('twig/twig', '3.17', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, NullCoalesceBinary::class)); + + if (!$left instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($left)); + } + if (!$right instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($right)); + } + + $test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine()); // for "block()", we don't need the null test as the return value is always a string if (!$left instanceof BlockReferenceExpression) { $test = new AndBinary( $test, - new NotUnary(new NullTest($left, 'null', new Node(), $left->getTemplateLine()), $left->getTemplateLine()), + new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()), $left->getTemplateLine() ); } @@ -44,7 +61,7 @@ public function compile(Compiler $compiler): void * cases might be implemented as an optimizer node visitor, but has not been done * as benefits are probably not worth the added complexity. */ - if ($this->getNode('expr2') instanceof NameExpression) { + if ($this->getNode('expr2') instanceof ContextVariable) { $this->getNode('expr2')->setAttribute('always_defined', true); $compiler ->raw('((') diff --git a/app/vendor/twig/twig/src/Node/Expression/OperatorEscapeInterface.php b/app/vendor/twig/twig/src/Node/Expression/OperatorEscapeInterface.php new file mode 100644 index 000000000..06db6c616 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/OperatorEscapeInterface.php @@ -0,0 +1,25 @@ + 1. + * + * @author Fabien Potencier + */ +interface OperatorEscapeInterface +{ + /** + * @return string[] + */ + public function getOperandNamesToEscape(): array; +} diff --git a/app/vendor/twig/twig/src/Node/Expression/ParentExpression.php b/app/vendor/twig/twig/src/Node/Expression/ParentExpression.php index 254919718..22fe38f6a 100644 --- a/app/vendor/twig/twig/src/Node/Expression/ParentExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/ParentExpression.php @@ -21,9 +21,9 @@ */ class ParentExpression extends AbstractExpression { - public function __construct(string $name, int $lineno, string $tag = null) + public function __construct(string $name, int $lineno) { - parent::__construct([], ['output' => false, 'name' => $name], $lineno, $tag); + parent::__construct([], ['output' => false, 'name' => $name], $lineno); } public function compile(Compiler $compiler): void @@ -31,7 +31,7 @@ public function compile(Compiler $compiler): void if ($this->getAttribute('output')) { $compiler ->addDebugInfo($this) - ->write('$this->displayParentBlock(') + ->write('yield from $this->yieldParentBlock(') ->string($this->getAttribute('name')) ->raw(", \$context, \$blocks);\n") ; diff --git a/app/vendor/twig/twig/src/Node/Expression/TempNameExpression.php b/app/vendor/twig/twig/src/Node/Expression/TempNameExpression.php index 004c704a5..8cb66a193 100644 --- a/app/vendor/twig/twig/src/Node/Expression/TempNameExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/TempNameExpression.php @@ -12,20 +12,38 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Error\SyntaxError; class TempNameExpression extends AbstractExpression { - public function __construct(string $name, int $lineno) + public const RESERVED_NAMES = ['varargs', 'context', 'macros', 'blocks', 'this']; + + public function __construct(string|int|null $name, int $lineno) { + // All names supported by ExpressionParser::parsePrimaryExpression() should be excluded + if ($name && \in_array(strtolower($name), ['true', 'false', 'none', 'null'])) { + throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno); + } + + if (self::class === static::class) { + trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated.', self::class); + } + + if (null !== $name && (\is_int($name) || ctype_digit($name))) { + $name = (int) $name; + } elseif (\in_array($name, self::RESERVED_NAMES)) { + $name = "\u{035C}".$name; + } + parent::__construct([], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void { - $compiler - ->raw('$_') - ->raw($this->getAttribute('name')) - ->raw('_') - ; + if (null === $this->getAttribute('name')) { + $this->setAttribute('name', $compiler->getVarName()); + } + + $compiler->raw('$'.$this->getAttribute('name')); } } diff --git a/app/vendor/twig/twig/src/Node/Expression/Ternary/ConditionalTernary.php b/app/vendor/twig/twig/src/Node/Expression/Ternary/ConditionalTernary.php new file mode 100644 index 000000000..627da7a43 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/Ternary/ConditionalTernary.php @@ -0,0 +1,42 @@ + $test, 'left' => $left, 'right' => $right], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('((') + ->subcompile($this->getNode('test')) + ->raw(') ? (') + ->subcompile($this->getNode('left')) + ->raw(') : (') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function getOperandNamesToEscape(): array + { + return ['left', 'right']; + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/Test/ConstantTest.php b/app/vendor/twig/twig/src/Node/Expression/Test/ConstantTest.php index 57e9319d5..867fd0951 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Test/ConstantTest.php +++ b/app/vendor/twig/twig/src/Node/Expression/Test/ConstantTest.php @@ -33,16 +33,16 @@ public function compile(Compiler $compiler): void ->raw(' === constant(') ; - if ($this->getNode('arguments')->hasNode(1)) { + if ($this->getNode('arguments')->hasNode('1')) { $compiler ->raw('get_class(') - ->subcompile($this->getNode('arguments')->getNode(1)) + ->subcompile($this->getNode('arguments')->getNode('1')) ->raw(')."::".') ; } $compiler - ->subcompile($this->getNode('arguments')->getNode(0)) + ->subcompile($this->getNode('arguments')->getNode('0')) ->raw('))') ; } diff --git a/app/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php b/app/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php index 3953bbbe2..5e32c38bb 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php +++ b/app/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php @@ -11,17 +11,21 @@ namespace Twig\Node\Expression\Test; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; use Twig\Error\SyntaxError; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; +use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\MethodCallExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\TestExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; +use Twig\TwigTest; /** * Checks if a variable is defined in the current context. @@ -35,15 +39,25 @@ */ class DefinedTest extends TestExpression { - public function __construct(Node $node, string $name, ?Node $arguments, int $lineno) + /** + * @param AbstractExpression $node + */ + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, int $lineno) { - if ($node instanceof NameExpression) { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + } + + if ($node instanceof ContextVariable) { $node->setAttribute('is_defined_test', true); } elseif ($node instanceof GetAttrExpression) { $node->setAttribute('is_defined_test', true); $this->changeIgnoreStrictCheck($node); } elseif ($node instanceof BlockReferenceExpression) { $node->setAttribute('is_defined_test', true); + } elseif ($node instanceof MacroReferenceExpression) { + $node->setAttribute('is_defined_test', true); } elseif ($node instanceof FunctionExpression && 'constant' === $node->getAttribute('name')) { $node->setAttribute('is_defined_test', true); } elseif ($node instanceof ConstantExpression || $node instanceof ArrayExpression) { @@ -54,10 +68,14 @@ public function __construct(Node $node, string $name, ?Node $arguments, int $lin throw new SyntaxError('The "defined" test only works with simple variables.', $lineno); } + if (\is_string($name) && 'defined' !== $name) { + trigger_deprecation('twig/twig', '3.12', 'Creating a "DefinedTest" instance with a test name that is not "defined" is deprecated.'); + } + parent::__construct($node, $name, $arguments, $lineno); } - private function changeIgnoreStrictCheck(GetAttrExpression $node) + private function changeIgnoreStrictCheck(GetAttrExpression $node): void { $node->setAttribute('optimizable', false); $node->setAttribute('ignore_strict_check', true); diff --git a/app/vendor/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php b/app/vendor/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php index 4cb3ee096..90d58a49a 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php +++ b/app/vendor/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php @@ -29,7 +29,7 @@ public function compile(Compiler $compiler): void ->raw('(0 == ') ->subcompile($this->getNode('node')) ->raw(' % ') - ->subcompile($this->getNode('arguments')->getNode(0)) + ->subcompile($this->getNode('arguments')->getNode('0')) ->raw(')') ; } diff --git a/app/vendor/twig/twig/src/Node/Expression/Test/SameasTest.php b/app/vendor/twig/twig/src/Node/Expression/Test/SameasTest.php index c96d2bc01..f1e24db6f 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Test/SameasTest.php +++ b/app/vendor/twig/twig/src/Node/Expression/Test/SameasTest.php @@ -27,7 +27,7 @@ public function compile(Compiler $compiler): void ->raw('(') ->subcompile($this->getNode('node')) ->raw(' === ') - ->subcompile($this->getNode('arguments')->getNode(0)) + ->subcompile($this->getNode('arguments')->getNode('0')) ->raw(')') ; } diff --git a/app/vendor/twig/twig/src/Node/Expression/TestExpression.php b/app/vendor/twig/twig/src/Node/Expression/TestExpression.php index e518bd8f1..7b9a54138 100644 --- a/app/vendor/twig/twig/src/Node/Expression/TestExpression.php +++ b/app/vendor/twig/twig/src/Node/Expression/TestExpression.php @@ -11,31 +11,62 @@ namespace Twig\Node\Expression; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Node\NameDeprecation; use Twig\Node\Node; +use Twig\TwigTest; class TestExpression extends CallExpression { - public function __construct(Node $node, string $name, ?Node $arguments, int $lineno) + #[FirstClassTwigCallableReady] + /** + * @param AbstractExpression $node + */ + public function __construct(Node $node, string|TwigTest $test, ?Node $arguments, int $lineno) { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + } + $nodes = ['node' => $node]; if (null !== $arguments) { $nodes['arguments'] = $arguments; } - parent::__construct($nodes, ['name' => $name], $lineno); + if ($test instanceof TwigTest) { + $name = $test->getName(); + } else { + $name = $test; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigTest" when creating a "%s" test of type "%s" is deprecated.', $name, static::class); + } + + parent::__construct($nodes, ['name' => $name, 'type' => 'test'], $lineno); + + if ($test instanceof TwigTest) { + $this->setAttribute('twig_callable', $test); + } + + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); } public function compile(Compiler $compiler): void { $name = $this->getAttribute('name'); - $test = $compiler->getEnvironment()->getTest($name); + if ($this->hasAttribute('twig_callable')) { + $name = $this->getAttribute('twig_callable')->getName(); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.12', 'Changing the value of a "test" node in a NodeVisitor class is not supported anymore.'); + $this->removeAttribute('twig_callable'); + } + } - $this->setAttribute('name', $name); - $this->setAttribute('type', 'test'); - $this->setAttribute('arguments', $test->getArguments()); - $this->setAttribute('callable', $test->getCallable()); - $this->setAttribute('is_variadic', $test->isVariadic()); + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getTest($this->getAttribute('name'))); + } $this->compileCallable($compiler); } diff --git a/app/vendor/twig/twig/src/Node/Expression/Unary/AbstractUnary.php b/app/vendor/twig/twig/src/Node/Expression/Unary/AbstractUnary.php index e31e3f84b..b00027d1a 100644 --- a/app/vendor/twig/twig/src/Node/Expression/Unary/AbstractUnary.php +++ b/app/vendor/twig/twig/src/Node/Expression/Unary/AbstractUnary.php @@ -16,18 +16,32 @@ use Twig\Node\Expression\AbstractExpression; use Twig\Node\Node; -abstract class AbstractUnary extends AbstractExpression +abstract class AbstractUnary extends AbstractExpression implements UnaryInterface { + /** + * @param AbstractExpression $node + */ public function __construct(Node $node, int $lineno) { - parent::__construct(['node' => $node], [], $lineno); + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance argument to "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + } + + parent::__construct(['node' => $node], ['with_parentheses' => false], $lineno); } public function compile(Compiler $compiler): void { - $compiler->raw(' '); + if ($this->hasExplicitParentheses()) { + $compiler->raw('('); + } else { + $compiler->raw(' '); + } $this->operator($compiler); $compiler->subcompile($this->getNode('node')); + if ($this->hasExplicitParentheses()) { + $compiler->raw(')'); + } } abstract public function operator(Compiler $compiler): Compiler; diff --git a/app/vendor/twig/twig/src/Node/Expression/Unary/SpreadUnary.php b/app/vendor/twig/twig/src/Node/Expression/Unary/SpreadUnary.php new file mode 100644 index 000000000..f99072c25 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/Unary/SpreadUnary.php @@ -0,0 +1,22 @@ +raw('...'); + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/Unary/StringCastUnary.php b/app/vendor/twig/twig/src/Node/Expression/Unary/StringCastUnary.php new file mode 100644 index 000000000..87ea17ca8 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/Unary/StringCastUnary.php @@ -0,0 +1,22 @@ +raw('(string)'); + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/Unary/UnaryInterface.php b/app/vendor/twig/twig/src/Node/Expression/Unary/UnaryInterface.php new file mode 100644 index 000000000..b094ef4f4 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/Unary/UnaryInterface.php @@ -0,0 +1,22 @@ + $var], ['global' => $global], $var->getTemplateLine()); + } + + public function compile(Compiler $compiler): void + { + /** @var TemplateVariable $var */ + $var = $this->nodes['var']; + + $compiler + ->addDebugInfo($this) + ->write('$macros[') + ->string($var->getName($compiler)) + ->raw('] = ') + ; + + if ($this->getAttribute('global')) { + $compiler + ->raw('$this->macros[') + ->string($var->getName($compiler)) + ->raw('] = ') + ; + } + } +} diff --git a/app/vendor/twig/twig/src/Node/Expression/Variable/ContextVariable.php b/app/vendor/twig/twig/src/Node/Expression/Variable/ContextVariable.php new file mode 100644 index 000000000..01bbcb711 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Expression/Variable/ContextVariable.php @@ -0,0 +1,18 @@ +getAttribute('name')) { + $this->setAttribute('name', $compiler->getVarName()); + } + + return $this->getAttribute('name'); + } + + public function compile(Compiler $compiler): void + { + $name = $this->getName($compiler); + + if ('_self' === $name) { + $compiler->raw('$this'); + } else { + $compiler + ->raw('$macros[') + ->string($name) + ->raw(']') + ; + } + } +} diff --git a/app/vendor/twig/twig/src/Node/FlushNode.php b/app/vendor/twig/twig/src/Node/FlushNode.php index fa50a88ee..ff3bd1cf1 100644 --- a/app/vendor/twig/twig/src/Node/FlushNode.php +++ b/app/vendor/twig/twig/src/Node/FlushNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,18 +19,22 @@ * * @author Fabien Potencier */ +#[YieldReady] class FlushNode extends Node { - public function __construct(int $lineno, string $tag) + public function __construct(int $lineno) { - parent::__construct([], [], $lineno, $tag); + parent::__construct([], [], $lineno); } public function compile(Compiler $compiler): void { - $compiler - ->addDebugInfo($this) - ->write("flush();\n") - ; + $compiler->addDebugInfo($this); + + if ($compiler->getEnvironment()->useYield()) { + $compiler->write("yield '';\n"); + } + + $compiler->write("flush();\n"); } } diff --git a/app/vendor/twig/twig/src/Node/ForElseNode.php b/app/vendor/twig/twig/src/Node/ForElseNode.php new file mode 100644 index 000000000..56d6646bf --- /dev/null +++ b/app/vendor/twig/twig/src/Node/ForElseNode.php @@ -0,0 +1,41 @@ + + */ +#[YieldReady] +class ForElseNode extends Node +{ + public function __construct(Node $body, int $lineno) + { + parent::__construct(['body' => $body], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write("if (!\$context['_iterated']) {\n") + ->indent() + ->subcompile($this->getNode('body')) + ->outdent() + ->write("}\n") + ; + } +} diff --git a/app/vendor/twig/twig/src/Node/ForLoopNode.php b/app/vendor/twig/twig/src/Node/ForLoopNode.php index d5ce845a7..1f0a4f321 100644 --- a/app/vendor/twig/twig/src/Node/ForLoopNode.php +++ b/app/vendor/twig/twig/src/Node/ForLoopNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,11 +19,12 @@ * * @author Fabien Potencier */ +#[YieldReady] class ForLoopNode extends Node { - public function __construct(int $lineno, string $tag = null) + public function __construct(int $lineno) { - parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno, $tag); + parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno); } public function compile(Compiler $compiler): void @@ -36,7 +38,7 @@ public function compile(Compiler $compiler): void ->write("++\$context['loop']['index0'];\n") ->write("++\$context['loop']['index'];\n") ->write("\$context['loop']['first'] = false;\n") - ->write("if (isset(\$context['loop']['length'])) {\n") + ->write("if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) {\n") ->indent() ->write("--\$context['loop']['revindex0'];\n") ->write("--\$context['loop']['revindex'];\n") diff --git a/app/vendor/twig/twig/src/Node/ForNode.php b/app/vendor/twig/twig/src/Node/ForNode.php index 04addfbfe..2c86622d4 100644 --- a/app/vendor/twig/twig/src/Node/ForNode.php +++ b/app/vendor/twig/twig/src/Node/ForNode.php @@ -12,29 +12,41 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\AssignNameExpression; +use Twig\Node\Expression\Variable\AssignContextVariable; /** * Represents a for node. * * @author Fabien Potencier */ +#[YieldReady] class ForNode extends Node { private $loop; - public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno, string $tag = null) + public function __construct(AssignContextVariable $keyTarget, AssignContextVariable $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno) { - $body = new Node([$body, $this->loop = new ForLoopNode($lineno, $tag)]); + $body = new Nodes([$body, $this->loop = new ForLoopNode($lineno)]); + + if (null !== $ifexpr) { + trigger_deprecation('twig/twig', '3.19', \sprintf('Passing not-null to the "ifexpr" argument of the "%s" constructor is deprecated.', static::class)); + } + + if (null !== $else && !$else instanceof ForElseNode) { + trigger_deprecation('twig/twig', '3.19', \sprintf('Not passing an instance of "%s" to the "else" argument of the "%s" constructor is deprecated.', ForElseNode::class, static::class)); + + $else = new ForElseNode($else, $else->getTemplateLine()); + } $nodes = ['key_target' => $keyTarget, 'value_target' => $valueTarget, 'seq' => $seq, 'body' => $body]; if (null !== $else) { $nodes['else'] = $else; } - parent::__construct($nodes, ['with_loop' => true], $lineno, $tag); + parent::__construct($nodes, ['with_loop' => true], $lineno); } public function compile(Compiler $compiler): void @@ -42,7 +54,7 @@ public function compile(Compiler $compiler): void $compiler ->addDebugInfo($this) ->write("\$context['_parent'] = \$context;\n") - ->write("\$context['_seq'] = twig_ensure_traversable(") + ->write("\$context['_seq'] = CoreExtension::ensureTraversable(") ->subcompile($this->getNode('seq')) ->raw(");\n") ; @@ -87,19 +99,20 @@ public function compile(Compiler $compiler): void ; if ($this->hasNode('else')) { - $compiler - ->write("if (!\$context['_iterated']) {\n") - ->indent() - ->subcompile($this->getNode('else')) - ->outdent() - ->write("}\n") - ; + $compiler->subcompile($this->getNode('else')); } $compiler->write("\$_parent = \$context['_parent'];\n"); // remove some "private" loop variables (needed for nested loops) - $compiler->write('unset($context[\'_seq\'], $context[\'_iterated\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\'], $context[\'loop\']);'."\n"); + $compiler->write('unset($context[\'_seq\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\']'); + if ($this->hasNode('else')) { + $compiler->raw(', $context[\'_iterated\']'); + } + if ($this->getAttribute('with_loop')) { + $compiler->raw(', $context[\'loop\']'); + } + $compiler->raw(");\n"); // keep the values set in the inner context for variables defined in the outer context $compiler->write("\$context = array_intersect_key(\$context, \$_parent) + \$_parent;\n"); diff --git a/app/vendor/twig/twig/src/Node/IfNode.php b/app/vendor/twig/twig/src/Node/IfNode.php index 569ab7950..2af48fa81 100644 --- a/app/vendor/twig/twig/src/Node/IfNode.php +++ b/app/vendor/twig/twig/src/Node/IfNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,16 +20,17 @@ * * @author Fabien Potencier */ +#[YieldReady] class IfNode extends Node { - public function __construct(Node $tests, ?Node $else, int $lineno, string $tag = null) + public function __construct(Node $tests, ?Node $else, int $lineno) { $nodes = ['tests' => $tests]; if (null !== $else) { $nodes['else'] = $else; } - parent::__construct($nodes, [], $lineno, $tag); + parent::__construct($nodes, [], $lineno); } public function compile(Compiler $compiler): void @@ -47,13 +49,13 @@ public function compile(Compiler $compiler): void } $compiler - ->subcompile($this->getNode('tests')->getNode($i)) + ->subcompile($this->getNode('tests')->getNode((string) $i)) ->raw(") {\n") ->indent() ; // The node might not exists if the content is empty - if ($this->getNode('tests')->hasNode($i + 1)) { - $compiler->subcompile($this->getNode('tests')->getNode($i + 1)); + if ($this->getNode('tests')->hasNode((string) ($i + 1))) { + $compiler->subcompile($this->getNode('tests')->getNode((string) ($i + 1))); } } diff --git a/app/vendor/twig/twig/src/Node/ImportNode.php b/app/vendor/twig/twig/src/Node/ImportNode.php index 5378d799e..77a9af939 100644 --- a/app/vendor/twig/twig/src/Node/ImportNode.php +++ b/app/vendor/twig/twig/src/Node/ImportNode.php @@ -11,40 +11,40 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Variable\AssignTemplateVariable; +use Twig\Node\Expression\Variable\ContextVariable; /** * Represents an import node. * * @author Fabien Potencier */ +#[YieldReady] class ImportNode extends Node { - public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, string $tag = null, bool $global = true) + public function __construct(AbstractExpression $expr, AbstractExpression|AssignTemplateVariable $var, int $lineno) { - parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno, $tag); + if (\func_num_args() > 3) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Passing more than 3 arguments to "%s()" is deprecated.', __METHOD__)); + } + + if (!$var instanceof AssignTemplateVariable) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Passing a "%s" instance as the second argument of "%s" is deprecated, pass a "%s" instead.', $var::class, __CLASS__, AssignTemplateVariable::class)); + + $var = new AssignTemplateVariable($var->getAttribute('name'), $lineno); + } + + parent::__construct(['expr' => $expr, 'var' => $var], [], $lineno); } public function compile(Compiler $compiler): void { - $compiler - ->addDebugInfo($this) - ->write('$macros[') - ->repr($this->getNode('var')->getAttribute('name')) - ->raw('] = ') - ; - - if ($this->getAttribute('global')) { - $compiler - ->raw('$this->macros[') - ->repr($this->getNode('var')->getAttribute('name')) - ->raw('] = ') - ; - } + $compiler->subcompile($this->getNode('var')); - if ($this->getNode('expr') instanceof NameExpression && '_self' === $this->getNode('expr')->getAttribute('name')) { + if ($this->getNode('expr') instanceof ContextVariable && '_self' === $this->getNode('expr')->getAttribute('name')) { $compiler->raw('$this'); } else { $compiler diff --git a/app/vendor/twig/twig/src/Node/IncludeNode.php b/app/vendor/twig/twig/src/Node/IncludeNode.php index d540d6b23..4761cb832 100644 --- a/app/vendor/twig/twig/src/Node/IncludeNode.php +++ b/app/vendor/twig/twig/src/Node/IncludeNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -20,16 +21,17 @@ * * @author Fabien Potencier */ +#[YieldReady] class IncludeNode extends Node implements NodeOutputInterface { - public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null) + public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno) { $nodes = ['expr' => $expr]; if (null !== $variables) { $nodes['variables'] = $variables; } - parent::__construct($nodes, ['only' => $only, 'ignore_missing' => $ignoreMissing], $lineno, $tag); + parent::__construct($nodes, ['only' => $only, 'ignore_missing' => $ignoreMissing], $lineno); } public function compile(Compiler $compiler): void @@ -40,13 +42,12 @@ public function compile(Compiler $compiler): void $template = $compiler->getVarName(); $compiler - ->write(sprintf("$%s = null;\n", $template)) ->write("try {\n") ->indent() - ->write(sprintf('$%s = ', $template)) + ->write(\sprintf('$%s = ', $template)) ; - $this->addGetTemplate($compiler); + $this->addGetTemplate($compiler, $template); $compiler ->raw(";\n") @@ -54,12 +55,14 @@ public function compile(Compiler $compiler): void ->write("} catch (LoaderError \$e) {\n") ->indent() ->write("// ignore missing template\n") + ->write(\sprintf("\$$template = null;\n", $template)) ->outdent() ->write("}\n") - ->write(sprintf("if ($%s) {\n", $template)) + ->write(\sprintf("if ($%s) {\n", $template)) ->indent() - ->write(sprintf('$%s->display(', $template)) + ->write(\sprintf('yield from $%s->unwrap()->yield(', $template)) ; + $this->addTemplateArguments($compiler); $compiler ->raw(");\n") @@ -67,17 +70,21 @@ public function compile(Compiler $compiler): void ->write("}\n") ; } else { + $compiler->write('yield from '); $this->addGetTemplate($compiler); - $compiler->raw('->display('); + $compiler->raw('->unwrap()->yield('); $this->addTemplateArguments($compiler); $compiler->raw(");\n"); } } - protected function addGetTemplate(Compiler $compiler) + /** + * @return void + */ + protected function addGetTemplate(Compiler $compiler/* , string $template = '' */) { $compiler - ->write('$this->loadTemplate(') + ->raw('$this->loadTemplate(') ->subcompile($this->getNode('expr')) ->raw(', ') ->repr($this->getTemplateName()) @@ -87,18 +94,21 @@ protected function addGetTemplate(Compiler $compiler) ; } + /** + * @return void + */ protected function addTemplateArguments(Compiler $compiler) { if (!$this->hasNode('variables')) { $compiler->raw(false === $this->getAttribute('only') ? '$context' : '[]'); } elseif (false === $this->getAttribute('only')) { $compiler - ->raw('twig_array_merge($context, ') + ->raw('CoreExtension::merge($context, ') ->subcompile($this->getNode('variables')) ->raw(')') ; } else { - $compiler->raw('twig_to_array('); + $compiler->raw('CoreExtension::toArray('); $compiler->subcompile($this->getNode('variables')); $compiler->raw(')'); } diff --git a/app/vendor/twig/twig/src/Node/MacroNode.php b/app/vendor/twig/twig/src/Node/MacroNode.php index 7f1b24d53..db3ca458c 100644 --- a/app/vendor/twig/twig/src/Node/MacroNode.php +++ b/app/vendor/twig/twig/src/Node/MacroNode.php @@ -11,101 +11,109 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Error\SyntaxError; +use Twig\Node\Expression\ArrayExpression; +use Twig\Node\Expression\Variable\LocalVariable; /** * Represents a macro node. * * @author Fabien Potencier */ +#[YieldReady] class MacroNode extends Node { public const VARARGS_NAME = 'varargs'; - public function __construct(string $name, Node $body, Node $arguments, int $lineno, string $tag = null) + /** + * @param BodyNode $body + * @param ArrayExpression $arguments + */ + public function __construct(string $name, Node $body, Node $arguments, int $lineno) { - foreach ($arguments as $argumentName => $argument) { - if (self::VARARGS_NAME === $argumentName) { - throw new SyntaxError(sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext()); + if (!$body instanceof BodyNode) { + trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated ("%s" given).', BodyNode::class, static::class, $body::class)); + } + + if (!$arguments instanceof ArrayExpression) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class)); + + $args = new ArrayExpression([], $arguments->getTemplateLine()); + foreach ($arguments as $n => $default) { + $args->addElement($default, new LocalVariable($n, $default->getTemplateLine())); + } + $arguments = $args; + } + + foreach ($arguments->getKeyValuePairs() as $pair) { + if ("\u{035C}".self::VARARGS_NAME === $pair['key']->getAttribute('name')) { + throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $pair['value']->getTemplateLine(), $pair['value']->getSourceContext()); } } - parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno, $tag); + parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write(sprintf('public function macro_%s(', $this->getAttribute('name'))) + ->write(\sprintf('public function macro_%s(', $this->getAttribute('name'))) ; - $count = \count($this->getNode('arguments')); - $pos = 0; - foreach ($this->getNode('arguments') as $name => $default) { + /** @var ArrayExpression $arguments */ + $arguments = $this->getNode('arguments'); + foreach ($arguments->getKeyValuePairs() as $pair) { + $name = $pair['key']; + $default = $pair['value']; $compiler - ->raw('$__'.$name.'__ = ') + ->subcompile($name) + ->raw(' = ') ->subcompile($default) + ->raw(', ') ; - - if (++$pos < $count) { - $compiler->raw(', '); - } - } - - if ($count) { - $compiler->raw(', '); } $compiler - ->raw('...$__varargs__') - ->raw(")\n") + ->raw('...$varargs') + ->raw("): string|Markup\n") ->write("{\n") ->indent() ->write("\$macros = \$this->macros;\n") - ->write("\$context = \$this->env->mergeGlobals([\n") + ->write("\$context = [\n") ->indent() ; - foreach ($this->getNode('arguments') as $name => $default) { + foreach ($arguments->getKeyValuePairs() as $pair) { + $name = $pair['key']; + $var = $name->getAttribute('name'); + if (str_starts_with($var, "\u{035C}")) { + $var = substr($var, \strlen("\u{035C}")); + } $compiler ->write('') - ->string($name) - ->raw(' => $__'.$name.'__') + ->string($var) + ->raw(' => ') + ->subcompile($name) ->raw(",\n") ; } + $node = new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno); + $compiler ->write('') ->string(self::VARARGS_NAME) ->raw(' => ') - ; - - $compiler - ->raw("\$__varargs__,\n") + ->raw("\$varargs,\n") ->outdent() - ->write("]);\n\n") + ->write("] + \$this->env->getGlobals();\n\n") ->write("\$blocks = [];\n\n") - ; - if ($compiler->getEnvironment()->isDebug()) { - $compiler->write("ob_start();\n"); - } else { - $compiler->write("ob_start(function () { return ''; });\n"); - } - $compiler - ->write("try {\n") - ->indent() - ->subcompile($this->getNode('body')) + ->write('return ') + ->subcompile($node) ->raw("\n") - ->write("return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());\n") - ->outdent() - ->write("} finally {\n") - ->indent() - ->write("ob_end_clean();\n") - ->outdent() - ->write("}\n") ->outdent() ->write("}\n\n") ; diff --git a/app/vendor/twig/twig/src/Node/ModuleNode.php b/app/vendor/twig/twig/src/Node/ModuleNode.php index 9b485eeaf..b3f4e6c2a 100644 --- a/app/vendor/twig/twig/src/Node/ModuleNode.php +++ b/app/vendor/twig/twig/src/Node/ModuleNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -20,26 +21,34 @@ /** * Represents a module node. * - * Consider this class as being final. If you need to customize the behavior of - * the generated class, consider adding nodes to the following nodes: display_start, - * display_end, constructor_start, constructor_end, and class_end. + * If you need to customize the behavior of the generated class, add nodes to + * the following nodes: display_start, display_end, constructor_start, + * constructor_end, and class_end. * * @author Fabien Potencier */ +#[YieldReady] final class ModuleNode extends Node { + /** + * @param BodyNode $body + */ public function __construct(Node $body, ?AbstractExpression $parent, Node $blocks, Node $macros, Node $traits, $embeddedTemplates, Source $source) { + if (!$body instanceof BodyNode) { + trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); + } + $nodes = [ 'body' => $body, 'blocks' => $blocks, 'macros' => $macros, 'traits' => $traits, - 'display_start' => new Node(), - 'display_end' => new Node(), - 'constructor_start' => new Node(), - 'constructor_end' => new Node(), - 'class_end' => new Node(), + 'display_start' => new Nodes(), + 'display_end' => new Nodes(), + 'constructor_start' => new Nodes(), + 'constructor_end' => new Nodes(), + 'class_end' => new Nodes(), ]; if (null !== $parent) { $nodes['parent'] = $parent; @@ -55,6 +64,9 @@ public function __construct(Node $body, ?AbstractExpression $parent, Node $block $this->setSourceContext($source); } + /** + * @return void + */ public function setIndex($index) { $this->setAttribute('index', $index); @@ -69,6 +81,9 @@ public function compile(Compiler $compiler): void } } + /** + * @return void + */ protected function compileTemplate(Compiler $compiler) { if (!$this->getAttribute('index')) { @@ -98,6 +113,9 @@ protected function compileTemplate(Compiler $compiler) $this->compileClassFooter($compiler); } + /** + * @return void + */ protected function compileGetParent(Compiler $compiler) { if (!$this->hasNode('parent')) { @@ -106,7 +124,7 @@ protected function compileGetParent(Compiler $compiler) $parent = $this->getNode('parent'); $compiler - ->write("protected function doGetParent(array \$context)\n", "{\n") + ->write("protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper\n", "{\n") ->indent() ->addDebugInfo($parent) ->write('return ') @@ -133,6 +151,9 @@ protected function compileGetParent(Compiler $compiler) ; } + /** + * @return void + */ protected function compileClassHeader(Compiler $compiler) { $compiler @@ -143,6 +164,7 @@ protected function compileClassHeader(Compiler $compiler) ->write("use Twig\Environment;\n") ->write("use Twig\Error\LoaderError;\n") ->write("use Twig\Error\RuntimeError;\n") + ->write("use Twig\Extension\CoreExtension;\n") ->write("use Twig\Extension\SandboxExtension;\n") ->write("use Twig\Markup;\n") ->write("use Twig\Sandbox\SecurityError;\n") @@ -150,7 +172,9 @@ protected function compileClassHeader(Compiler $compiler) ->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n") ->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n") ->write("use Twig\Source;\n") - ->write("use Twig\Template;\n\n") + ->write("use Twig\Template;\n") + ->write("use Twig\TemplateWrapper;\n") + ->write("\n") ; } $compiler @@ -160,11 +184,17 @@ protected function compileClassHeader(Compiler $compiler) ->raw(" extends Template\n") ->write("{\n") ->indent() - ->write("private \$source;\n") - ->write("private \$macros = [];\n\n") + ->write("private Source \$source;\n") + ->write("/**\n") + ->write(" * @var array\n") + ->write(" */\n") + ->write("private array \$macros = [];\n\n") ; } + /** + * @return void + */ protected function compileConstructor(Compiler $compiler) { $compiler @@ -188,14 +218,14 @@ protected function compileConstructor(Compiler $compiler) $compiler ->addDebugInfo($node) - ->write(sprintf('$_trait_%s = $this->loadTemplate(', $i)) + ->write(\sprintf('$_trait_%s = $this->loadTemplate(', $i)) ->subcompile($node) ->raw(', ') ->repr($node->getTemplateName()) ->raw(', ') ->repr($node->getTemplateLine()) ->raw(");\n") - ->write(sprintf("if (!\$_trait_%s->isTraitable()) {\n", $i)) + ->write(\sprintf("if (!\$_trait_%s->unwrap()->isTraitable()) {\n", $i)) ->indent() ->write("throw new RuntimeError('Template \"'.") ->subcompile($trait->getNode('template')) @@ -204,12 +234,12 @@ protected function compileConstructor(Compiler $compiler) ->raw(", \$this->source);\n") ->outdent() ->write("}\n") - ->write(sprintf("\$_trait_%s_blocks = \$_trait_%s->getBlocks();\n\n", $i, $i)) + ->write(\sprintf("\$_trait_%s_blocks = \$_trait_%s->unwrap()->getBlocks();\n\n", $i, $i)) ; foreach ($trait->getNode('targets') as $key => $value) { $compiler - ->write(sprintf('if (!isset($_trait_%s_blocks[', $i)) + ->write(\sprintf('if (!isset($_trait_%s_blocks[', $i)) ->string($key) ->raw("])) {\n") ->indent() @@ -223,13 +253,17 @@ protected function compileConstructor(Compiler $compiler) ->outdent() ->write("}\n\n") - ->write(sprintf('$_trait_%s_blocks[', $i)) + ->write(\sprintf('$_trait_%s_blocks[', $i)) ->subcompile($value) - ->raw(sprintf('] = $_trait_%s_blocks[', $i)) + ->raw(\sprintf('] = $_trait_%s_blocks[', $i)) ->string($key) - ->raw(sprintf(']; unset($_trait_%s_blocks[', $i)) + ->raw(\sprintf(']; unset($_trait_%s_blocks[', $i)) ->string($key) - ->raw("]);\n\n") + ->raw(']); $this->traitAliases[') + ->subcompile($value) + ->raw('] = ') + ->string($key) + ->raw(";\n\n") ; } } @@ -242,7 +276,7 @@ protected function compileConstructor(Compiler $compiler) for ($i = 0; $i < $countTraits; ++$i) { $compiler - ->write(sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i)) + ->write(\sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i)) ; } @@ -275,7 +309,7 @@ protected function compileConstructor(Compiler $compiler) foreach ($this->getNode('blocks') as $name => $node) { $compiler - ->write(sprintf("'%s' => [\$this, 'block_%s'],\n", $name, $name)) + ->write(\sprintf("'%s' => [\$this, 'block_%s'],\n", $name, $name)) ; } @@ -300,10 +334,13 @@ protected function compileConstructor(Compiler $compiler) ; } + /** + * @return void + */ protected function compileDisplay(Compiler $compiler) { $compiler - ->write("protected function doDisplay(array \$context, array \$blocks = [])\n", "{\n") + ->write("protected function doDisplay(array \$context, array \$blocks = []): iterable\n", "{\n") ->indent() ->write("\$macros = \$this->macros;\n") ->subcompile($this->getNode('display_start')) @@ -324,20 +361,32 @@ protected function compileDisplay(Compiler $compiler) ->repr($parent->getTemplateLine()) ->raw(");\n") ; - $compiler->write('$this->parent'); + } + $compiler->write('yield from '); + + if ($parent instanceof ConstantExpression) { + $compiler->raw('$this->parent'); } else { - $compiler->write('$this->getParent($context)'); + $compiler->raw('$this->getParent($context)'); } - $compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n"); + $compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\n"); + } + + $compiler->subcompile($this->getNode('display_end')); + + if (!$this->hasNode('parent')) { + $compiler->write("yield from [];\n"); } $compiler - ->subcompile($this->getNode('display_end')) ->outdent() ->write("}\n\n") ; } + /** + * @return void + */ protected function compileClassFooter(Compiler $compiler) { $compiler @@ -347,18 +396,24 @@ protected function compileClassFooter(Compiler $compiler) ; } + /** + * @return void + */ protected function compileMacros(Compiler $compiler) { $compiler->subcompile($this->getNode('macros')); } + /** + * @return void + */ protected function compileGetTemplateName(Compiler $compiler) { $compiler ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function getTemplateName()\n", "{\n") + ->write("public function getTemplateName(): string\n", "{\n") ->indent() ->write('return ') ->repr($this->getSourceContext()->getName()) @@ -368,6 +423,9 @@ protected function compileGetTemplateName(Compiler $compiler) ; } + /** + * @return void + */ protected function compileIsTraitable(Compiler $compiler) { // A template can be used as a trait if: @@ -380,13 +438,13 @@ protected function compileIsTraitable(Compiler $compiler) $traitable = !$this->hasNode('parent') && 0 === \count($this->getNode('macros')); if ($traitable) { if ($this->getNode('body') instanceof BodyNode) { - $nodes = $this->getNode('body')->getNode(0); + $nodes = $this->getNode('body')->getNode('0'); } else { $nodes = $this->getNode('body'); } if (!\count($nodes)) { - $nodes = new Node([$nodes]); + $nodes = new Nodes([$nodes]); } foreach ($nodes as $node) { @@ -394,14 +452,6 @@ protected function compileIsTraitable(Compiler $compiler) continue; } - if ($node instanceof TextNode && ctype_space($node->getAttribute('data'))) { - continue; - } - - if ($node instanceof BlockReferenceNode) { - continue; - } - $traitable = false; break; } @@ -415,32 +465,38 @@ protected function compileIsTraitable(Compiler $compiler) ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function isTraitable()\n", "{\n") + ->write("public function isTraitable(): bool\n", "{\n") ->indent() - ->write(sprintf("return %s;\n", $traitable ? 'true' : 'false')) + ->write("return false;\n") ->outdent() ->write("}\n\n") ; } + /** + * @return void + */ protected function compileDebugInfo(Compiler $compiler) { $compiler ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function getDebugInfo()\n", "{\n") + ->write("public function getDebugInfo(): array\n", "{\n") ->indent() - ->write(sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) + ->write(\sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) ->outdent() ->write("}\n\n") ; } + /** + * @return void + */ protected function compileGetSourceContext(Compiler $compiler) { $compiler - ->write("public function getSourceContext()\n", "{\n") + ->write("public function getSourceContext(): Source\n", "{\n") ->indent() ->write('return new Source(') ->string($compiler->getEnvironment()->isDebug() ? $this->getSourceContext()->getCode() : '') @@ -453,21 +509,4 @@ protected function compileGetSourceContext(Compiler $compiler) ->write("}\n") ; } - - protected function compileLoadTemplate(Compiler $compiler, $node, $var) - { - if ($node instanceof ConstantExpression) { - $compiler - ->write(sprintf('%s = $this->loadTemplate(', $var)) - ->subcompile($node) - ->raw(', ') - ->repr($node->getTemplateName()) - ->raw(', ') - ->repr($node->getTemplateLine()) - ->raw(");\n") - ; - } else { - throw new \LogicException('Trait templates can only be constant nodes.'); - } - } } diff --git a/app/vendor/twig/twig/src/Node/NameDeprecation.php b/app/vendor/twig/twig/src/Node/NameDeprecation.php new file mode 100644 index 000000000..63ab28576 --- /dev/null +++ b/app/vendor/twig/twig/src/Node/NameDeprecation.php @@ -0,0 +1,46 @@ + + */ +class NameDeprecation +{ + private $package; + private $version; + private $newName; + + public function __construct(string $package = '', string $version = '', string $newName = '') + { + $this->package = $package; + $this->version = $version; + $this->newName = $newName; + } + + public function getPackage(): string + { + return $this->package; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getNewName(): string + { + return $this->newName; + } +} diff --git a/app/vendor/twig/twig/src/Node/Node.php b/app/vendor/twig/twig/src/Node/Node.php index 30659ae0f..389119b55 100644 --- a/app/vendor/twig/twig/src/Node/Node.php +++ b/app/vendor/twig/twig/src/Node/Node.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Source; @@ -19,61 +20,96 @@ * Represents a node in the AST. * * @author Fabien Potencier + * + * @implements \IteratorAggregate */ +#[YieldReady] class Node implements \Countable, \IteratorAggregate { + /** + * @var array + */ protected $nodes; protected $attributes; protected $lineno; protected $tag; private $sourceContext; + /** @var array */ + private $nodeNameDeprecations = []; + /** @var array */ + private $attributeNameDeprecations = []; /** - * @param array $nodes An array of named nodes - * @param array $attributes An array of attributes (should not be nodes) - * @param int $lineno The line number - * @param string $tag The tag name associated with the Node + * @param array $nodes An array of named nodes + * @param array $attributes An array of attributes (should not be nodes) + * @param int $lineno The line number */ - public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0, string $tag = null) + public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0) { + if (self::class === static::class) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Instantiating "%s" directly is deprecated; the class will become abstract in 4.0.', self::class)); + } + foreach ($nodes as $name => $node) { if (!$node instanceof self) { - throw new \InvalidArgumentException(sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, static::class)); + throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', get_debug_type($node), $name, static::class)); } } $this->nodes = $nodes; $this->attributes = $attributes; $this->lineno = $lineno; - $this->tag = $tag; + + if (\func_num_args() > 3) { + trigger_deprecation('twig/twig', '3.12', \sprintf('The "tag" constructor argument of the "%s" class is deprecated and ignored (check which TokenParser class set it to "%s"), the tag is now automatically set by the Parser when needed.', static::class, func_get_arg(3) ?: 'null')); + } } - public function __toString() + public function __toString(): string { + $repr = static::class; + + if ($this->tag) { + $repr .= \sprintf("\n tag: %s", $this->tag); + } + $attributes = []; foreach ($this->attributes as $name => $value) { - $attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); + if (\is_callable($value)) { + $v = '\Closure'; + } elseif ($value instanceof \Stringable) { + $v = (string) $value; + } else { + $v = str_replace("\n", '', var_export($value, true)); + } + $attributes[] = \sprintf('%s: %s', $name, $v); } - $repr = [static::class.'('.implode(', ', $attributes)]; + if ($attributes) { + $repr .= \sprintf("\n attributes:\n %s", implode("\n ", $attributes)); + } if (\count($this->nodes)) { + $repr .= "\n nodes:"; foreach ($this->nodes as $name => $node) { - $len = \strlen($name) + 4; + $len = \strlen($name) + 6; $noderepr = []; foreach (explode("\n", (string) $node) as $line) { $noderepr[] = str_repeat(' ', $len).$line; } - $repr[] = sprintf(' %s: %s', $name, ltrim(implode("\n", $noderepr))); + $repr .= \sprintf("\n %s: %s", $name, ltrim(implode("\n", $noderepr))); } - - $repr[] = ')'; - } else { - $repr[0] .= ')'; } - return implode("\n", $repr); + return $repr; + } + + public function __clone() + { + foreach ($this->nodes as $name => $node) { + $this->nodes[$name] = clone $node; + } } /** @@ -82,7 +118,7 @@ public function __toString() public function compile(Compiler $compiler) { foreach ($this->nodes as $node) { - $node->compile($compiler); + $compiler->subcompile($node); } } @@ -96,15 +132,40 @@ public function getNodeTag(): ?string return $this->tag; } + /** + * @internal + */ + public function setNodeTag(string $tag): void + { + if ($this->tag) { + throw new \LogicException('The tag of a node can only be set once.'); + } + + $this->tag = $tag; + } + public function hasAttribute(string $name): bool { return \array_key_exists($name, $this->attributes); } + /** + * @return mixed + */ public function getAttribute(string $name) { if (!\array_key_exists($name, $this->attributes)) { - throw new \LogicException(sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class)); + throw new \LogicException(\sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class)); + } + + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; + if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) { + $dep = $this->attributeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated, get the "%s" attribute instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated.', $name, static::class); + } } return $this->attributes[$name]; @@ -112,38 +173,96 @@ public function getAttribute(string $name) public function setAttribute(string $name, $value): void { + $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true; + if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) { + $dep = $this->attributeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated, set the "%s" attribute instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + $this->attributes[$name] = $value; } + public function deprecateAttribute(string $name, NameDeprecation $dep): void + { + $this->attributeNameDeprecations[$name] = $dep; + } + public function removeAttribute(string $name): void { unset($this->attributes[$name]); } + /** + * @param string|int $name + */ public function hasNode(string $name): bool { return isset($this->nodes[$name]); } + /** + * @param string|int $name + */ public function getNode(string $name): self { if (!isset($this->nodes[$name])) { - throw new \LogicException(sprintf('Node "%s" does not exist for Node "%s".', $name, static::class)); + throw new \LogicException(\sprintf('Node "%s" does not exist for Node "%s".', $name, static::class)); + } + + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; + if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) { + $dep = $this->nodeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated, get the "%s" node instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated.', $name, static::class); + } } return $this->nodes[$name]; } + /** + * @param string|int $name + */ public function setNode(string $name, self $node): void { + $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true; + if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) { + $dep = $this->nodeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated, set the "%s" node instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + + if (null !== $this->sourceContext) { + $node->setSourceContext($this->sourceContext); + } $this->nodes[$name] = $node; } + /** + * @param string|int $name + */ public function removeNode(string $name): void { unset($this->nodes[$name]); } + /** + * @param string|int $name + */ + public function deprecateNode(string $name, NameDeprecation $dep): void + { + $this->nodeNameDeprecations[$name] = $dep; + } + /** * @return int */ diff --git a/app/vendor/twig/twig/src/Node/Nodes.php b/app/vendor/twig/twig/src/Node/Nodes.php new file mode 100644 index 000000000..bd67053ab --- /dev/null +++ b/app/vendor/twig/twig/src/Node/Nodes.php @@ -0,0 +1,28 @@ + + */ +#[YieldReady] +final class Nodes extends Node +{ + public function __construct(array $nodes = [], int $lineno = 0) + { + parent::__construct($nodes, [], $lineno); + } +} diff --git a/app/vendor/twig/twig/src/Node/PrintNode.php b/app/vendor/twig/twig/src/Node/PrintNode.php index 60386d299..e3c23bbfa 100644 --- a/app/vendor/twig/twig/src/Node/PrintNode.php +++ b/app/vendor/twig/twig/src/Node/PrintNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -20,19 +21,23 @@ * * @author Fabien Potencier */ +#[YieldReady] class PrintNode extends Node implements NodeOutputInterface { - public function __construct(AbstractExpression $expr, int $lineno, string $tag = null) + public function __construct(AbstractExpression $expr, int $lineno) { - parent::__construct(['expr' => $expr], [], $lineno, $tag); + parent::__construct(['expr' => $expr], [], $lineno); } public function compile(Compiler $compiler): void { + /** @var AbstractExpression */ + $expr = $this->getNode('expr'); + $compiler ->addDebugInfo($this) - ->write('echo ') - ->subcompile($this->getNode('expr')) + ->write($expr->isGenerator() ? 'yield from ' : 'yield ') + ->subcompile($expr) ->raw(";\n") ; } diff --git a/app/vendor/twig/twig/src/Node/SandboxNode.php b/app/vendor/twig/twig/src/Node/SandboxNode.php index 4d5666bff..d51cea44b 100644 --- a/app/vendor/twig/twig/src/Node/SandboxNode.php +++ b/app/vendor/twig/twig/src/Node/SandboxNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,11 +19,12 @@ * * @author Fabien Potencier */ +#[YieldReady] class SandboxNode extends Node { - public function __construct(Node $body, int $lineno, string $tag = null) + public function __construct(Node $body, int $lineno) { - parent::__construct(['body' => $body], [], $lineno, $tag); + parent::__construct(['body' => $body], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/app/vendor/twig/twig/src/Node/SetNode.php b/app/vendor/twig/twig/src/Node/SetNode.php index 96b6bd8bf..4d97adb22 100644 --- a/app/vendor/twig/twig/src/Node/SetNode.php +++ b/app/vendor/twig/twig/src/Node/SetNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\ConstantExpression; @@ -19,26 +20,35 @@ * * @author Fabien Potencier */ +#[YieldReady] class SetNode extends Node implements NodeCaptureInterface { - public function __construct(bool $capture, Node $names, Node $values, int $lineno, string $tag = null) + public function __construct(bool $capture, Node $names, Node $values, int $lineno) { - parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => false], $lineno, $tag); - /* * Optimizes the node when capture is used for a large block of text. * * {% set foo %}foo{% endset %} is compiled to $context['foo'] = new Twig\Markup("foo"); */ - if ($this->getAttribute('capture')) { - $this->setAttribute('safe', true); - - $values = $this->getNode('values'); - if ($values instanceof TextNode) { - $this->setNode('values', new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine())); - $this->setAttribute('capture', false); + $safe = false; + if ($capture) { + $safe = true; + // Node::class === get_class($values) should be removed in Twig 4.0 + if (($values instanceof Nodes || Node::class === \get_class($values)) && !\count($values)) { + $values = new ConstantExpression('', $values->getTemplateLine()); + $capture = false; + } elseif ($values instanceof TextNode) { + $values = new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine()); + $capture = false; + } elseif ($values instanceof PrintNode && $values->getNode('expr') instanceof ConstantExpression) { + $values = $values->getNode('expr'); + $capture = false; + } else { + $values = new CaptureNode($values, $values->getTemplateLine()); } } + + parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => $safe], $lineno); } public function compile(Compiler $compiler): void @@ -46,7 +56,7 @@ public function compile(Compiler $compiler): void $compiler->addDebugInfo($this); if (\count($this->getNode('names')) > 1) { - $compiler->write('list('); + $compiler->write('['); foreach ($this->getNode('names') as $idx => $node) { if ($idx) { $compiler->raw(', '); @@ -54,29 +64,15 @@ public function compile(Compiler $compiler): void $compiler->subcompile($node); } - $compiler->raw(')'); + $compiler->raw(']'); } else { - if ($this->getAttribute('capture')) { - if ($compiler->getEnvironment()->isDebug()) { - $compiler->write("ob_start();\n"); - } else { - $compiler->write("ob_start(function () { return ''; });\n"); - } - $compiler - ->subcompile($this->getNode('values')) - ; - } - $compiler->subcompile($this->getNode('names'), false); - - if ($this->getAttribute('capture')) { - $compiler->raw(" = ('' === \$tmp = ob_get_clean()) ? '' : new Markup(\$tmp, \$this->env->getCharset())"); - } } + $compiler->raw(' = '); - if (!$this->getAttribute('capture')) { - $compiler->raw(' = '); - + if ($this->getAttribute('capture')) { + $compiler->subcompile($this->getNode('values')); + } else { if (\count($this->getNode('names')) > 1) { $compiler->write('['); foreach ($this->getNode('values') as $idx => $value) { @@ -89,17 +85,31 @@ public function compile(Compiler $compiler): void $compiler->raw(']'); } else { if ($this->getAttribute('safe')) { - $compiler - ->raw("('' === \$tmp = ") - ->subcompile($this->getNode('values')) - ->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())") - ; + if ($this->getNode('values') instanceof ConstantExpression) { + if ('' === $this->getNode('values')->getAttribute('value')) { + $compiler->raw('""'); + } else { + $compiler + ->raw('new Markup(') + ->subcompile($this->getNode('values')) + ->raw(', $this->env->getCharset())') + ; + } + } else { + $compiler + ->raw("('' === \$tmp = ") + ->subcompile($this->getNode('values')) + ->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())") + ; + } } else { $compiler->subcompile($this->getNode('values')); } } + + $compiler->raw(';'); } - $compiler->raw(";\n"); + $compiler->raw("\n"); } } diff --git a/app/vendor/twig/twig/src/Node/TextNode.php b/app/vendor/twig/twig/src/Node/TextNode.php index d74ebe630..fae65fb2c 100644 --- a/app/vendor/twig/twig/src/Node/TextNode.php +++ b/app/vendor/twig/twig/src/Node/TextNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class TextNode extends Node implements NodeOutputInterface { public function __construct(string $data, int $lineno) @@ -28,9 +30,10 @@ public function __construct(string $data, int $lineno) public function compile(Compiler $compiler): void { + $compiler->addDebugInfo($this); + $compiler - ->addDebugInfo($this) - ->write('echo ') + ->write('yield ') ->string($this->getAttribute('data')) ->raw(";\n") ; diff --git a/app/vendor/twig/twig/src/Node/TypesNode.php b/app/vendor/twig/twig/src/Node/TypesNode.php new file mode 100644 index 000000000..b5949848d --- /dev/null +++ b/app/vendor/twig/twig/src/Node/TypesNode.php @@ -0,0 +1,31 @@ + + */ +#[YieldReady] +class TypesNode extends Node +{ + /** + * @param array $types + */ + public function __construct(array $types, int $lineno) + { + parent::__construct([], ['mapping' => $types], $lineno); + } + + /** + * @return void + */ + public function compile(Compiler $compiler) + { + // Don't compile anything. + } +} diff --git a/app/vendor/twig/twig/src/Node/WithNode.php b/app/vendor/twig/twig/src/Node/WithNode.php index 2ac9123d0..487e2800b 100644 --- a/app/vendor/twig/twig/src/Node/WithNode.php +++ b/app/vendor/twig/twig/src/Node/WithNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,16 +19,17 @@ * * @author Fabien Potencier */ +#[YieldReady] class WithNode extends Node { - public function __construct(Node $body, ?Node $variables, bool $only, int $lineno, string $tag = null) + public function __construct(Node $body, ?Node $variables, bool $only, int $lineno) { $nodes = ['body' => $body]; if (null !== $variables) { $nodes['variables'] = $variables; } - parent::__construct($nodes, ['only' => $only], $lineno, $tag); + parent::__construct($nodes, ['only' => $only], $lineno); } public function compile(Compiler $compiler): void @@ -36,35 +38,35 @@ public function compile(Compiler $compiler): void $parentContextName = $compiler->getVarName(); - $compiler->write(sprintf("\$%s = \$context;\n", $parentContextName)); + $compiler->write(\sprintf("\$%s = \$context;\n", $parentContextName)); if ($this->hasNode('variables')) { $node = $this->getNode('variables'); $varsName = $compiler->getVarName(); $compiler - ->write(sprintf('$%s = ', $varsName)) + ->write(\sprintf('$%s = ', $varsName)) ->subcompile($node) ->raw(";\n") - ->write(sprintf("if (!is_iterable(\$%s)) {\n", $varsName)) + ->write(\sprintf("if (!is_iterable(\$%s)) {\n", $varsName)) ->indent() - ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a hash.', ") + ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a mapping.', ") ->repr($node->getTemplateLine()) ->raw(", \$this->getSourceContext());\n") ->outdent() ->write("}\n") - ->write(sprintf("\$%s = twig_to_array(\$%s);\n", $varsName, $varsName)) + ->write(\sprintf("\$%s = CoreExtension::toArray(\$%s);\n", $varsName, $varsName)) ; if ($this->getAttribute('only')) { $compiler->write("\$context = [];\n"); } - $compiler->write(sprintf("\$context = \$this->env->mergeGlobals(array_merge(\$context, \$%s));\n", $varsName)); + $compiler->write(\sprintf("\$context = \$%s + \$context + \$this->env->getGlobals();\n", $varsName)); } $compiler ->subcompile($this->getNode('body')) - ->write(sprintf("\$context = \$%s;\n", $parentContextName)) + ->write(\sprintf("\$context = \$%s;\n", $parentContextName)) ; } } diff --git a/app/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php b/app/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php index d7036ae55..38b1ec9d0 100644 --- a/app/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php +++ b/app/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php @@ -17,9 +17,9 @@ /** * Used to make node visitors compatible with Twig 1.x and 2.x. * - * To be removed in Twig 3.1. - * * @author Fabien Potencier + * + * @deprecated since Twig 3.9 (to be removed in 4.0) */ abstract class AbstractNodeVisitor implements NodeVisitorInterface { diff --git a/app/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php b/app/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php index c390d7cc7..b35ae8817 100644 --- a/app/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php +++ b/app/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php @@ -16,14 +16,14 @@ use Twig\Node\AutoEscapeNode; use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; -use Twig\Node\DoNode; -use Twig\Node\Expression\ConditionalExpression; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; -use Twig\Node\Expression\InlinePrint; +use Twig\Node\Expression\OperatorEscapeInterface; use Twig\Node\ImportNode; use Twig\Node\ModuleNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\NodeTraverser; @@ -59,7 +59,7 @@ public function enterNode(Node $node, Environment $env): Node } elseif ($node instanceof BlockNode) { $this->statusStack[] = $this->blocks[$node->getAttribute('name')] ?? $this->needEscaping(); } elseif ($node instanceof ImportNode) { - $this->safeVars[] = $node->getNode('var')->getAttribute('name'); + $this->safeVars[] = $node->getNode('var')->getNode('var')->getAttribute('name'); } return $node; @@ -75,11 +75,13 @@ public function leaveNode(Node $node, Environment $env): ?Node return $this->preEscapeFilterNode($node, $env); } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) { $expression = $node->getNode('expr'); - if ($expression instanceof ConditionalExpression && $this->shouldUnwrapConditional($expression, $env, $type)) { - return new DoNode($this->unwrapConditional($expression, $env, $type), $expression->getTemplateLine()); + if ($expression instanceof OperatorEscapeInterface) { + $this->escapeConditional($expression, $env, $type); + } else { + $node->setNode('expr', $this->escapeExpression($expression, $env, $type)); } - return $this->escapePrintNode($node, $env, $type); + return $node; } if ($node instanceof AutoEscapeNode || $node instanceof BlockNode) { @@ -91,85 +93,57 @@ public function leaveNode(Node $node, Environment $env): ?Node return $node; } - private function shouldUnwrapConditional(ConditionalExpression $expression, Environment $env, string $type): bool - { - $expr2Safe = $this->isSafeFor($type, $expression->getNode('expr2'), $env); - $expr3Safe = $this->isSafeFor($type, $expression->getNode('expr3'), $env); - - return $expr2Safe !== $expr3Safe; - } - - private function unwrapConditional(ConditionalExpression $expression, Environment $env, string $type): ConditionalExpression - { - // convert "echo a ? b : c" to "a ? echo b : echo c" recursively - $expr2 = $expression->getNode('expr2'); - if ($expr2 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr2, $env, $type)) { - $expr2 = $this->unwrapConditional($expr2, $env, $type); - } else { - $expr2 = $this->escapeInlinePrintNode(new InlinePrint($expr2, $expr2->getTemplateLine()), $env, $type); - } - $expr3 = $expression->getNode('expr3'); - if ($expr3 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr3, $env, $type)) { - $expr3 = $this->unwrapConditional($expr3, $env, $type); - } else { - $expr3 = $this->escapeInlinePrintNode(new InlinePrint($expr3, $expr3->getTemplateLine()), $env, $type); - } - - return new ConditionalExpression($expression->getNode('expr1'), $expr2, $expr3, $expression->getTemplateLine()); - } - - private function escapeInlinePrintNode(InlinePrint $node, Environment $env, string $type): Node + /** + * @param AbstractExpression&OperatorEscapeInterface $expression + */ + private function escapeConditional($expression, Environment $env, string $type): void { - $expression = $node->getNode('node'); - - if ($this->isSafeFor($type, $expression, $env)) { - return $node; + foreach ($expression->getOperandNamesToEscape() as $name) { + /** @var AbstractExpression $operand */ + $operand = $expression->getNode($name); + if ($operand instanceof OperatorEscapeInterface) { + $this->escapeConditional($operand, $env, $type); + } else { + $expression->setNode($name, $this->escapeExpression($operand, $env, $type)); + } } - - return new InlinePrint($this->getEscaperFilter($type, $expression), $node->getTemplateLine()); } - private function escapePrintNode(PrintNode $node, Environment $env, string $type): Node + private function escapeExpression(AbstractExpression $expression, Environment $env, string $type): AbstractExpression { - if (false === $type) { - return $node; - } - - $expression = $node->getNode('expr'); - - if ($this->isSafeFor($type, $expression, $env)) { - return $node; - } - - $class = \get_class($node); - - return new $class($this->getEscaperFilter($type, $expression), $node->getTemplateLine()); + return $this->isSafeFor($type, $expression, $env) ? $expression : $this->getEscaperFilter($env, $type, $expression); } private function preEscapeFilterNode(FilterExpression $filter, Environment $env): FilterExpression { - $name = $filter->getNode('filter')->getAttribute('value'); + if ($filter->hasAttribute('twig_callable')) { + $type = $filter->getAttribute('twig_callable')->getPreEscape(); + } else { + // legacy + $name = $filter->getNode('filter', false)->getAttribute('value'); + $type = $env->getFilter($name)->getPreEscape(); + } - $type = $env->getFilter($name)->getPreEscape(); if (null === $type) { return $filter; } + /** @var AbstractExpression $node */ $node = $filter->getNode('node'); if ($this->isSafeFor($type, $node, $env)) { return $filter; } - $filter->setNode('node', $this->getEscaperFilter($type, $node)); + $filter->setNode('node', $this->getEscaperFilter($env, $type, $node)); return $filter; } - private function isSafeFor(string $type, Node $expression, Environment $env): bool + private function isSafeFor(string $type, AbstractExpression $expression, Environment $env): bool { $safe = $this->safeAnalysis->getSafe($expression); - if (null === $safe) { + if (!$safe) { if (null === $this->traverser) { $this->traverser = new NodeTraverser($env, [$this->safeAnalysis]); } @@ -183,7 +157,10 @@ private function isSafeFor(string $type, Node $expression, Environment $env): bo return \in_array($type, $safe) || \in_array('all', $safe); } - private function needEscaping() + /** + * @return string|false + */ + private function needEscaping(): string|bool { if (\count($this->statusStack)) { return $this->statusStack[\count($this->statusStack) - 1]; @@ -192,13 +169,13 @@ private function needEscaping() return $this->defaultStrategy ?: false; } - private function getEscaperFilter(string $type, Node $node): FilterExpression + private function getEscaperFilter(Environment $env, string $type, AbstractExpression $node): FilterExpression { $line = $node->getTemplateLine(); - $name = new ConstantExpression('escape', $line); - $args = new Node([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]); + $filter = $env->getFilter('escape'); + $args = new Nodes([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]); - return new FilterExpression($node, $name, $args, $line); + return new FilterExpression($node, $filter, $args, $line); } public function getPriority(): int diff --git a/app/vendor/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php b/app/vendor/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php deleted file mode 100644 index d6a7781ba..000000000 --- a/app/vendor/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * @internal - */ -final class MacroAutoImportNodeVisitor implements NodeVisitorInterface -{ - private $inAModule = false; - private $hasMacroCalls = false; - - public function enterNode(Node $node, Environment $env): Node - { - if ($node instanceof ModuleNode) { - $this->inAModule = true; - $this->hasMacroCalls = false; - } - - return $node; - } - - public function leaveNode(Node $node, Environment $env): Node - { - if ($node instanceof ModuleNode) { - $this->inAModule = false; - if ($this->hasMacroCalls) { - $node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0, 'import', true)); - } - } elseif ($this->inAModule) { - if ( - $node instanceof GetAttrExpression - && $node->getNode('node') instanceof NameExpression - && '_self' === $node->getNode('node')->getAttribute('name') - && $node->getNode('attribute') instanceof ConstantExpression - ) { - $this->hasMacroCalls = true; - - $name = $node->getNode('attribute')->getAttribute('value'); - $node = new MethodCallExpression($node->getNode('node'), 'macro_'.$name, $node->getNode('arguments'), $node->getTemplateLine()); - $node->setAttribute('safe', true); - } - } - - return $node; - } - - public function getPriority(): int - { - // we must be ran before auto-escaping - return -10; - } -} diff --git a/app/vendor/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php b/app/vendor/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php index 6b39f0094..9283737f5 100644 --- a/app/vendor/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/app/vendor/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php @@ -15,15 +15,15 @@ use Twig\Node\BlockReferenceNode; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\ParentExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\ForNode; use Twig\Node\IncludeNode; use Twig\Node\Node; use Twig\Node\PrintNode; +use Twig\Node\TextNode; /** * Tries to optimize the AST. @@ -43,21 +43,28 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface public const OPTIMIZE_NONE = 0; public const OPTIMIZE_FOR = 2; public const OPTIMIZE_RAW_FILTER = 4; + public const OPTIMIZE_TEXT_NODES = 8; private $loops = []; private $loopsTargets = []; - private $optimizers; /** * @param int $optimizers The optimizer mode */ - public function __construct(int $optimizers = -1) - { - if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER)) { - throw new \InvalidArgumentException(sprintf('Optimizer mode "%s" is not valid.', $optimizers)); + public function __construct( + private int $optimizers = -1, + ) { + if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_TEXT_NODES)) { + throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers)); + } + + if (-1 !== $optimizers && self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $optimizers)) { + trigger_deprecation('twig/twig', '3.11', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER" option is deprecated and does nothing.'); } - $this->optimizers = $optimizers; + if (-1 !== $optimizers && self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $optimizers)) { + trigger_deprecation('twig/twig', '3.12', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES" option is deprecated and does nothing.'); + } } public function enterNode(Node $node, Environment $env): Node @@ -75,10 +82,6 @@ public function leaveNode(Node $node, Environment $env): ?Node $this->leaveOptimizeFor($node); } - if (self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $this->optimizers)) { - $node = $this->optimizeRawFilter($node); - } - $node = $this->optimizePrintNode($node); return $node; @@ -98,6 +101,11 @@ private function optimizePrintNode(Node $node): Node } $exprNode = $node->getNode('expr'); + + if ($exprNode instanceof ConstantExpression && \is_string($exprNode->getAttribute('value'))) { + return new TextNode($exprNode->getAttribute('value'), $exprNode->getTemplateLine()); + } + if ( $exprNode instanceof BlockReferenceExpression || $exprNode instanceof ParentExpression @@ -110,18 +118,6 @@ private function optimizePrintNode(Node $node): Node return $node; } - /** - * Removes "raw" filters. - */ - private function optimizeRawFilter(Node $node): Node - { - if ($node instanceof FilterExpression && 'raw' == $node->getNode('filter')->getAttribute('value')) { - return $node->getNode('node'); - } - - return $node; - } - /** * Optimizes "for" tag by removing the "loop" variable creation whenever possible. */ @@ -141,13 +137,13 @@ private function enterOptimizeFor(Node $node): void // when do we need to add the loop variable back? // the loop variable is referenced for the current loop - elseif ($node instanceof NameExpression && 'loop' === $node->getAttribute('name')) { + elseif ($node instanceof ContextVariable && 'loop' === $node->getAttribute('name')) { $node->setAttribute('always_defined', true); $this->addLoopToCurrent(); } // optimize access to loop targets - elseif ($node instanceof NameExpression && \in_array($node->getAttribute('name'), $this->loopsTargets)) { + elseif ($node instanceof ContextVariable && \in_array($node->getAttribute('name'), $this->loopsTargets)) { $node->setAttribute('always_defined', true); } @@ -177,7 +173,7 @@ private function enterOptimizeFor(Node $node): void || 'parent' === $node->getNode('attribute')->getAttribute('value') ) && (true === $this->loops[0]->getAttribute('with_loop') - || ($node->getNode('node') instanceof NameExpression + || ($node->getNode('node') instanceof ContextVariable && 'loop' === $node->getNode('node')->getAttribute('name') ) ) diff --git a/app/vendor/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/app/vendor/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php index 90d6f2e0f..a5361fbf7 100644 --- a/app/vendor/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/app/vendor/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -13,14 +13,15 @@ use Twig\Environment; use Twig\Node\Expression\BlockReferenceExpression; -use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; +use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\MethodCallExpression; -use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\OperatorEscapeInterface; use Twig\Node\Expression\ParentExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; /** @@ -36,11 +37,14 @@ public function setSafeVars(array $safeVars): void $this->safeVars = $safeVars; } + /** + * @return array + */ public function getSafe(Node $node) { - $hash = spl_object_hash($node); + $hash = spl_object_id($node); if (!isset($this->data[$hash])) { - return; + return []; } foreach ($this->data[$hash] as $bucket) { @@ -54,11 +58,13 @@ public function getSafe(Node $node) return $bucket['value']; } + + return []; } private function setSafe(Node $node, array $safe): void { - $hash = spl_object_hash($node); + $hash = spl_object_id($node); if (isset($this->data[$hash])) { foreach ($this->data[$hash] as &$bucket) { if ($bucket['key'] === $node) { @@ -90,55 +96,69 @@ public function leaveNode(Node $node, Environment $env): ?Node } elseif ($node instanceof ParentExpression) { // parent block is safe by definition $this->setSafe($node, ['all']); - } elseif ($node instanceof ConditionalExpression) { - // intersect safeness of both operands - $safe = $this->intersectSafe($this->getSafe($node->getNode('expr2')), $this->getSafe($node->getNode('expr3'))); - $this->setSafe($node, $safe); + } elseif ($node instanceof OperatorEscapeInterface) { + // intersect safeness of operands + $operands = $node->getOperandNamesToEscape(); + if (2 < \count($operands)) { + throw new \LogicException(\sprintf('Operators with more than 2 operands are not supported yet, got %d.', \count($operands))); + } elseif (2 === \count($operands)) { + $safe = $this->intersectSafe($this->getSafe($node->getNode($operands[0])), $this->getSafe($node->getNode($operands[1]))); + $this->setSafe($node, $safe); + } } elseif ($node instanceof FilterExpression) { // filter expression is safe when the filter is safe - $name = $node->getNode('filter')->getAttribute('value'); - $args = $node->getNode('arguments'); - if ($filter = $env->getFilter($name)) { - $safe = $filter->getSafe($args); + if ($node->hasAttribute('twig_callable')) { + $filter = $node->getAttribute('twig_callable'); + } else { + // legacy + $filter = $env->getFilter($node->getAttribute('name')); + } + + if ($filter) { + $safe = $filter->getSafe($node->getNode('arguments')); if (null === $safe) { + trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $filter::class); + $safe = []; + } + + if (!$safe) { $safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety()); } $this->setSafe($node, $safe); - } else { - $this->setSafe($node, []); } } elseif ($node instanceof FunctionExpression) { // function expression is safe when the function is safe - $name = $node->getAttribute('name'); - $args = $node->getNode('arguments'); - if ($function = $env->getFunction($name)) { - $this->setSafe($node, $function->getSafe($args)); + if ($node->hasAttribute('twig_callable')) { + $function = $node->getAttribute('twig_callable'); } else { - $this->setSafe($node, []); + // legacy + $function = $env->getFunction($node->getAttribute('name')); } - } elseif ($node instanceof MethodCallExpression) { - if ($node->getAttribute('safe')) { - $this->setSafe($node, ['all']); - } else { - $this->setSafe($node, []); + + if ($function) { + $safe = $function->getSafe($node->getNode('arguments')); + if (null === $safe) { + trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $function::class); + $safe = []; + } + $this->setSafe($node, $safe); } - } elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof NameExpression) { + } elseif ($node instanceof MethodCallExpression || $node instanceof MacroReferenceExpression) { + // all macro calls are safe + $this->setSafe($node, ['all']); + } elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof ContextVariable) { $name = $node->getNode('node')->getAttribute('name'); if (\in_array($name, $this->safeVars)) { $this->setSafe($node, ['all']); - } else { - $this->setSafe($node, []); } - } else { - $this->setSafe($node, []); } return $node; } - private function intersectSafe(array $a = null, array $b = null): array + private function intersectSafe(array $a, array $b): array { - if (null === $a || null === $b) { + if (!$a || !$b) { return []; } diff --git a/app/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php b/app/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php index 1446cee6b..7e89ef83a 100644 --- a/app/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php +++ b/app/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php @@ -15,14 +15,17 @@ 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\Expression\Variable\ContextVariable; use Twig\Node\ModuleNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Node\SetNode; @@ -34,8 +37,11 @@ final class SandboxNodeVisitor implements NodeVisitorInterface { private $inAModule = false; + /** @var array */ private $tags; + /** @var array */ private $filters; + /** @var array */ private $functions; private $needsToStringWrap = false; @@ -51,22 +57,22 @@ public function enterNode(Node $node, Environment $env): Node } elseif ($this->inAModule) { // look for tags if ($node->getNodeTag() && !isset($this->tags[$node->getNodeTag()])) { - $this->tags[$node->getNodeTag()] = $node; + $this->tags[$node->getNodeTag()] = $node->getTemplateLine(); } // look for filters - if ($node instanceof FilterExpression && !isset($this->filters[$node->getNode('filter')->getAttribute('value')])) { - $this->filters[$node->getNode('filter')->getAttribute('value')] = $node; + if ($node instanceof FilterExpression && !isset($this->filters[$node->getAttribute('name')])) { + $this->filters[$node->getAttribute('name')] = $node->getTemplateLine(); } // look for functions if ($node instanceof FunctionExpression && !isset($this->functions[$node->getAttribute('name')])) { - $this->functions[$node->getAttribute('name')] = $node; + $this->functions[$node->getAttribute('name')] = $node->getTemplateLine(); } // the .. operator is equivalent to the range() function if ($node instanceof RangeBinary && !isset($this->functions['range'])) { - $this->functions['range'] = $node; + $this->functions['range'] = $node->getTemplateLine(); } if ($node instanceof PrintNode) { @@ -102,8 +108,8 @@ public function leaveNode(Node $node, Environment $env): ?Node if ($node instanceof ModuleNode) { $this->inAModule = false; - $node->setNode('constructor_end', new Node([new CheckSecurityCallNode(), $node->getNode('constructor_end')])); - $node->setNode('class_end', new Node([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('class_end')])); + $node->setNode('constructor_end', new Nodes([new CheckSecurityCallNode(), $node->getNode('constructor_end')])); + $node->setNode('class_end', new Nodes([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('class_end')])); } elseif ($this->inAModule) { if ($node instanceof PrintNode || $node instanceof SetNode) { $this->needsToStringWrap = false; @@ -116,8 +122,19 @@ public function leaveNode(Node $node, Environment $env): ?Node private function wrapNode(Node $node, string $name): void { $expr = $node->getNode($name); - if ($expr instanceof NameExpression || $expr instanceof GetAttrExpression) { - $node->setNode($name, new CheckToStringNode($expr)); + if (($expr instanceof ContextVariable || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) { + // 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); + } } } diff --git a/app/vendor/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php b/app/vendor/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php new file mode 100644 index 000000000..3c9786275 --- /dev/null +++ b/app/vendor/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php @@ -0,0 +1,59 @@ +yieldReadyNodes[$class])) { + return $node; + } + + if (!$this->yieldReadyNodes[$class] = (bool) (new \ReflectionClass($class))->getAttributes(YieldReady::class)) { + if ($this->useYield) { + throw new \LogicException(\sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.', $class)); + } + + trigger_deprecation('twig/twig', '3.9', 'Twig node "%s" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.', $class); + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + return $node; + } + + public function getPriority(): int + { + return 255; + } +} diff --git a/app/vendor/twig/twig/src/OperatorPrecedenceChange.php b/app/vendor/twig/twig/src/OperatorPrecedenceChange.php new file mode 100644 index 000000000..1d9edefd1 --- /dev/null +++ b/app/vendor/twig/twig/src/OperatorPrecedenceChange.php @@ -0,0 +1,42 @@ + + */ +class OperatorPrecedenceChange +{ + public function __construct( + private string $package, + private string $version, + private int $newPrecedence, + ) { + } + + public function getPackage(): string + { + return $this->package; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getNewPrecedence(): int + { + return $this->newPrecedence; + } +} diff --git a/app/vendor/twig/twig/src/Parser.php b/app/vendor/twig/twig/src/Parser.php index 4016a5f39..ff1772c16 100644 --- a/app/vendor/twig/twig/src/Parser.php +++ b/app/vendor/twig/twig/src/Parser.php @@ -16,15 +16,20 @@ use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; use Twig\Node\BodyNode; +use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\Variable\AssignTemplateVariable; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\MacroNode; use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\Node\NodeCaptureInterface; use Twig\Node\NodeOutputInterface; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Node\TextNode; use Twig\TokenParser\TokenParserInterface; +use Twig\Util\ReflectionCallable; /** * @author Fabien Potencier @@ -39,20 +44,27 @@ class Parser private $blocks; private $blockStack; private $macros; - private $env; private $importedSymbols; private $traits; private $embeddedTemplates = []; private $varNameSalt = 0; + private $ignoreUnknownTwigCallables = false; - public function __construct(Environment $env) + public function __construct( + private Environment $env, + ) { + } + + public function getEnvironment(): Environment { - $this->env = $env; + return $this->env; } public function getVarName(): string { - return sprintf('__internal_parse_%d', $this->varNameSalt++); + trigger_deprecation('twig/twig', '3.15', 'The "%s()" method is deprecated.', __METHOD__); + + return \sprintf('__internal_parse_%d', $this->varNameSalt++); } public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode @@ -83,7 +95,7 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals $body = $this->subparse($test, $dropNeedle); if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) { - $body = new Node(); + $body = new EmptyNode(); } } catch (SyntaxError $e) { if (!$e->getSourceContext()) { @@ -91,16 +103,19 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals } if (!$e->getTemplateLine()) { - $e->setTemplateLine($this->stream->getCurrent()->getLine()); + $e->setTemplateLine($this->getCurrentToken()->getLine()); } throw $e; } - $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Node($this->blocks), new Node($this->macros), new Node($this->traits), $this->embeddedTemplates, $stream->getSourceContext()); + $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Nodes($this->blocks), new Nodes($this->macros), new Nodes($this->traits), $this->embeddedTemplates, $stream->getSourceContext()); $traverser = new NodeTraverser($this->env, $this->visitors); + /** + * @var ModuleNode $node + */ $node = $traverser->traverse($node); // restore previous stack so previous parse() call can resume working @@ -111,29 +126,45 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals return $node; } + public function shouldIgnoreUnknownTwigCallables(): bool + { + return $this->ignoreUnknownTwigCallables; + } + + public function subparseIgnoreUnknownTwigCallables($test, bool $dropNeedle = false): void + { + $previous = $this->ignoreUnknownTwigCallables; + $this->ignoreUnknownTwigCallables = true; + try { + $this->subparse($test, $dropNeedle); + } finally { + $this->ignoreUnknownTwigCallables = $previous; + } + } + public function subparse($test, bool $dropNeedle = false): Node { $lineno = $this->getCurrentToken()->getLine(); $rv = []; while (!$this->stream->isEOF()) { - switch ($this->getCurrentToken()->getType()) { - case /* Token::TEXT_TYPE */ 0: + switch (true) { + case $this->stream->getCurrent()->test(Token::TEXT_TYPE): $token = $this->stream->next(); $rv[] = new TextNode($token->getValue(), $token->getLine()); break; - case /* Token::VAR_START_TYPE */ 2: + case $this->stream->getCurrent()->test(Token::VAR_START_TYPE): $token = $this->stream->next(); $expr = $this->expressionParser->parseExpression(); - $this->stream->expect(/* Token::VAR_END_TYPE */ 4); + $this->stream->expect(Token::VAR_END_TYPE); $rv[] = new PrintNode($expr, $token->getLine()); break; - case /* Token::BLOCK_START_TYPE */ 1: + case $this->stream->getCurrent()->test(Token::BLOCK_START_TYPE): $this->stream->next(); $token = $this->getCurrentToken(); - if (/* Token::NAME_TYPE */ 5 !== $token->getType()) { + if (!$token->test(Token::NAME_TYPE)) { throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext()); } @@ -146,18 +177,19 @@ public function subparse($test, bool $dropNeedle = false): Node return $rv[0]; } - return new Node($rv, [], $lineno); + return new Nodes($rv, $lineno); } if (!$subparser = $this->env->getTokenParser($token->getValue())) { if (null !== $test) { - $e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); + $e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); - if (\is_array($test) && isset($test[0]) && $test[0] instanceof TokenParserInterface) { - $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno)); + $callable = (new ReflectionCallable(new TwigTest('decision', $test)))->getCallable(); + if (\is_array($callable) && $callable[0] instanceof TokenParserInterface) { + $e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $callable[0]->getTag(), $lineno)); } } else { - $e = new SyntaxError(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); + $e = new SyntaxError(\sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); $e->addSuggestions($token->getValue(), array_keys($this->env->getTokenParsers())); } @@ -168,13 +200,16 @@ public function subparse($test, bool $dropNeedle = false): Node $subparser->setParser($this); $node = $subparser->parse($token); - if (null !== $node) { + if (!$node) { + trigger_deprecation('twig/twig', '3.12', 'Returning "null" from "%s" is deprecated and forbidden by "TokenParserInterface".', $subparser::class); + } else { + $node->setNodeTag($subparser->getTag()); $rv[] = $node; } break; default: - throw new SyntaxError('Lexer or parser ended up in unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext()); + throw new SyntaxError('The lexer or the parser ended up in an unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext()); } } @@ -182,14 +217,19 @@ public function subparse($test, bool $dropNeedle = false): Node return $rv[0]; } - return new Node($rv, [], $lineno); + return new Nodes($rv, $lineno); } public function getBlockStack(): array { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return $this->blockStack; } + /** + * @return string|null + */ public function peekBlockStack() { return $this->blockStack[\count($this->blockStack) - 1] ?? null; @@ -207,21 +247,31 @@ public function pushBlockStack($name): void public function hasBlock(string $name): bool { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return isset($this->blocks[$name]); } public function getBlock(string $name): Node { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return $this->blocks[$name]; } public function setBlock(string $name, BlockNode $value): void { + if (isset($this->blocks[$name])) { + throw new SyntaxError(\sprintf("The block '%s' has already been defined line %d.", $name, $this->blocks[$name]->getTemplateLine()), $this->getCurrentToken()->getLine(), $this->blocks[$name]->getSourceContext()); + } + $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine()); } public function hasMacro(string $name): bool { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return isset($this->macros[$name]); } @@ -237,9 +287,14 @@ public function addTrait($trait): void public function hasTraits(): bool { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return \count($this->traits) > 0; } + /** + * @return void + */ public function embedTemplate(ModuleNode $template) { $template->setIndex(mt_rand()); @@ -247,11 +302,20 @@ public function embedTemplate(ModuleNode $template) $this->embeddedTemplates[] = $template; } - public function addImportedSymbol(string $type, string $alias, string $name = null, AbstractExpression $node = null): void + public function addImportedSymbol(string $type, string $alias, ?string $name = null, AbstractExpression|AssignTemplateVariable|null $internalRef = null): void { - $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node]; + if ($internalRef && !$internalRef instanceof AssignTemplateVariable) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance as an internal reference is deprecated ("%s" given).', __METHOD__, AssignTemplateVariable::class, $internalRef::class); + + $internalRef = new AssignTemplateVariable(new TemplateVariable($internalRef->getAttribute('name'), $internalRef->getTemplateLine()), $internalRef->getAttribute('global')); + } + + $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $internalRef]; } + /** + * @return array{name: string, node: AssignTemplateVariable|null}|null + */ public function getImportedSymbol(string $type, string $alias) { // if the symbol does not exist in the current scope (0), try in the main/global scope (last index) @@ -280,11 +344,29 @@ public function getExpressionParser(): ExpressionParser public function getParent(): ?Node { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return $this->parent; } + /** + * @return bool + */ + public function hasInheritance() + { + return $this->parent || 0 < \count($this->traits); + } + public function setParent(?Node $parent): void { + if (null === $parent) { + trigger_deprecation('twig/twig', '3.12', 'Passing "null" to "%s()" is deprecated.', __METHOD__); + } + + if (null !== $this->parent) { + throw new SyntaxError('Multiple extends tags are forbidden.', $parent->getTemplateLine(), $parent->getSourceContext()); + } + $this->parent = $parent; } @@ -335,7 +417,8 @@ private function filterBodyNodes(Node $node, bool $nested = false): ?Node // here, $nested means "being at the root level of a child template" // we need to discard the wrapping "Node" for the "body" node - $nested = $nested || Node::class !== \get_class($node); + // Node::class !== \get_class($node) should be removed in Twig 4.0 + $nested = $nested || (Node::class !== \get_class($node) && !$node instanceof Nodes); foreach ($node as $k => $n) { if (null !== $n && null === $this->filterBodyNodes($n, $nested)) { $node->removeNode($k); diff --git a/app/vendor/twig/twig/src/Profiler/Dumper/BaseDumper.php b/app/vendor/twig/twig/src/Profiler/Dumper/BaseDumper.php index 4da43e475..267718c1f 100644 --- a/app/vendor/twig/twig/src/Profiler/Dumper/BaseDumper.php +++ b/app/vendor/twig/twig/src/Profiler/Dumper/BaseDumper.php @@ -50,7 +50,7 @@ private function dumpProfile(Profile $profile, $prefix = '', $sibling = false): if ($profile->getDuration() * 1000 < 1) { $str = $start."\n"; } else { - $str = sprintf("%s %s\n", $start, $this->formatTime($profile, $percent)); + $str = \sprintf("%s %s\n", $start, $this->formatTime($profile, $percent)); } $nCount = \count($profile->getProfiles()); diff --git a/app/vendor/twig/twig/src/Profiler/Dumper/BlackfireDumper.php b/app/vendor/twig/twig/src/Profiler/Dumper/BlackfireDumper.php index 03abe0fa0..7cfae16f1 100644 --- a/app/vendor/twig/twig/src/Profiler/Dumper/BlackfireDumper.php +++ b/app/vendor/twig/twig/src/Profiler/Dumper/BlackfireDumper.php @@ -24,7 +24,7 @@ public function dump(Profile $profile): string $this->dumpProfile('main()', $profile, $data); $this->dumpChildren('main()', $profile, $data); - $start = sprintf('%f', microtime(true)); + $start = \sprintf('%f', microtime(true)); $str = <<isTemplate()) { $name = $p->getTemplate(); } else { - $name = sprintf('%s::%s(%s)', $p->getTemplate(), $p->getType(), $p->getName()); + $name = \sprintf('%s::%s(%s)', $p->getTemplate(), $p->getType(), $p->getName()); } - $this->dumpProfile(sprintf('%s==>%s', $parent, $name), $p, $data); + $this->dumpProfile(\sprintf('%s==>%s', $parent, $name), $p, $data); $this->dumpChildren($name, $p, $data); } } - private function dumpProfile(string $edge, Profile $profile, &$data) + private function dumpProfile(string $edge, Profile $profile, &$data): void { if (isset($data[$edge])) { ++$data[$edge]['ct']; diff --git a/app/vendor/twig/twig/src/Profiler/Dumper/HtmlDumper.php b/app/vendor/twig/twig/src/Profiler/Dumper/HtmlDumper.php index 3c0daf1c8..cdab2de59 100644 --- a/app/vendor/twig/twig/src/Profiler/Dumper/HtmlDumper.php +++ b/app/vendor/twig/twig/src/Profiler/Dumper/HtmlDumper.php @@ -32,16 +32,16 @@ public function dump(Profile $profile): string protected function formatTemplate(Profile $profile, $prefix): string { - return sprintf('%sâ”” %s', $prefix, self::$colors['template'], $profile->getTemplate()); + return \sprintf('%sâ”” %s', $prefix, self::$colors['template'], $profile->getTemplate()); } protected function formatNonTemplate(Profile $profile, $prefix): string { - return sprintf('%sâ”” %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName()); + return \sprintf('%sâ”” %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName()); } protected function formatTime(Profile $profile, $percent): string { - return sprintf('%.2fms/%.0f%%', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent); + return \sprintf('%.2fms/%.0f%%', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent); } } diff --git a/app/vendor/twig/twig/src/Profiler/Dumper/TextDumper.php b/app/vendor/twig/twig/src/Profiler/Dumper/TextDumper.php index 31561c466..1c1f77e94 100644 --- a/app/vendor/twig/twig/src/Profiler/Dumper/TextDumper.php +++ b/app/vendor/twig/twig/src/Profiler/Dumper/TextDumper.php @@ -20,16 +20,16 @@ final class TextDumper extends BaseDumper { protected function formatTemplate(Profile $profile, $prefix): string { - return sprintf('%sâ”” %s', $prefix, $profile->getTemplate()); + return \sprintf('%sâ”” %s', $prefix, $profile->getTemplate()); } protected function formatNonTemplate(Profile $profile, $prefix): string { - return sprintf('%sâ”” %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), $profile->getName()); + return \sprintf('%sâ”” %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), $profile->getName()); } protected function formatTime(Profile $profile, $percent): string { - return sprintf('%.2fms/%.0f%%', $profile->getDuration() * 1000, $percent); + return \sprintf('%.2fms/%.0f%%', $profile->getDuration() * 1000, $percent); } } diff --git a/app/vendor/twig/twig/src/Profiler/Node/EnterProfileNode.php b/app/vendor/twig/twig/src/Profiler/Node/EnterProfileNode.php index 1494baf44..4d8e504d1 100644 --- a/app/vendor/twig/twig/src/Profiler/Node/EnterProfileNode.php +++ b/app/vendor/twig/twig/src/Profiler/Node/EnterProfileNode.php @@ -11,6 +11,7 @@ namespace Twig\Profiler\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Node; @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class EnterProfileNode extends Node { public function __construct(string $extensionName, string $type, string $name, string $varName) @@ -29,10 +31,10 @@ public function __construct(string $extensionName, string $type, string $name, s public function compile(Compiler $compiler): void { $compiler - ->write(sprintf('$%s = $this->extensions[', $this->getAttribute('var_name'))) + ->write(\sprintf('$%s = $this->extensions[', $this->getAttribute('var_name'))) ->repr($this->getAttribute('extension_name')) ->raw("];\n") - ->write(sprintf('$%s->enter($%s = new \Twig\Profiler\Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) + ->write(\sprintf('$%s->enter($%s = new \Twig\Profiler\Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) ->repr($this->getAttribute('type')) ->raw(', ') ->repr($this->getAttribute('name')) diff --git a/app/vendor/twig/twig/src/Profiler/Node/LeaveProfileNode.php b/app/vendor/twig/twig/src/Profiler/Node/LeaveProfileNode.php index 94cebbaa8..bd9227e52 100644 --- a/app/vendor/twig/twig/src/Profiler/Node/LeaveProfileNode.php +++ b/app/vendor/twig/twig/src/Profiler/Node/LeaveProfileNode.php @@ -11,6 +11,7 @@ namespace Twig\Profiler\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Node; @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class LeaveProfileNode extends Node { public function __construct(string $varName) @@ -30,7 +32,7 @@ public function compile(Compiler $compiler): void { $compiler ->write("\n") - ->write(sprintf("\$%s->leave(\$%s);\n\n", $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) + ->write(\sprintf("\$%s->leave(\$%s);\n\n", $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) ; } } diff --git a/app/vendor/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php b/app/vendor/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php index 91abee807..4c5c2005d 100644 --- a/app/vendor/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php +++ b/app/vendor/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php @@ -17,6 +17,7 @@ use Twig\Node\MacroNode; use Twig\Node\ModuleNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\Profiler\Node\EnterProfileNode; use Twig\Profiler\Node\LeaveProfileNode; @@ -27,13 +28,12 @@ */ final class ProfilerNodeVisitor implements NodeVisitorInterface { - private $extensionName; private $varName; - public function __construct(string $extensionName) - { - $this->extensionName = $extensionName; - $this->varName = sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName)); + public function __construct( + private string $extensionName, + ) { + $this->varName = \sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName)); } public function enterNode(Node $node, Environment $env): Node @@ -44,8 +44,8 @@ public function enterNode(Node $node, Environment $env): Node public function leaveNode(Node $node, Environment $env): ?Node { if ($node instanceof ModuleNode) { - $node->setNode('display_start', new Node([new EnterProfileNode($this->extensionName, Profile::TEMPLATE, $node->getTemplateName(), $this->varName), $node->getNode('display_start')])); - $node->setNode('display_end', new Node([new LeaveProfileNode($this->varName), $node->getNode('display_end')])); + $node->setNode('display_start', new Nodes([new EnterProfileNode($this->extensionName, Profile::TEMPLATE, $node->getTemplateName(), $this->varName), $node->getNode('display_start')])); + $node->setNode('display_end', new Nodes([new LeaveProfileNode($this->varName), $node->getNode('display_end')])); } elseif ($node instanceof BlockNode) { $node->setNode('body', new BodyNode([ new EnterProfileNode($this->extensionName, Profile::BLOCK, $node->getAttribute('name'), $this->varName), diff --git a/app/vendor/twig/twig/src/Profiler/Profile.php b/app/vendor/twig/twig/src/Profiler/Profile.php index 7979a23c6..a3c6ee02e 100644 --- a/app/vendor/twig/twig/src/Profiler/Profile.php +++ b/app/vendor/twig/twig/src/Profiler/Profile.php @@ -20,18 +20,15 @@ final class Profile implements \IteratorAggregate, \Serializable public const BLOCK = 'block'; public const TEMPLATE = 'template'; public const MACRO = 'macro'; - - private $template; - private $name; - private $type; private $starts = []; private $ends = []; private $profiles = []; - public function __construct(string $template = 'main', string $type = self::ROOT, string $name = 'main') - { - $this->template = $template; - $this->type = $type; + public function __construct( + private string $template = 'main', + private string $type = self::ROOT, + private string $name = 'main', + ) { $this->name = str_starts_with($name, '__internal_') ? 'INTERNAL' : $name; $this->enter(); } @@ -102,6 +99,22 @@ public function getDuration(): float return isset($this->ends['wt']) && isset($this->starts['wt']) ? $this->ends['wt'] - $this->starts['wt'] : 0; } + /** + * Returns the start time in microseconds. + */ + public function getStartTime(): float + { + return $this->starts['wt'] ?? 0.0; + } + + /** + * Returns the end time in microseconds. + */ + public function getEndTime(): float + { + return $this->ends['wt'] ?? 0.0; + } + /** * Returns the memory usage in bytes. */ @@ -176,6 +189,6 @@ public function __serialize(): array */ public function __unserialize(array $data): void { - list($this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles) = $data; + [$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles] = $data; } } diff --git a/app/vendor/twig/twig/src/Resources/core.php b/app/vendor/twig/twig/src/Resources/core.php new file mode 100644 index 000000000..bc0b27104 --- /dev/null +++ b/app/vendor/twig/twig/src/Resources/core.php @@ -0,0 +1,541 @@ +getCharset(), $values, $max); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->formatDate($date, $format, $timezone); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_date_modify_filter(Environment $env, $date, $modifier) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->modifyDate($date, $modifier); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_sprintf($format, ...$values) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::sprintf($format, ...$values); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_date_converter(Environment $env, $date = null, $timezone = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->convertDate($date, $timezone); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_replace_filter($str, $from) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::replace($str, $from); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_round($value, $precision = 0, $method = 'common') +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::round($value, $precision, $method); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->formatNumber($number, $decimal, $decimalPoint, $thousandSep); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_urlencode_filter($url) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::urlencode($url); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_merge(...$arrays) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::merge(...$arrays); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::slice($env->getCharset(), $item, $start, $length, $preserveKeys); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_first(Environment $env, $item) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::first($env->getCharset(), $item); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_last(Environment $env, $item) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::last($env->getCharset(), $item); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_join_filter($value, $glue = '', $and = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::join($value, $glue, $and); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::split($env->getCharset(), $value, $delimiter, $limit); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_get_array_keys_filter($array) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::keys($array); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::reverse($env->getCharset(), $item, $preserveKeys); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_sort_filter(Environment $env, $array, $arrow = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::sort($env, $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_matches(string $regexp, ?string $str) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::matches($regexp, $str); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_trim_filter($string, $characterMask = null, $side = 'both') +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::trim($string, $characterMask, $side); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_nl2br($string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::nl2br($string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_spaceless($content) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::spaceless($content); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_convert_encoding($string, $to, $from) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::convertEncoding($string, $to, $from); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_length_filter(Environment $env, $thing) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::length($env->getCharset(), $thing); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_upper_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::upper($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_lower_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::lower($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_striptags($string, $allowable_tags = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::striptags($string, $allowable_tags); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_title_string_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::titleCase($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_capitalize_string_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::capitalize($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_test_empty($value) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::testEmpty($value); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_test_iterable($value) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return is_iterable($value); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::include($env, $context, $template, $variables, $withContext, $ignoreMissing, $sandboxed); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_source(Environment $env, $name, $ignoreMissing = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::source($env, $name, $ignoreMissing); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_constant($constant, $object = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::constant($constant, $object); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_constant_is_defined($constant, $object = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::constant($constant, $object, true); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::batch($items, $size, $fill, $preserveKeys); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_column($array, $name, $index = null): array +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::column($array, $name, $index); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_filter(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::filter($env, $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_map(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::map($env, $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::reduce($env, $array, $arrow, $initial); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_some(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::arraySome($env, $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_every(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::arrayEvery($env, $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + CoreExtension::checkArrow($env, $arrow, $thing, $type); +} diff --git a/app/vendor/twig/twig/src/Resources/debug.php b/app/vendor/twig/twig/src/Resources/debug.php new file mode 100644 index 000000000..104b4f4e0 --- /dev/null +++ b/app/vendor/twig/twig/src/Resources/debug.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Twig\Environment; +use Twig\Extension\DebugExtension; + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_var_dump(Environment $env, $context, ...$vars) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + DebugExtension::dump($env, $context, ...$vars); +} diff --git a/app/vendor/twig/twig/src/Resources/escaper.php b/app/vendor/twig/twig/src/Resources/escaper.php new file mode 100644 index 000000000..a2ee8e7aa --- /dev/null +++ b/app/vendor/twig/twig/src/Resources/escaper.php @@ -0,0 +1,51 @@ +getRuntime(EscaperRuntime::class)->escape($string, $strategy, $charset, $autoescape); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_escape_filter_is_safe(Node $filterArgs) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return EscaperExtension::escapeFilterIsSafe($filterArgs); +} diff --git a/app/vendor/twig/twig/src/Resources/string_loader.php b/app/vendor/twig/twig/src/Resources/string_loader.php new file mode 100644 index 000000000..8f0e6492a --- /dev/null +++ b/app/vendor/twig/twig/src/Resources/string_loader.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Twig\Environment; +use Twig\Extension\StringLoaderExtension; +use Twig\TemplateWrapper; + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_template_from_string(Environment $env, $template, ?string $name = null): TemplateWrapper +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return StringLoaderExtension::templateFromString($env, $template, $name); +} diff --git a/app/vendor/twig/twig/src/Runtime/EscaperRuntime.php b/app/vendor/twig/twig/src/Runtime/EscaperRuntime.php new file mode 100644 index 000000000..719a5696a --- /dev/null +++ b/app/vendor/twig/twig/src/Runtime/EscaperRuntime.php @@ -0,0 +1,340 @@ + */ + private $escapers = []; + + /** @internal */ + public $safeClasses = []; + + /** @internal */ + public $safeLookup = []; + + public function __construct( + private $charset = 'UTF-8', + ) { + } + + /** + * Defines a new escaper to be used via the escape filter. + * + * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param callable(string $string, string $charset): string $callable A valid PHP callable + * + * @return void + */ + public function setEscaper($strategy, callable $callable) + { + $this->escapers[$strategy] = $callable; + } + + /** + * Gets all defined escapers. + * + * @return array An array of escapers + */ + public function getEscapers() + { + return $this->escapers; + } + + /** + * @param array, string[]> $safeClasses + * + * @return void + */ + public function setSafeClasses(array $safeClasses = []) + { + $this->safeClasses = []; + $this->safeLookup = []; + foreach ($safeClasses as $class => $strategies) { + $this->addSafeClass($class, $strategies); + } + } + + /** + * @param class-string<\Stringable> $class + * @param string[] $strategies + * + * @return void + */ + public function addSafeClass(string $class, array $strategies) + { + $class = ltrim($class, '\\'); + if (!isset($this->safeClasses[$class])) { + $this->safeClasses[$class] = []; + } + $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies); + + foreach ($strategies as $strategy) { + $this->safeLookup[$strategy][$class] = true; + } + } + + /** + * Escapes a string. + * + * @param mixed $string The value to be escaped + * @param string $strategy The escaping strategy + * @param string|null $charset The charset + * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) + * + * @throws RuntimeError + */ + public function escape($string, string $strategy = 'html', ?string $charset = null, bool $autoescape = false) + { + if ($autoescape && $string instanceof Markup) { + return $string; + } + + if (!\is_string($string)) { + if ($string instanceof \Stringable) { + if ($autoescape) { + $c = \get_class($string); + if (!isset($this->safeClasses[$c])) { + $this->safeClasses[$c] = []; + foreach (class_parents($string) + class_implements($string) as $class) { + if (isset($this->safeClasses[$class])) { + $this->safeClasses[$c] = array_unique(array_merge($this->safeClasses[$c], $this->safeClasses[$class])); + foreach ($this->safeClasses[$class] as $s) { + $this->safeLookup[$s][$c] = true; + } + } + } + } + if (isset($this->safeLookup[$strategy][$c]) || isset($this->safeLookup['all'][$c])) { + return (string) $string; + } + } + + $string = (string) $string; + } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { + // we return the input as is (which can be of any type) + return $string; + } + } + + if ('' === $string) { + return ''; + } + + $charset = $charset ?: $this->charset; + + switch ($strategy) { + case 'html': + // see https://www.php.net/htmlspecialchars + + // Using a static variable to avoid initializing the array + // each time the function is called. Moving the declaration on the + // top of the function slow downs other escaping strategies. + static $htmlspecialcharsCharsets = [ + 'ISO-8859-1' => true, 'ISO8859-1' => true, + 'ISO-8859-15' => true, 'ISO8859-15' => true, + 'utf-8' => true, 'UTF-8' => true, + 'CP866' => true, 'IBM866' => true, '866' => true, + 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, + '1251' => true, + 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, + 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, + 'BIG5' => true, '950' => true, + 'GB2312' => true, '936' => true, + 'BIG5-HKSCS' => true, + 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, + 'EUC-JP' => true, 'EUCJP' => true, + 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, + ]; + + if (isset($htmlspecialcharsCharsets[$charset])) { + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); + } + + if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { + // cache the lowercase variant for future iterations + $htmlspecialcharsCharsets[$charset] = true; + + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); + } + + $string = $this->convertEncoding($string, 'UTF-8', $charset); + $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); + + return iconv('UTF-8', $charset, $string); + + case 'js': + // escape all non-alphanumeric characters + // into their \x or \uHHHH representations + if ('UTF-8' !== $charset) { + $string = $this->convertEncoding($string, 'UTF-8', $charset); + } + + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) { + $char = $matches[0]; + + /* + * A few characters have short escape sequences in JSON and JavaScript. + * Escape sequences supported only by JavaScript, not JSON, are omitted. + * \" is also supported but omitted, because the resulting string is not HTML safe. + */ + static $shortMap = [ + '\\' => '\\\\', + '/' => '\\/', + "\x08" => '\b', + "\x0C" => '\f', + "\x0A" => '\n', + "\x0D" => '\r', + "\x09" => '\t', + ]; + + if (isset($shortMap[$char])) { + return $shortMap[$char]; + } + + $codepoint = mb_ord($char, 'UTF-8'); + if (0x10000 > $codepoint) { + return \sprintf('\u%04X', $codepoint); + } + + // Split characters outside the BMP into surrogate pairs + // https://tools.ietf.org/html/rfc2781.html#section-2.1 + $u = $codepoint - 0x10000; + $high = 0xD800 | ($u >> 10); + $low = 0xDC00 | ($u & 0x3FF); + + return \sprintf('\u%04X\u%04X', $high, $low); + }, $string); + + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } + + return $string; + + case 'css': + if ('UTF-8' !== $charset) { + $string = $this->convertEncoding($string, 'UTF-8', $charset); + } + + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) { + $char = $matches[0]; + + return \sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); + }, $string); + + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } + + return $string; + + case 'html_attr': + if ('UTF-8' !== $charset) { + $string = $this->convertEncoding($string, 'UTF-8', $charset); + } + + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) { + /** + * This function is adapted from code coming from Zend Framework. + * + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://framework.zend.com/license/new-bsd New BSD License + */ + $chr = $matches[0]; + $ord = \ord($chr); + + /* + * The following replaces characters undefined in HTML with the + * hex entity for the Unicode replacement character. + */ + if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) { + return '�'; + } + + /* + * Check if the current character to escape has a name entity we should + * replace it with while grabbing the hex value of the character. + */ + if (1 === \strlen($chr)) { + /* + * While HTML supports far more named entities, the lowest common denominator + * has become HTML5's XML Serialisation which is restricted to the those named + * entities that XML supports. Using HTML entities would result in this error: + * XML Parsing Error: undefined entity + */ + static $entityMap = [ + 34 => '"', /* quotation mark */ + 38 => '&', /* ampersand */ + 60 => '<', /* less-than sign */ + 62 => '>', /* greater-than sign */ + ]; + + if (isset($entityMap[$ord])) { + return $entityMap[$ord]; + } + + return \sprintf('&#x%02X;', $ord); + } + + /* + * Per OWASP recommendations, we'll use hex entities for any other + * characters where a named entity does not exist. + */ + return \sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); + }, $string); + + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } + + return $string; + + case 'url': + return rawurlencode($string); + + default: + if (\array_key_exists($strategy, $this->escapers)) { + return $this->escapers[$strategy]($string, $charset); + } + + $validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($this->escapers))); + + throw new RuntimeError(\sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies)); + } + } + + private function convertEncoding(string $string, string $to, string $from) + { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + } + + return iconv($from, $to, $string); + } +} diff --git a/app/vendor/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php b/app/vendor/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php index b360d7bea..05106680c 100644 --- a/app/vendor/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php +++ b/app/vendor/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php @@ -23,11 +23,9 @@ */ class ContainerRuntimeLoader implements RuntimeLoaderInterface { - private $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; + public function __construct( + private ContainerInterface $container, + ) { } public function load(string $class) diff --git a/app/vendor/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php b/app/vendor/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php index 130648392..5d4e70b92 100644 --- a/app/vendor/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php +++ b/app/vendor/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php @@ -18,14 +18,12 @@ */ class FactoryRuntimeLoader implements RuntimeLoaderInterface { - private $map; - /** * @param array $map An array where keys are class names and values factory callables */ - public function __construct(array $map = []) - { - $this->map = $map; + public function __construct( + private array $map = [], + ) { } public function load(string $class) diff --git a/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedFilterError.php b/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedFilterError.php index 02d306360..9293a3f0b 100644 --- a/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedFilterError.php +++ b/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedFilterError.php @@ -18,7 +18,7 @@ */ final class SecurityNotAllowedFilterError extends SecurityError { - private $filterName; + private string $filterName; public function __construct(string $message, string $functionName) { diff --git a/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedFunctionError.php b/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedFunctionError.php index 4f76dc6ec..71c9f02bc 100644 --- a/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedFunctionError.php +++ b/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedFunctionError.php @@ -18,7 +18,7 @@ */ final class SecurityNotAllowedFunctionError extends SecurityError { - private $functionName; + private string $functionName; public function __construct(string $message, string $functionName) { diff --git a/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedMethodError.php b/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedMethodError.php index 8df9d0baa..98e8e434d 100644 --- a/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedMethodError.php +++ b/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedMethodError.php @@ -18,8 +18,8 @@ */ final class SecurityNotAllowedMethodError extends SecurityError { - private $className; - private $methodName; + private string $className; + private string $methodName; public function __construct(string $message, string $className, string $methodName) { @@ -33,7 +33,7 @@ public function getClassName(): string return $this->className; } - public function getMethodName() + public function getMethodName(): string { return $this->methodName; } diff --git a/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedPropertyError.php b/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedPropertyError.php index 42ec4f386..e74ffeddb 100644 --- a/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedPropertyError.php +++ b/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedPropertyError.php @@ -18,8 +18,8 @@ */ final class SecurityNotAllowedPropertyError extends SecurityError { - private $className; - private $propertyName; + private string $className; + private string $propertyName; public function __construct(string $message, string $className, string $propertyName) { @@ -33,7 +33,7 @@ public function getClassName(): string return $this->className; } - public function getPropertyName() + public function getPropertyName(): string { return $this->propertyName; } diff --git a/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedTagError.php b/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedTagError.php index 4522150e1..f9cd625b4 100644 --- a/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedTagError.php +++ b/app/vendor/twig/twig/src/Sandbox/SecurityNotAllowedTagError.php @@ -18,7 +18,7 @@ */ final class SecurityNotAllowedTagError extends SecurityError { - private $tagName; + private string $tagName; public function __construct(string $message, string $tagName) { diff --git a/app/vendor/twig/twig/src/Sandbox/SecurityPolicy.php b/app/vendor/twig/twig/src/Sandbox/SecurityPolicy.php index a725aa4f1..b0d054260 100644 --- a/app/vendor/twig/twig/src/Sandbox/SecurityPolicy.php +++ b/app/vendor/twig/twig/src/Sandbox/SecurityPolicy.php @@ -50,7 +50,7 @@ public function setAllowedMethods(array $methods): void { $this->allowedMethods = []; foreach ($methods as $class => $m) { - $this->allowedMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, \is_array($m) ? $m : [$m]); + $this->allowedMethods[$class] = array_map('strtolower', \is_array($m) ? $m : [$m]); } } @@ -68,19 +68,25 @@ public function checkSecurity($tags, $filters, $functions): void { foreach ($tags as $tag) { if (!\in_array($tag, $this->allowedTags)) { - throw new SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag); + if ('extends' === $tag) { + trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.'); + } elseif ('use' === $tag) { + trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.'); + } else { + throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag); + } } } foreach ($filters as $filter) { if (!\in_array($filter, $this->allowedFilters)) { - throw new SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter); + throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter); } } foreach ($functions as $function) { if (!\in_array($function, $this->allowedFunctions)) { - throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function); + throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function); } } } @@ -92,7 +98,7 @@ public function checkMethodAllowed($obj, $method): void } $allowed = false; - $method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + $method = strtolower($method); foreach ($this->allowedMethods as $class => $methods) { if ($obj instanceof $class && \in_array($method, $methods)) { $allowed = true; @@ -102,7 +108,7 @@ public function checkMethodAllowed($obj, $method): void if (!$allowed) { $class = \get_class($obj); - throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method); + throw new SecurityNotAllowedMethodError(\sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method); } } @@ -118,7 +124,7 @@ public function checkPropertyAllowed($obj, $property): void if (!$allowed) { $class = \get_class($obj); - throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property); + throw new SecurityNotAllowedPropertyError(\sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property); } } } diff --git a/app/vendor/twig/twig/src/Sandbox/SourcePolicyInterface.php b/app/vendor/twig/twig/src/Sandbox/SourcePolicyInterface.php new file mode 100644 index 000000000..b952f1ea6 --- /dev/null +++ b/app/vendor/twig/twig/src/Sandbox/SourcePolicyInterface.php @@ -0,0 +1,24 @@ +code = $code; - $this->name = $name; - $this->path = $path; + public function __construct( + private string $code, + private string $name, + private string $path = '', + ) { } public function getCode(): string diff --git a/app/vendor/twig/twig/src/Template.php b/app/vendor/twig/twig/src/Template.php index ffbaae1ea..156752f8b 100644 --- a/app/vendor/twig/twig/src/Template.php +++ b/app/vendor/twig/twig/src/Template.php @@ -13,7 +13,6 @@ namespace Twig; use Twig\Error\Error; -use Twig\Error\LoaderError; use Twig\Error\RuntimeError; /** @@ -35,38 +34,37 @@ abstract class Template protected $parent; protected $parents = []; - protected $env; protected $blocks = []; protected $traits = []; + protected $traitAliases = []; protected $extensions = []; protected $sandbox; - public function __construct(Environment $env) - { - $this->env = $env; + private $useYield; + + public function __construct( + protected Environment $env, + ) { + $this->useYield = $env->useYield(); $this->extensions = $env->getExtensions(); } /** * Returns the template name. - * - * @return string The template name */ - abstract public function getTemplateName(); + abstract public function getTemplateName(): string; /** * Returns debug information about the template. * - * @return array Debug information + * @return array Debug information */ - abstract public function getDebugInfo(); + abstract public function getDebugInfo(): array; /** * Returns information about the original template source code. - * - * @return Source */ - abstract public function getSourceContext(); + abstract public function getSourceContext(): Source; /** * Returns the parent template. @@ -74,44 +72,35 @@ abstract public function getSourceContext(); * This method is for internal use only and should never be called * directly. * - * @return Template|TemplateWrapper|false The parent template or false if there is no parent + * @return self|TemplateWrapper|false The parent template or false if there is no parent */ - public function getParent(array $context) + public function getParent(array $context): self|TemplateWrapper|false { if (null !== $this->parent) { return $this->parent; } - try { - $parent = $this->doGetParent($context); - - if (false === $parent) { - return false; - } - - if ($parent instanceof self || $parent instanceof TemplateWrapper) { - return $this->parents[$parent->getSourceContext()->getName()] = $parent; - } + if (!$parent = $this->doGetParent($context)) { + return false; + } - if (!isset($this->parents[$parent])) { - $this->parents[$parent] = $this->loadTemplate($parent); - } - } catch (LoaderError $e) { - $e->setSourceContext(null); - $e->guess(); + if ($parent instanceof self || $parent instanceof TemplateWrapper) { + return $this->parents[$parent->getSourceContext()->getName()] = $parent; + } - throw $e; + if (!isset($this->parents[$parent])) { + $this->parents[$parent] = $this->loadTemplate($parent); } return $this->parents[$parent]; } - protected function doGetParent(array $context) + protected function doGetParent(array $context): bool|string|self|TemplateWrapper { return false; } - public function isTraitable() + public function isTraitable(): bool { return true; } @@ -126,14 +115,10 @@ public function isTraitable() * @param array $context The context * @param array $blocks The current set of blocks */ - public function displayParentBlock($name, array $context, array $blocks = []) + public function displayParentBlock($name, array $context, array $blocks = []): void { - if (isset($this->traits[$name])) { - $this->traits[$name][0]->displayBlock($name, $context, $blocks, false); - } elseif (false !== $parent = $this->getParent($context)) { - $parent->displayBlock($name, $context, $blocks, false); - } else { - throw new RuntimeError(sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + echo $data; } } @@ -148,51 +133,10 @@ public function displayParentBlock($name, array $context, array $blocks = []) * @param array $blocks The current set of blocks * @param bool $useBlocks Whether to use the current set of blocks */ - public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, self $templateContext = null) + public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): void { - if ($useBlocks && isset($blocks[$name])) { - $template = $blocks[$name][0]; - $block = $blocks[$name][1]; - } elseif (isset($this->blocks[$name])) { - $template = $this->blocks[$name][0]; - $block = $this->blocks[$name][1]; - } else { - $template = null; - $block = null; - } - - // avoid RCEs when sandbox is enabled - if (null !== $template && !$template instanceof self) { - throw new \LogicException('A block must be a method on a \Twig\Template instance.'); - } - - if (null !== $template) { - try { - $template->$block($context, $blocks); - } catch (Error $e) { - if (!$e->getSourceContext()) { - $e->setSourceContext($template->getSourceContext()); - } - - // this is mostly useful for \Twig\Error\LoaderError exceptions - // see \Twig\Error\LoaderError - if (-1 === $e->getTemplateLine()) { - $e->guess(); - } - - throw $e; - } catch (\Throwable $e) { - $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); - $e->guess(); - - throw $e; - } - } elseif (false !== $parent = $this->getParent($context)) { - $parent->displayBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); - } elseif (isset($blocks[$name])) { - throw new RuntimeError(sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); - } else { - throw new RuntimeError(sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) { + echo $data; } } @@ -208,16 +152,25 @@ public function displayBlock($name, array $context, array $blocks = [], $useBloc * * @return string The rendered block */ - public function renderParentBlock($name, array $context, array $blocks = []) + public function renderParentBlock($name, array $context, array $blocks = []): string { - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); + if (!$this->useYield) { + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(function () { return ''; }); + } + $this->displayParentBlock($name, $context, $blocks); + + return ob_get_clean(); + } + + $content = ''; + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + $content .= $data; } - $this->displayParentBlock($name, $context, $blocks); - return ob_get_clean(); + return $content; } /** @@ -233,16 +186,34 @@ public function renderParentBlock($name, array $context, array $blocks = []) * * @return string The rendered block */ - public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true) + public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true): string { - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); + if (!$this->useYield) { + $level = ob_get_level(); + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(function () { return ''; }); + } + try { + $this->displayBlock($name, $context, $blocks, $useBlocks); + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } + + return ob_get_clean(); } - $this->displayBlock($name, $context, $blocks, $useBlocks); - return ob_get_clean(); + $content = ''; + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) { + $content .= $data; + } + + return $content; } /** @@ -257,7 +228,7 @@ public function renderBlock($name, array $context, array $blocks = [], $useBlock * * @return bool true if the block exists, false otherwise */ - public function hasBlock($name, array $context, array $blocks = []) + public function hasBlock($name, array $context, array $blocks = []): bool { if (isset($blocks[$name])) { return $blocks[$name][0] instanceof self; @@ -267,7 +238,7 @@ public function hasBlock($name, array $context, array $blocks = []) return true; } - if (false !== $parent = $this->getParent($context)) { + if ($parent = $this->getParent($context)) { return $parent->hasBlock($name, $context); } @@ -283,13 +254,13 @@ public function hasBlock($name, array $context, array $blocks = []) * @param array $context The context * @param array $blocks The current set of blocks * - * @return array An array of block names + * @return array An array of block names */ - public function getBlockNames(array $context, array $blocks = []) + public function getBlockNames(array $context, array $blocks = []): array { $names = array_merge(array_keys($blocks), array_keys($this->blocks)); - if (false !== $parent = $this->getParent($context)) { + if ($parent = $this->getParent($context)) { $names = array_merge($names, $parent->getBlockNames($context)); } @@ -297,16 +268,22 @@ public function getBlockNames(array $context, array $blocks = []) } /** - * @return Template|TemplateWrapper + * @param string|TemplateWrapper|array $template */ - protected function loadTemplate($template, $templateName = null, $line = null, $index = null) + protected function loadTemplate($template, $templateName = null, $line = null, $index = null): self|TemplateWrapper { try { if (\is_array($template)) { return $this->env->resolveTemplate($template); } - if ($template instanceof self || $template instanceof TemplateWrapper) { + if ($template instanceof TemplateWrapper) { + return $template; + } + + if ($template instanceof self) { + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__); + return $template; } @@ -342,9 +319,9 @@ protected function loadTemplate($template, $templateName = null, $line = null, $ /** * @internal * - * @return Template + * @return $this */ - public function unwrap() + public function unwrap(): self { return $this; } @@ -357,41 +334,58 @@ public function unwrap() * * @return array An array of blocks */ - public function getBlocks() + public function getBlocks(): array { return $this->blocks; } - public function display(array $context, array $blocks = []) + public function display(array $context, array $blocks = []): void { - $this->displayWithErrorHandling($this->env->mergeGlobals($context), array_merge($this->blocks, $blocks)); + foreach ($this->yield($context, $blocks) as $data) { + echo $data; + } } - public function render(array $context) + public function render(array $context): string { - $level = ob_get_level(); - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); - } - try { - $this->display($context); - } catch (\Throwable $e) { - while (ob_get_level() > $level) { - ob_end_clean(); + if (!$this->useYield) { + $level = ob_get_level(); + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(function () { return ''; }); } + try { + $this->display($context); + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } - throw $e; + throw $e; + } + + return ob_get_clean(); } - return ob_get_clean(); + $content = ''; + foreach ($this->yield($context) as $data) { + $content .= $data; + } + + return $content; } - protected function displayWithErrorHandling(array $context, array $blocks = []) + /** + * @return iterable + */ + public function yield(array $context, array $blocks = []): iterable { + $context += $this->env->getGlobals(); + $blocks = array_merge($this->blocks, $blocks); + try { - $this->doDisplay($context, $blocks); + yield from $this->doDisplay($context, $blocks); } catch (Error $e) { if (!$e->getSourceContext()) { $e->setSourceContext($this->getSourceContext()); @@ -405,18 +399,123 @@ protected function displayWithErrorHandling(array $context, array $blocks = []) throw $e; } catch (\Throwable $e) { - $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e); + $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e); $e->guess(); throw $e; } } + /** + * @return iterable + */ + public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable + { + if ($useBlocks && isset($blocks[$name])) { + $template = $blocks[$name][0]; + $block = $blocks[$name][1]; + } elseif (isset($this->blocks[$name])) { + $template = $this->blocks[$name][0]; + $block = $this->blocks[$name][1]; + } else { + $template = null; + $block = null; + } + + // avoid RCEs when sandbox is enabled + if (null !== $template && !$template instanceof self) { + throw new \LogicException('A block must be a method on a \Twig\Template instance.'); + } + + if (null !== $template) { + try { + yield from $template->$block($context, $blocks); + } catch (Error $e) { + if (!$e->getSourceContext()) { + $e->setSourceContext($template->getSourceContext()); + } + + // this is mostly useful for \Twig\Error\LoaderError exceptions + // see \Twig\Error\LoaderError + if (-1 === $e->getTemplateLine()) { + $e->guess(); + } + + throw $e; + } catch (\Throwable $e) { + $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); + $e->guess(); + + throw $e; + } + } elseif ($parent = $this->getParent($context)) { + yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); + } elseif (isset($blocks[$name])) { + throw new RuntimeError(\sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); + } else { + throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); + } + } + + /** + * Yields a parent block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to display from the parent + * @param array $context The context + * @param array $blocks The current set of blocks + * + * @return iterable + */ + public function yieldParentBlock($name, array $context, array $blocks = []): iterable + { + if (isset($this->traits[$name])) { + yield from $this->traits[$name][0]->yieldBlock($this->traitAliases[$name] ?? $name, $context, $blocks, false); + } elseif ($parent = $this->getParent($context)) { + yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false); + } else { + throw new RuntimeError(\sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); + } + } + + protected function hasMacro(string $name, array $context): bool + { + if (method_exists($this, $name)) { + return true; + } + + if (!$parent = $this->getParent($context)) { + return false; + } + + return $parent->hasMacro($name, $context); + } + + protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): self + { + if (method_exists($this, $name)) { + return $this; + } + + $parent = $this; + while ($parent = $parent->getParent($context)) { + if (method_exists($parent, $name)) { + return $parent; + } + } + + throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($name, \strlen('macro_')), $this->getTemplateName()), $line, $source); + } + /** * Auto-generated method to display the template with the given context. * * @param array $context An array of parameters to pass to the template * @param array $blocks An array of blocks to pass to the template + * + * @return iterable */ - abstract protected function doDisplay(array $context, array $blocks = []); + abstract protected function doDisplay(array $context, array $blocks = []): iterable; } diff --git a/app/vendor/twig/twig/src/TemplateWrapper.php b/app/vendor/twig/twig/src/TemplateWrapper.php index 1ecd82251..265ce3e1c 100644 --- a/app/vendor/twig/twig/src/TemplateWrapper.php +++ b/app/vendor/twig/twig/src/TemplateWrapper.php @@ -18,19 +18,32 @@ */ final class TemplateWrapper { - private $env; - private $template; - /** * This method is for internal use only and should never be called * directly (use Twig\Environment::load() instead). * * @internal */ - public function __construct(Environment $env, Template $template) + public function __construct( + private Environment $env, + private Template $template, + ) { + } + + /** + * @return iterable + */ + public function stream(array $context = []): iterable + { + yield from $this->template->yield($context); + } + + /** + * @return iterable + */ + public function streamBlock(string $name, array $context = []): iterable { - $this->env = $env; - $this->template = $template; + yield from $this->template->yieldBlock($name, $context); } public function render(array $context = []): string @@ -38,6 +51,9 @@ public function render(array $context = []): string return $this->template->render($context); } + /** + * @return void + */ public function display(array $context = []) { // using func_get_args() allows to not expose the blocks argument @@ -60,29 +76,18 @@ public function getBlockNames(array $context = []): array public function renderBlock(string $name, array $context = []): string { - $context = $this->env->mergeGlobals($context); - $level = ob_get_level(); - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); - } - try { - $this->template->displayBlock($name, $context); - } catch (\Throwable $e) { - while (ob_get_level() > $level) { - ob_end_clean(); - } - - throw $e; - } - - return ob_get_clean(); + return $this->template->renderBlock($name, $context + $this->env->getGlobals()); } + /** + * @return void + */ public function displayBlock(string $name, array $context = []) { - $this->template->displayBlock($name, $this->env->mergeGlobals($context)); + $context += $this->env->getGlobals(); + foreach ($this->template->yieldBlock($name, $context) as $data) { + echo $data; + } } public function getSourceContext(): Source diff --git a/app/vendor/twig/twig/src/Test/IntegrationTestCase.php b/app/vendor/twig/twig/src/Test/IntegrationTestCase.php index e97ad4170..f4a5dc7e5 100644 --- a/app/vendor/twig/twig/src/Test/IntegrationTestCase.php +++ b/app/vendor/twig/twig/src/Test/IntegrationTestCase.php @@ -17,6 +17,7 @@ use Twig\Extension\ExtensionInterface; use Twig\Loader\ArrayLoader; use Twig\RuntimeLoader\RuntimeLoaderInterface; +use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; @@ -30,9 +31,19 @@ abstract class IntegrationTestCase extends TestCase { /** + * @deprecated since Twig 3.13, use getFixturesDirectory() instead. + * * @return string */ - abstract protected function getFixturesDir(); + protected function getFixturesDir() + { + throw new \BadMethodCallException('Not implemented.'); + } + + protected static function getFixturesDirectory(): string + { + throw new \BadMethodCallException('Not implemented.'); + } /** * @return RuntimeLoaderInterface[] @@ -74,8 +85,34 @@ protected function getTwigTests() return []; } + /** + * @return array + */ + protected function getUndefinedFilterCallbacks(): array + { + return []; + } + + /** + * @return array + */ + protected function getUndefinedFunctionCallbacks(): array + { + return []; + } + + /** + * @return array + */ + protected function getUndefinedTokenParserCallbacks(): array + { + return []; + } + /** * @dataProvider getTests + * + * @return void */ public function testIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '') { @@ -86,15 +123,29 @@ public function testIntegration($file, $message, $condition, $templates, $except * @dataProvider getLegacyTests * * @group legacy + * + * @return void */ public function testLegacyIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '') { $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation); } + /** + * @return iterable + * + * @final since Twig 3.13 + */ public function getTests($name, $legacyTests = false) { - $fixturesDir = realpath($this->getFixturesDir()); + try { + $fixturesDir = static::getFixturesDirectory(); + } catch (\BadMethodCallException) { + trigger_deprecation('twig/twig', '3.13', 'Not overriding "%s::getFixturesDirectory()" in "%s" is deprecated. This method will be abstract in 4.0.', self::class, static::class); + $fixturesDir = $this->getFixturesDir(); + } + + $fixturesDir = realpath($fixturesDir); $tests = []; foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($fixturesDir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { @@ -123,13 +174,13 @@ public function getTests($name, $legacyTests = false) $exception = false; preg_match_all('/--DATA--(.*?)(?:--CONFIG--(.*?))?--EXPECT--(.*?)(?=\-\-DATA\-\-|$)/s', $test, $outputs, \PREG_SET_ORDER); } else { - throw new \InvalidArgumentException(sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file))); + throw new \InvalidArgumentException(\sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file))); } - $tests[] = [str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs, $deprecation]; + $tests[str_replace($fixturesDir.'/', '', $file)] = [str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs, $deprecation]; } - if ($legacyTests && empty($tests)) { + if ($legacyTests && !$tests) { // add a dummy test to avoid a PHPUnit message return [['not', '-', '', [], '', []]]; } @@ -137,11 +188,19 @@ public function getTests($name, $legacyTests = false) return $tests; } + /** + * @final since Twig 3.13 + * + * @return iterable + */ public function getLegacyTests() { return $this->getTests('testLegacyIntegration', true); } + /** + * @return void + */ protected function doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '') { if (!$outputs) { @@ -149,19 +208,23 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e } if ($condition) { + $ret = ''; eval('$ret = '.$condition.';'); if (!$ret) { $this->markTestSkipped($condition); } } - $loader = new ArrayLoader($templates); - foreach ($outputs as $i => $match) { $config = array_merge([ 'cache' => false, 'strict_variables' => true, ], $match[2] ? eval($match[2].';') : []); + // make sure that template are always compiled even if they are the same (useful when testing with more than one data/expect sections) + foreach ($templates as $j => $template) { + $templates[$j] = $template.str_repeat(' ', $i); + } + $loader = new ArrayLoader($templates); $twig = new Environment($loader, $config); $twig->addGlobal('global', 'global'); foreach ($this->getRuntimeLoaders() as $runtimeLoader) { @@ -184,10 +247,17 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e $twig->addFunction($function); } - // avoid using the same PHP class name for different cases - $p = new \ReflectionProperty($twig, 'templateClassPrefix'); - $p->setAccessible(true); - $p->setValue($twig, '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', uniqid(mt_rand(), true), false).'_'); + foreach ($this->getUndefinedFilterCallbacks() as $callback) { + $twig->registerUndefinedFilterCallback($callback); + } + + foreach ($this->getUndefinedFunctionCallbacks() as $callback) { + $twig->registerUndefinedFunctionCallback($callback); + } + + foreach ($this->getUndefinedTokenParserCallbacks() as $callback) { + $twig->registerUndefinedTokenParserCallback($callback); + } $deprecations = []; try { @@ -205,14 +275,14 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e } catch (\Exception $e) { if (false !== $exception) { $message = $e->getMessage(); - $this->assertSame(trim($exception), trim(sprintf('%s: %s', \get_class($e), $message))); + $this->assertSame(trim($exception), trim(\sprintf('%s: %s', \get_class($e), $message))); $last = substr($message, \strlen($message) - 1); $this->assertTrue('.' === $last || '?' === $last, 'Exception message must end with a dot or a question mark.'); return; } - throw new Error(sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); + throw new Error(\sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); } finally { restore_error_handler(); } @@ -223,18 +293,18 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e $output = trim($template->render(eval($match[1].';')), "\n "); } catch (\Exception $e) { if (false !== $exception) { - $this->assertSame(trim($exception), trim(sprintf('%s: %s', \get_class($e), $e->getMessage()))); + $this->assertStringMatchesFormat(trim($exception), trim(\sprintf('%s: %s', \get_class($e), $e->getMessage()))); return; } - $e = new Error(sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); + $e = new Error(\sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); - $output = trim(sprintf('%s: %s', \get_class($e), $e->getMessage())); + $output = trim(\sprintf('%s: %s', \get_class($e), $e->getMessage())); } if (false !== $exception) { - list($class) = explode(':', $exception); + [$class] = explode(':', $exception); $constraintClass = class_exists('PHPUnit\Framework\Constraint\Exception') ? 'PHPUnit\Framework\Constraint\Exception' : 'PHPUnit_Framework_Constraint_Exception'; $this->assertThat(null, new $constraintClass($class)); } @@ -253,6 +323,9 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e } } + /** + * @return array + */ protected static function parseTemplates($test) { $templates = []; diff --git a/app/vendor/twig/twig/src/Test/NodeTestCase.php b/app/vendor/twig/twig/src/Test/NodeTestCase.php index 8b1bef776..0cb5b2fab 100644 --- a/app/vendor/twig/twig/src/Test/NodeTestCase.php +++ b/app/vendor/twig/twig/src/Test/NodeTestCase.php @@ -11,6 +11,8 @@ namespace Twig\Test; +use PHPUnit\Framework\Attributes\BeforeClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Twig\Compiler; use Twig\Environment; @@ -19,17 +21,45 @@ abstract class NodeTestCase extends TestCase { - abstract public function getTests(); + /** + * @var Environment + */ + private $currentEnv; + + /** + * @return iterable + */ + public function getTests() + { + return []; + } + + /** + * @return iterable + */ + public static function provideTests(): iterable + { + trigger_deprecation('twig/twig', '3.13', 'Not implementing "%s()" in "%s" is deprecated. This method will be abstract in 4.0.', __METHOD__, static::class); + + return []; + } /** * @dataProvider getTests + * @dataProvider provideTests + * + * @return void */ + #[DataProvider('getTests'), DataProvider('provideTests')] public function testCompile($node, $source, $environment = null, $isPattern = false) { $this->assertNodeCompilation($source, $node, $environment, $isPattern); } - public function assertNodeCompilation($source, Node $node, Environment $environment = null, $isPattern = false) + /** + * @return void + */ + public function assertNodeCompilation($source, Node $node, ?Environment $environment = null, $isPattern = false) { $compiler = $this->getCompiler($environment); $compiler->compile($node); @@ -41,25 +71,72 @@ public function assertNodeCompilation($source, Node $node, Environment $environm } } - protected function getCompiler(Environment $environment = null) + /** + * @return Compiler + */ + protected function getCompiler(?Environment $environment = null) { return new Compiler($environment ?? $this->getEnvironment()); } + /** + * @return Environment + * + * @final since Twig 3.13 + */ protected function getEnvironment() { - return new Environment(new ArrayLoader([])); + return $this->currentEnv ??= static::createEnvironment(); } + protected static function createEnvironment(): Environment + { + return new Environment(new ArrayLoader()); + } + + /** + * @return string + * + * @deprecated since Twig 3.13, use createVariableGetter() instead. + */ protected function getVariableGetter($name, $line = false) + { + trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createVariableGetter()" instead.', __METHOD__); + + return self::createVariableGetter($name, $line); + } + + final protected static function createVariableGetter(string $name, bool $line = false): string { $line = $line > 0 ? "// line $line\n" : ''; - return sprintf('%s($context["%s"] ?? null)', $line, $name); + return \sprintf('%s($context["%s"] ?? null)', $line, $name); } + /** + * @return string + * + * @deprecated since Twig 3.13, use createAttributeGetter() instead. + */ protected function getAttributeGetter() { - return 'twig_get_attribute($this->env, $this->source, '; + trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createAttributeGetter()" instead.', __METHOD__); + + return self::createAttributeGetter(); + } + + final protected static function createAttributeGetter(): string + { + return 'CoreExtension::getAttribute($this->env, $this->source, '; + } + + /** @beforeClass */ + #[BeforeClass] + final public static function checkDataProvider(): void + { + $r = new \ReflectionMethod(static::class, 'getTests'); + if (self::class !== $r->getDeclaringClass()->getName()) { + trigger_deprecation('twig/twig', '3.13', 'Implementing "%s::getTests()" in "%s" is deprecated, implement "provideTests()" instead.', self::class, static::class); + } } } diff --git a/app/vendor/twig/twig/src/Token.php b/app/vendor/twig/twig/src/Token.php index 59279b8fe..a4da548cb 100644 --- a/app/vendor/twig/twig/src/Token.php +++ b/app/vendor/twig/twig/src/Token.php @@ -17,10 +17,6 @@ */ final class Token { - private $value; - private $type; - private $lineno; - public const EOF_TYPE = -1; public const TEXT_TYPE = 0; public const BLOCK_START_TYPE = 1; @@ -37,16 +33,16 @@ final class Token public const ARROW_TYPE = 12; public const SPREAD_TYPE = 13; - public function __construct(int $type, $value, int $lineno) - { - $this->type = $type; - $this->value = $value; - $this->lineno = $lineno; + public function __construct( + private int $type, + private $value, + private int $lineno, + ) { } - public function __toString() + public function __toString(): string { - return sprintf('%s(%s)', self::typeToString($this->type, true), $this->value); + return \sprintf('%s(%s)', self::typeToString($this->type, true), $this->value); } /** @@ -79,16 +75,29 @@ public function getLine(): int return $this->lineno; } + /** + * @deprecated since Twig 3.19 + */ public function getType(): int { + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated.', __METHOD__)); + return $this->type; } + /** + * @return mixed + */ public function getValue() { return $this->value; } + public function toEnglish(): string + { + return self::typeToEnglish($this->type); + } + public static function typeToString(int $type, bool $short = false): string { switch ($type) { @@ -138,7 +147,7 @@ public static function typeToString(int $type, bool $short = false): string $name = 'SPREAD_TYPE'; break; default: - throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type)); + throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type)); } return $short ? $name : 'Twig\Token::'.$name; @@ -178,7 +187,7 @@ public static function typeToEnglish(int $type): string case self::SPREAD_TYPE: return 'spread operator'; default: - throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type)); + throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type)); } } } diff --git a/app/vendor/twig/twig/src/TokenParser/ApplyTokenParser.php b/app/vendor/twig/twig/src/TokenParser/ApplyTokenParser.php index 4dbf30406..0c9507482 100644 --- a/app/vendor/twig/twig/src/TokenParser/ApplyTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/ApplyTokenParser.php @@ -11,8 +11,9 @@ namespace Twig\TokenParser; -use Twig\Node\Expression\TempNameExpression; +use Twig\Node\Expression\Variable\LocalVariable; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Node\SetNode; use Twig\Token; @@ -31,21 +32,17 @@ final class ApplyTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $lineno = $token->getLine(); - $name = $this->parser->getVarName(); - - $ref = new TempNameExpression($name, $lineno); - $ref->setAttribute('always_defined', true); - - $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref, $this->getTag()); + $ref = new LocalVariable(null, $lineno); + $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideApplyEnd'], true); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new Node([ - new SetNode(true, $ref, $body, $lineno, $this->getTag()), - new PrintNode($filter, $lineno, $this->getTag()), - ]); + return new Nodes([ + new SetNode(true, $ref, $body, $lineno), + new PrintNode($filter, $lineno), + ], $lineno); } public function decideApplyEnd(Token $token): bool diff --git a/app/vendor/twig/twig/src/TokenParser/AutoEscapeTokenParser.php b/app/vendor/twig/twig/src/TokenParser/AutoEscapeTokenParser.php index b674bea4a..b50b29e65 100644 --- a/app/vendor/twig/twig/src/TokenParser/AutoEscapeTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/AutoEscapeTokenParser.php @@ -29,7 +29,7 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $stream = $this->parser->getStream(); - if ($stream->test(/* Token::BLOCK_END_TYPE */ 3)) { + if ($stream->test(Token::BLOCK_END_TYPE)) { $value = 'html'; } else { $expr = $this->parser->getExpressionParser()->parseExpression(); @@ -39,11 +39,11 @@ public function parse(Token $token): Node $value = $expr->getAttribute('value'); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - return new AutoEscapeNode($value, $body, $lineno, $this->getTag()); + return new AutoEscapeNode($value, $body, $lineno); } public function decideBlockEnd(Token $token): bool diff --git a/app/vendor/twig/twig/src/TokenParser/BlockTokenParser.php b/app/vendor/twig/twig/src/TokenParser/BlockTokenParser.php index 5878131be..3561b99cd 100644 --- a/app/vendor/twig/twig/src/TokenParser/BlockTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/BlockTokenParser.php @@ -15,7 +15,9 @@ use Twig\Error\SyntaxError; use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; +use Twig\Node\EmptyNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Token; @@ -35,35 +37,32 @@ public function parse(Token $token): Node { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); - if ($this->parser->hasBlock($name)) { - throw new SyntaxError(sprintf("The block '%s' has already been defined line %d.", $name, $this->parser->getBlock($name)->getTemplateLine()), $stream->getCurrent()->getLine(), $stream->getSourceContext()); - } - $this->parser->setBlock($name, $block = new BlockNode($name, new Node([]), $lineno)); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); + $this->parser->setBlock($name, $block = new BlockNode($name, new EmptyNode(), $lineno)); $this->parser->pushLocalScope(); $this->parser->pushBlockStack($name); - if ($stream->nextIf(/* Token::BLOCK_END_TYPE */ 3)) { + if ($stream->nextIf(Token::BLOCK_END_TYPE)) { $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) { + if ($token = $stream->nextIf(Token::NAME_TYPE)) { $value = $token->getValue(); if ($value != $name) { - throw new SyntaxError(sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } } } else { - $body = new Node([ + $body = new Nodes([ new PrintNode($this->parser->getExpressionParser()->parseExpression(), $lineno), ]); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $block->setNode('body', $body); $this->parser->popBlockStack(); $this->parser->popLocalScope(); - return new BlockReferenceNode($name, $lineno, $this->getTag()); + return new BlockReferenceNode($name, $lineno); } public function decideBlockEnd(Token $token): bool diff --git a/app/vendor/twig/twig/src/TokenParser/DeprecatedTokenParser.php b/app/vendor/twig/twig/src/TokenParser/DeprecatedTokenParser.php index 31416c79c..164ef26ee 100644 --- a/app/vendor/twig/twig/src/TokenParser/DeprecatedTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/DeprecatedTokenParser.php @@ -11,6 +11,7 @@ namespace Twig\TokenParser; +use Twig\Error\SyntaxError; use Twig\Node\DeprecatedNode; use Twig\Node\Node; use Twig\Token; @@ -21,6 +22,8 @@ * {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' %} * {% extends 'layout.html.twig' %} * + * {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' package="foo/bar" version="1.1" %} + * * @author Yonel Ceruto * * @internal @@ -29,11 +32,31 @@ final class DeprecatedTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $expr = $this->parser->getExpressionParser()->parseExpression(); + $stream = $this->parser->getStream(); + $expressionParser = $this->parser->getExpressionParser(); + $expr = $expressionParser->parseExpression(); + $node = new DeprecatedNode($expr, $token->getLine()); + + while ($stream->test(Token::NAME_TYPE)) { + $k = $stream->getCurrent()->getValue(); + $stream->next(); + $stream->expect(Token::OPERATOR_TYPE, '='); + + switch ($k) { + case 'package': + $node->setNode('package', $expressionParser->parseExpression()); + break; + case 'version': + $node->setNode('version', $expressionParser->parseExpression()); + break; + default: + throw new SyntaxError(\sprintf('Unknown "%s" option.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + } - $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); - return new DeprecatedNode($expr, $token->getLine(), $this->getTag()); + return $node; } public function getTag(): string diff --git a/app/vendor/twig/twig/src/TokenParser/DoTokenParser.php b/app/vendor/twig/twig/src/TokenParser/DoTokenParser.php index 32c8f12ff..8afd48559 100644 --- a/app/vendor/twig/twig/src/TokenParser/DoTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/DoTokenParser.php @@ -26,9 +26,9 @@ public function parse(Token $token): Node { $expr = $this->parser->getExpressionParser()->parseExpression(); - $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new DoNode($expr, $token->getLine(), $this->getTag()); + return new DoNode($expr, $token->getLine()); } public function getTag(): string diff --git a/app/vendor/twig/twig/src/TokenParser/EmbedTokenParser.php b/app/vendor/twig/twig/src/TokenParser/EmbedTokenParser.php index 64b4f296f..f1acbf1ef 100644 --- a/app/vendor/twig/twig/src/TokenParser/EmbedTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/EmbedTokenParser.php @@ -13,7 +13,7 @@ use Twig\Node\EmbedNode; use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; use Twig\Token; @@ -30,21 +30,21 @@ public function parse(Token $token): Node $parent = $this->parser->getExpressionParser()->parseExpression(); - list($variables, $only, $ignoreMissing) = $this->parseArguments(); + [$variables, $only, $ignoreMissing] = $this->parseArguments(); - $parentToken = $fakeParentToken = new Token(/* Token::STRING_TYPE */ 7, '__parent__', $token->getLine()); + $parentToken = $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine()); if ($parent instanceof ConstantExpression) { - $parentToken = new Token(/* Token::STRING_TYPE */ 7, $parent->getAttribute('value'), $token->getLine()); - } elseif ($parent instanceof NameExpression) { - $parentToken = new Token(/* Token::NAME_TYPE */ 5, $parent->getAttribute('name'), $token->getLine()); + $parentToken = new Token(Token::STRING_TYPE, $parent->getAttribute('value'), $token->getLine()); + } elseif ($parent instanceof ContextVariable) { + $parentToken = new Token(Token::NAME_TYPE, $parent->getAttribute('name'), $token->getLine()); } // inject a fake parent to make the parent() function work $stream->injectTokens([ - new Token(/* Token::BLOCK_START_TYPE */ 1, '', $token->getLine()), - new Token(/* Token::NAME_TYPE */ 5, 'extends', $token->getLine()), + new Token(Token::BLOCK_START_TYPE, '', $token->getLine()), + new Token(Token::NAME_TYPE, 'extends', $token->getLine()), $parentToken, - new Token(/* Token::BLOCK_END_TYPE */ 3, '', $token->getLine()), + new Token(Token::BLOCK_END_TYPE, '', $token->getLine()), ]); $module = $this->parser->parse($stream, [$this, 'decideBlockEnd'], true); @@ -56,9 +56,9 @@ public function parse(Token $token): Node $this->parser->embedTemplate($module); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine()); } public function decideBlockEnd(Token $token): bool diff --git a/app/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php b/app/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php index 0ca46dd29..a93afe8cd 100644 --- a/app/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php @@ -13,6 +13,7 @@ namespace Twig\TokenParser; use Twig\Error\SyntaxError; +use Twig\Node\EmptyNode; use Twig\Node\Node; use Twig\Token; @@ -35,14 +36,11 @@ public function parse(Token $token): Node throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext()); } - if (null !== $this->parser->getParent()) { - throw new SyntaxError('Multiple extends tags are forbidden.', $token->getLine(), $stream->getSourceContext()); - } $this->parser->setParent($this->parser->getExpressionParser()->parseExpression()); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - return new Node(); + return new EmptyNode($token->getLine()); } public function getTag(): string diff --git a/app/vendor/twig/twig/src/TokenParser/FlushTokenParser.php b/app/vendor/twig/twig/src/TokenParser/FlushTokenParser.php index 02c74aa13..0d2388745 100644 --- a/app/vendor/twig/twig/src/TokenParser/FlushTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/FlushTokenParser.php @@ -26,9 +26,9 @@ final class FlushTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new FlushNode($token->getLine(), $this->getTag()); + return new FlushNode($token->getLine()); } public function getTag(): string diff --git a/app/vendor/twig/twig/src/TokenParser/ForTokenParser.php b/app/vendor/twig/twig/src/TokenParser/ForTokenParser.php index bac8ba2da..3e08b22fa 100644 --- a/app/vendor/twig/twig/src/TokenParser/ForTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/ForTokenParser.php @@ -12,7 +12,8 @@ namespace Twig\TokenParser; -use Twig\Node\Expression\AssignNameExpression; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\ForElseNode; use Twig\Node\ForNode; use Twig\Node\Node; use Twig\Token; @@ -35,30 +36,31 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $stream = $this->parser->getStream(); $targets = $this->parser->getExpressionParser()->parseAssignmentExpression(); - $stream->expect(/* Token::OPERATOR_TYPE */ 8, 'in'); + $stream->expect(Token::OPERATOR_TYPE, 'in'); $seq = $this->parser->getExpressionParser()->parseExpression(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideForFork']); if ('else' == $stream->next()->getValue()) { - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); - $else = $this->parser->subparse([$this, 'decideForEnd'], true); + $elseLineno = $stream->getCurrent()->getLine(); + $stream->expect(Token::BLOCK_END_TYPE); + $else = new ForElseNode($this->parser->subparse([$this, 'decideForEnd'], true), $elseLineno); } else { $else = null; } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); if (\count($targets) > 1) { - $keyTarget = $targets->getNode(0); - $keyTarget = new AssignNameExpression($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine()); - $valueTarget = $targets->getNode(1); + $keyTarget = $targets->getNode('0'); + $keyTarget = new AssignContextVariable($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine()); + $valueTarget = $targets->getNode('1'); } else { - $keyTarget = new AssignNameExpression('_key', $lineno); - $valueTarget = $targets->getNode(0); + $keyTarget = new AssignContextVariable('_key', $lineno); + $valueTarget = $targets->getNode('0'); } - $valueTarget = new AssignNameExpression($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine()); + $valueTarget = new AssignContextVariable($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine()); - return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno, $this->getTag()); + return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno); } public function decideForFork(Token $token): bool diff --git a/app/vendor/twig/twig/src/TokenParser/FromTokenParser.php b/app/vendor/twig/twig/src/TokenParser/FromTokenParser.php index 31b6cde41..c8732df29 100644 --- a/app/vendor/twig/twig/src/TokenParser/FromTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/FromTokenParser.php @@ -11,7 +11,9 @@ namespace Twig\TokenParser; -use Twig\Node\Expression\AssignNameExpression; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\AssignTemplateVariable; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\ImportNode; use Twig\Node\Node; use Twig\Token; @@ -19,7 +21,7 @@ /** * Imports macros. * - * {% from 'forms.html' import forms %} + * {% from 'forms.html.twig' import forms %} * * @internal */ @@ -29,31 +31,32 @@ public function parse(Token $token): Node { $macro = $this->parser->getExpressionParser()->parseExpression(); $stream = $this->parser->getStream(); - $stream->expect(/* Token::NAME_TYPE */ 5, 'import'); + $stream->expect(Token::NAME_TYPE, 'import'); $targets = []; while (true) { - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); - $alias = $name; if ($stream->nextIf('as')) { - $alias = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $alias = new AssignContextVariable($stream->expect(Token::NAME_TYPE)->getValue(), $token->getLine()); + } else { + $alias = new AssignContextVariable($name, $token->getLine()); } $targets[$name] = $alias; - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - $var = new AssignNameExpression($this->parser->getVarName(), $token->getLine()); - $node = new ImportNode($macro, $var, $token->getLine(), $this->getTag(), $this->parser->isMainScope()); + $internalRef = new AssignTemplateVariable(new TemplateVariable(null, $token->getLine()), $this->parser->isMainScope()); + $node = new ImportNode($macro, $internalRef, $token->getLine()); foreach ($targets as $name => $alias) { - $this->parser->addImportedSymbol('function', $alias, 'macro_'.$name, $var); + $this->parser->addImportedSymbol('function', $alias->getAttribute('name'), 'macro_'.$name, $internalRef); } return $node; diff --git a/app/vendor/twig/twig/src/TokenParser/GuardTokenParser.php b/app/vendor/twig/twig/src/TokenParser/GuardTokenParser.php new file mode 100644 index 000000000..1fcf76cd7 --- /dev/null +++ b/app/vendor/twig/twig/src/TokenParser/GuardTokenParser.php @@ -0,0 +1,73 @@ +parser->getStream(); + $typeToken = $stream->expect(Token::NAME_TYPE); + if (!\in_array($typeToken->getValue(), ['function', 'filter', 'test'])) { + throw new SyntaxError(\sprintf('Supported guard types are function, filter and test, "%s" given.', $typeToken->getValue()), $typeToken->getLine(), $stream->getSourceContext()); + } + $method = 'get'.$typeToken->getValue(); + + $nameToken = $stream->expect(Token::NAME_TYPE); + + try { + $exists = null !== $this->parser->getEnvironment()->$method($nameToken->getValue()); + } catch (SyntaxError) { + $exists = false; + } + + $stream->expect(Token::BLOCK_END_TYPE); + if ($exists) { + $body = $this->parser->subparse([$this, 'decideGuardFork']); + } else { + $body = new EmptyNode(); + $this->parser->subparseIgnoreUnknownTwigCallables([$this, 'decideGuardFork']); + } + $else = new EmptyNode(); + if ('else' === $stream->next()->getValue()) { + $stream->expect(Token::BLOCK_END_TYPE); + $else = $this->parser->subparse([$this, 'decideGuardEnd'], true); + } + $stream->expect(Token::BLOCK_END_TYPE); + + return new Nodes([$exists ? $body : $else]); + } + + public function decideGuardFork(Token $token): bool + { + return $token->test(['else', 'endguard']); + } + + public function decideGuardEnd(Token $token): bool + { + return $token->test(['endguard']); + } + + public function getTag(): string + { + return 'guard'; + } +} diff --git a/app/vendor/twig/twig/src/TokenParser/IfTokenParser.php b/app/vendor/twig/twig/src/TokenParser/IfTokenParser.php index c0fe6df0d..6b9010563 100644 --- a/app/vendor/twig/twig/src/TokenParser/IfTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/IfTokenParser.php @@ -15,6 +15,7 @@ use Twig\Error\SyntaxError; use Twig\Node\IfNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Token; /** @@ -37,7 +38,7 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $expr = $this->parser->getExpressionParser()->parseExpression(); $stream = $this->parser->getStream(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideIfFork']); $tests = [$expr, $body]; $else = null; @@ -46,13 +47,13 @@ public function parse(Token $token): Node while (!$end) { switch ($stream->next()->getValue()) { case 'else': - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $else = $this->parser->subparse([$this, 'decideIfEnd']); break; case 'elseif': $expr = $this->parser->getExpressionParser()->parseExpression(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideIfFork']); $tests[] = $expr; $tests[] = $body; @@ -63,13 +64,13 @@ public function parse(Token $token): Node break; default: - throw new SyntaxError(sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - return new IfNode(new Node($tests), $else, $lineno, $this->getTag()); + return new IfNode(new Nodes($tests), $else, $lineno); } public function decideIfFork(Token $token): bool diff --git a/app/vendor/twig/twig/src/TokenParser/ImportTokenParser.php b/app/vendor/twig/twig/src/TokenParser/ImportTokenParser.php index 44cb4dad7..f23584a5a 100644 --- a/app/vendor/twig/twig/src/TokenParser/ImportTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/ImportTokenParser.php @@ -11,7 +11,8 @@ namespace Twig\TokenParser; -use Twig\Node\Expression\AssignNameExpression; +use Twig\Node\Expression\Variable\AssignTemplateVariable; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\ImportNode; use Twig\Node\Node; use Twig\Token; @@ -19,7 +20,7 @@ /** * Imports macros. * - * {% import 'forms.html' as forms %} + * {% import 'forms.html.twig' as forms %} * * @internal */ @@ -28,13 +29,13 @@ final class ImportTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $macro = $this->parser->getExpressionParser()->parseExpression(); - $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5, 'as'); - $var = new AssignNameExpression($this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5)->getValue(), $token->getLine()); - $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3); + $this->parser->getStream()->expect(Token::NAME_TYPE, 'as'); + $name = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(); + $var = new AssignTemplateVariable(new TemplateVariable($name, $token->getLine()), $this->parser->isMainScope()); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + $this->parser->addImportedSymbol('template', $name); - $this->parser->addImportedSymbol('template', $var->getAttribute('name')); - - return new ImportNode($macro, $var, $token->getLine(), $this->getTag(), $this->parser->isMainScope()); + return new ImportNode($macro, $var, $token->getLine()); } public function getTag(): string diff --git a/app/vendor/twig/twig/src/TokenParser/IncludeTokenParser.php b/app/vendor/twig/twig/src/TokenParser/IncludeTokenParser.php index 28beb8ae4..c5ce180ad 100644 --- a/app/vendor/twig/twig/src/TokenParser/IncludeTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/IncludeTokenParser.php @@ -12,6 +12,7 @@ namespace Twig\TokenParser; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\IncludeNode; use Twig\Node\Node; use Twig\Token; @@ -19,9 +20,9 @@ /** * Includes a template. * - * {% include 'header.html' %} + * {% include 'header.html.twig' %} * Body - * {% include 'footer.html' %} + * {% include 'footer.html.twig' %} * * @internal */ @@ -31,33 +32,36 @@ public function parse(Token $token): Node { $expr = $this->parser->getExpressionParser()->parseExpression(); - list($variables, $only, $ignoreMissing) = $this->parseArguments(); + [$variables, $only, $ignoreMissing] = $this->parseArguments(); - return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine()); } + /** + * @return array{0: ?AbstractExpression, 1: bool, 2: bool} + */ protected function parseArguments() { $stream = $this->parser->getStream(); $ignoreMissing = false; - if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'ignore')) { - $stream->expect(/* Token::NAME_TYPE */ 5, 'missing'); + if ($stream->nextIf(Token::NAME_TYPE, 'ignore')) { + $stream->expect(Token::NAME_TYPE, 'missing'); $ignoreMissing = true; } $variables = null; - if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'with')) { + if ($stream->nextIf(Token::NAME_TYPE, 'with')) { $variables = $this->parser->getExpressionParser()->parseExpression(); } $only = false; - if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'only')) { + if ($stream->nextIf(Token::NAME_TYPE, 'only')) { $only = true; } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); return [$variables, $only, $ignoreMissing]; } diff --git a/app/vendor/twig/twig/src/TokenParser/MacroTokenParser.php b/app/vendor/twig/twig/src/TokenParser/MacroTokenParser.php index f584927e9..33379be03 100644 --- a/app/vendor/twig/twig/src/TokenParser/MacroTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/MacroTokenParser.php @@ -13,6 +13,12 @@ use Twig\Error\SyntaxError; use Twig\Node\BodyNode; +use Twig\Node\EmptyNode; +use Twig\Node\Expression\ArrayExpression; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\Unary\NegUnary; +use Twig\Node\Expression\Unary\PosUnary; +use Twig\Node\Expression\Variable\LocalVariable; use Twig\Node\MacroNode; use Twig\Node\Node; use Twig\Token; @@ -32,26 +38,25 @@ public function parse(Token $token): Node { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); + $arguments = $this->parseDefinition(); - $arguments = $this->parser->getExpressionParser()->parseArguments(true, true); - - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $this->parser->pushLocalScope(); $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) { + if ($token = $stream->nextIf(Token::NAME_TYPE)) { $value = $token->getValue(); if ($value != $name) { - throw new SyntaxError(sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } } $this->parser->popLocalScope(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno, $this->getTag())); + $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno)); - return new Node(); + return new EmptyNode($lineno); } public function decideBlockEnd(Token $token): bool @@ -63,4 +68,56 @@ public function getTag(): string { return 'macro'; } + + private function parseDefinition(): ArrayExpression + { + $arguments = new ArrayExpression([], $this->parser->getCurrentToken()->getLine()); + $stream = $this->parser->getStream(); + $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { + if (\count($arguments)) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + + // if the comma above was a trailing comma, early exit the argument parse loop + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { + break; + } + } + + $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); + $name = new LocalVariable($token->getValue(), $this->parser->getCurrentToken()->getLine()); + if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) { + $default = $this->parser->getExpressionParser()->parseExpression(); + } else { + $default = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine()); + $default->setAttribute('is_implicit', true); + } + + if (!$this->checkConstantExpression($default)) { + throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); + } + $arguments->addElement($default, $name); + } + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return $arguments; + } + + // checks that the node only contains "constant" elements + private function checkConstantExpression(Node $node): bool + { + if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression + || $node instanceof NegUnary || $node instanceof PosUnary + )) { + return false; + } + + foreach ($node as $n) { + if (!$this->checkConstantExpression($n)) { + return false; + } + } + + return true; + } } diff --git a/app/vendor/twig/twig/src/TokenParser/SandboxTokenParser.php b/app/vendor/twig/twig/src/TokenParser/SandboxTokenParser.php index c919556ec..536c14f30 100644 --- a/app/vendor/twig/twig/src/TokenParser/SandboxTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/SandboxTokenParser.php @@ -22,7 +22,7 @@ * Marks a section of a template as untrusted code that must be evaluated in the sandbox mode. * * {% sandbox %} - * {% include 'user.html' %} + * {% include 'user.html.twig' %} * {% endsandbox %} * * @see https://twig.symfony.com/doc/api.html#sandbox-extension for details @@ -34,9 +34,11 @@ final class SandboxTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + trigger_deprecation('twig/twig', '3.15', \sprintf('The "sandbox" tag is deprecated in "%s" at line %d.', $stream->getSourceContext()->getName(), $token->getLine())); + + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); // in a sandbox tag, only include tags are allowed if (!$body instanceof IncludeNode) { @@ -51,7 +53,7 @@ public function parse(Token $token): Node } } - return new SandboxNode($body, $token->getLine(), $this->getTag()); + return new SandboxNode($body, $token->getLine()); } public function decideBlockEnd(Token $token): bool diff --git a/app/vendor/twig/twig/src/TokenParser/SetTokenParser.php b/app/vendor/twig/twig/src/TokenParser/SetTokenParser.php index 2fbdfe090..bb43907bd 100644 --- a/app/vendor/twig/twig/src/TokenParser/SetTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/SetTokenParser.php @@ -37,10 +37,10 @@ public function parse(Token $token): Node $names = $this->parser->getExpressionParser()->parseAssignmentExpression(); $capture = false; - if ($stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) { + if ($stream->nextIf(Token::OPERATOR_TYPE, '=')) { $values = $this->parser->getExpressionParser()->parseMultitargetExpression(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); if (\count($names) !== \count($values)) { throw new SyntaxError('When using set, you must have the same number of variables and assignments.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); @@ -52,13 +52,13 @@ public function parse(Token $token): Node throw new SyntaxError('When using set with a block, you cannot have a multi-target.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $values = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); } - return new SetNode($capture, $names, $values, $lineno, $this->getTag()); + return new SetNode($capture, $names, $values, $lineno); } public function decideBlockEnd(Token $token): bool diff --git a/app/vendor/twig/twig/src/TokenParser/TypesTokenParser.php b/app/vendor/twig/twig/src/TokenParser/TypesTokenParser.php new file mode 100644 index 000000000..a7da0f5ec --- /dev/null +++ b/app/vendor/twig/twig/src/TokenParser/TypesTokenParser.php @@ -0,0 +1,89 @@ + + * + * @internal + */ +final class TypesTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $stream = $this->parser->getStream(); + $types = $this->parseSimpleMappingExpression($stream); + $stream->expect(Token::BLOCK_END_TYPE); + + return new TypesNode($types, $token->getLine()); + } + + /** + * @return array + * + * @throws SyntaxError + */ + private function parseSimpleMappingExpression(TokenStream $stream): array + { + $enclosed = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '{'); + $types = []; + $first = true; + while (!($stream->test(Token::PUNCTUATION_TYPE, '}') || $stream->test(Token::BLOCK_END_TYPE))) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A type string must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, '}') || $stream->test(Token::BLOCK_END_TYPE)) { + break; + } + } + $first = false; + + $nameToken = $stream->expect(Token::NAME_TYPE); + + if ($stream->nextIf(Token::OPERATOR_TYPE, '?:')) { + $isOptional = true; + } else { + $isOptional = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '?'); + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); + } + + $valueToken = $stream->expect(Token::STRING_TYPE); + + $types[$nameToken->getValue()] = [ + 'type' => $valueToken->getValue(), + 'optional' => $isOptional, + ]; + } + + if ($enclosed) { + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + } + + return $types; + } + + public function getTag(): string + { + return 'types'; + } +} diff --git a/app/vendor/twig/twig/src/TokenParser/UseTokenParser.php b/app/vendor/twig/twig/src/TokenParser/UseTokenParser.php index 3cdbb98ad..ebd95aa31 100644 --- a/app/vendor/twig/twig/src/TokenParser/UseTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/UseTokenParser.php @@ -12,8 +12,10 @@ namespace Twig\TokenParser; use Twig\Error\SyntaxError; +use Twig\Node\EmptyNode; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Token; /** @@ -44,26 +46,26 @@ public function parse(Token $token): Node $targets = []; if ($stream->nextIf('with')) { while (true) { - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); $alias = $name; if ($stream->nextIf('as')) { - $alias = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $alias = $stream->expect(Token::NAME_TYPE)->getValue(); } $targets[$name] = new ConstantExpression($alias, -1); - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - $this->parser->addTrait(new Node(['template' => $template, 'targets' => new Node($targets)])); + $this->parser->addTrait(new Nodes(['template' => $template, 'targets' => new Nodes($targets)])); - return new Node(); + return new EmptyNode($token->getLine()); } public function getTag(): string diff --git a/app/vendor/twig/twig/src/TokenParser/WithTokenParser.php b/app/vendor/twig/twig/src/TokenParser/WithTokenParser.php index 7d8cbe261..8ce4f02b2 100644 --- a/app/vendor/twig/twig/src/TokenParser/WithTokenParser.php +++ b/app/vendor/twig/twig/src/TokenParser/WithTokenParser.php @@ -30,18 +30,18 @@ public function parse(Token $token): Node $variables = null; $only = false; - if (!$stream->test(/* Token::BLOCK_END_TYPE */ 3)) { + if (!$stream->test(Token::BLOCK_END_TYPE)) { $variables = $this->parser->getExpressionParser()->parseExpression(); - $only = (bool) $stream->nextIf(/* Token::NAME_TYPE */ 5, 'only'); + $only = (bool) $stream->nextIf(Token::NAME_TYPE, 'only'); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideWithEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - return new WithNode($body, $variables, $only, $token->getLine(), $this->getTag()); + return new WithNode($body, $variables, $only, $token->getLine()); } public function decideWithEnd(Token $token): bool diff --git a/app/vendor/twig/twig/src/TokenStream.php b/app/vendor/twig/twig/src/TokenStream.php index 1eac11a02..7ee7539f1 100644 --- a/app/vendor/twig/twig/src/TokenStream.php +++ b/app/vendor/twig/twig/src/TokenStream.php @@ -21,21 +21,27 @@ */ final class TokenStream { - private $tokens; private $current = 0; - private $source; - public function __construct(array $tokens, Source $source = null) - { - $this->tokens = $tokens; - $this->source = $source ?: new Source('', ''); + public function __construct( + private array $tokens, + private ?Source $source = null, + ) { + if (null === $this->source) { + trigger_deprecation('twig/twig', '3.16', \sprintf('Not passing a "%s" object to "%s" constructor is deprecated.', Source::class, __CLASS__)); + + $this->source = new Source('', ''); + } } - public function __toString() + public function __toString(): string { return implode("\n", $this->tokens); } + /** + * @return void + */ public function injectTokens(array $tokens) { $this->tokens = array_merge(\array_slice($this->tokens, 0, $this->current), $tokens, \array_slice($this->tokens, $this->current)); @@ -60,24 +66,22 @@ public function next(): Token */ public function nextIf($primary, $secondary = null) { - if ($this->tokens[$this->current]->test($primary, $secondary)) { - return $this->next(); - } + return $this->tokens[$this->current]->test($primary, $secondary) ? $this->next() : null; } /** * Tests a token and returns it or throws a syntax error. */ - public function expect($type, $value = null, string $message = null): Token + public function expect($type, $value = null, ?string $message = null): Token { $token = $this->tokens[$this->current]; if (!$token->test($type, $value)) { $line = $token->getLine(); - throw new SyntaxError(sprintf('%sUnexpected token "%s"%s ("%s" expected%s).', + throw new SyntaxError(\sprintf('%sUnexpected token "%s"%s ("%s" expected%s).', $message ? $message.'. ' : '', - Token::typeToEnglish($token->getType()), - $token->getValue() ? sprintf(' of value "%s"', $token->getValue()) : '', - Token::typeToEnglish($type), $value ? sprintf(' with value "%s"', $value) : ''), + $token->toEnglish(), + $token->getValue() ? \sprintf(' of value "%s"', $token->getValue()) : '', + Token::typeToEnglish($type), $value ? \sprintf(' with value "%s"', $value) : ''), $line, $this->source ); @@ -112,7 +116,7 @@ public function test($primary, $secondary = null): bool */ public function isEOF(): bool { - return /* Token::EOF_TYPE */ -1 === $this->tokens[$this->current]->getType(); + return $this->tokens[$this->current]->test(Token::EOF_TYPE); } public function getCurrent(): Token @@ -120,11 +124,6 @@ public function getCurrent(): Token return $this->tokens[$this->current]; } - /** - * Gets the source associated with this stream. - * - * @internal - */ public function getSourceContext(): Source { return $this->source; diff --git a/app/vendor/twig/twig/src/TwigCallableInterface.php b/app/vendor/twig/twig/src/TwigCallableInterface.php new file mode 100644 index 000000000..2a8ff6116 --- /dev/null +++ b/app/vendor/twig/twig/src/TwigCallableInterface.php @@ -0,0 +1,53 @@ + + */ +interface TwigCallableInterface extends \Stringable +{ + public function getName(): string; + + public function getType(): string; + + public function getDynamicName(): string; + + /** + * @return callable|array{class-string, string}|null + */ + public function getCallable(); + + public function getNodeClass(): string; + + public function needsCharset(): bool; + + public function needsEnvironment(): bool; + + public function needsContext(): bool; + + public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self; + + public function getArguments(): array; + + public function isVariadic(): bool; + + public function isDeprecated(): bool; + + public function getDeprecatingPackage(): string; + + public function getDeprecatedVersion(): string; + + public function getAlternative(): ?string; + + public function getMinimalNumberOfRequiredArguments(): int; +} diff --git a/app/vendor/twig/twig/src/TwigFilter.php b/app/vendor/twig/twig/src/TwigFilter.php index 8993026c8..dece51843 100644 --- a/app/vendor/twig/twig/src/TwigFilter.php +++ b/app/vendor/twig/twig/src/TwigFilter.php @@ -21,72 +21,27 @@ * * @see https://twig.symfony.com/doc/templates.html#filters */ -final class TwigFilter +final class TwigFilter extends AbstractTwigCallable { - private $name; - private $callable; - private $options; - private $arguments = []; - /** * @param callable|array{class-string, string}|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { - $this->name = $name; - $this->callable = $callable; + parent::__construct($name, $callable, $options); + $this->options = array_merge([ - 'needs_environment' => false, - 'needs_context' => false, - 'is_variadic' => false, 'is_safe' => null, 'is_safe_callback' => null, 'pre_escape' => null, 'preserves_safety' => null, 'node_class' => FilterExpression::class, - 'deprecated' => false, - 'alternative' => null, - ], $options); - } - - public function getName(): string - { - return $this->name; - } - - /** - * Returns the callable to execute for this filter. - * - * @return callable|array{class-string, string}|null - */ - public function getCallable() - { - return $this->callable; - } - - public function getNodeClass(): string - { - return $this->options['node_class']; - } - - public function setArguments(array $arguments): void - { - $this->arguments = $arguments; + ], $this->options); } - public function getArguments(): array + public function getType(): string { - return $this->arguments; - } - - public function needsEnvironment(): bool - { - return $this->options['needs_environment']; - } - - public function needsContext(): bool - { - return $this->options['needs_context']; + return 'filter'; } public function getSafe(Node $filterArgs): ?array @@ -99,12 +54,12 @@ public function getSafe(Node $filterArgs): ?array return $this->options['is_safe_callback']($filterArgs); } - return null; + return []; } - public function getPreservesSafety(): ?array + public function getPreservesSafety(): array { - return $this->options['preserves_safety']; + return $this->options['preserves_safety'] ?? []; } public function getPreEscape(): ?string @@ -112,23 +67,8 @@ public function getPreEscape(): ?string return $this->options['pre_escape']; } - public function isVariadic(): bool - { - return $this->options['is_variadic']; - } - - public function isDeprecated(): bool - { - return (bool) $this->options['deprecated']; - } - - public function getDeprecatedVersion(): string - { - return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; - } - - public function getAlternative(): ?string + public function getMinimalNumberOfRequiredArguments(): int { - return $this->options['alternative']; + return parent::getMinimalNumberOfRequiredArguments() + 1; } } diff --git a/app/vendor/twig/twig/src/TwigFunction.php b/app/vendor/twig/twig/src/TwigFunction.php index d910d1fd5..4a10df95e 100644 --- a/app/vendor/twig/twig/src/TwigFunction.php +++ b/app/vendor/twig/twig/src/TwigFunction.php @@ -21,70 +21,31 @@ * * @see https://twig.symfony.com/doc/templates.html#functions */ -final class TwigFunction +final class TwigFunction extends AbstractTwigCallable { - private $name; - private $callable; - private $options; - private $arguments = []; - /** * @param callable|array{class-string, string}|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { - $this->name = $name; - $this->callable = $callable; + parent::__construct($name, $callable, $options); + $this->options = array_merge([ - 'needs_environment' => false, - 'needs_context' => false, - 'is_variadic' => false, 'is_safe' => null, 'is_safe_callback' => null, 'node_class' => FunctionExpression::class, - 'deprecated' => false, - 'alternative' => null, - ], $options); - } - - public function getName(): string - { - return $this->name; - } - - /** - * Returns the callable to execute for this function. - * - * @return callable|array{class-string, string}|null - */ - public function getCallable() - { - return $this->callable; - } - - public function getNodeClass(): string - { - return $this->options['node_class']; - } - - public function setArguments(array $arguments): void - { - $this->arguments = $arguments; + 'parser_callable' => null, + ], $this->options); } - public function getArguments(): array + public function getType(): string { - return $this->arguments; + return 'function'; } - public function needsEnvironment(): bool + public function getParserCallable(): ?callable { - return $this->options['needs_environment']; - } - - public function needsContext(): bool - { - return $this->options['needs_context']; + return $this->options['parser_callable']; } public function getSafe(Node $functionArgs): ?array @@ -99,24 +60,4 @@ public function getSafe(Node $functionArgs): ?array return []; } - - public function isVariadic(): bool - { - return (bool) $this->options['is_variadic']; - } - - public function isDeprecated(): bool - { - return (bool) $this->options['deprecated']; - } - - public function getDeprecatedVersion(): string - { - return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; - } - - public function getAlternative(): ?string - { - return $this->options['alternative']; - } } diff --git a/app/vendor/twig/twig/src/TwigTest.php b/app/vendor/twig/twig/src/TwigTest.php index 3769ec162..5e58ad8b0 100644 --- a/app/vendor/twig/twig/src/TwigTest.php +++ b/app/vendor/twig/twig/src/TwigTest.php @@ -20,81 +20,48 @@ * * @see https://twig.symfony.com/doc/templates.html#test-operator */ -final class TwigTest +final class TwigTest extends AbstractTwigCallable { - private $name; - private $callable; - private $options; - private $arguments = []; - /** * @param callable|array{class-string, string}|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { - $this->name = $name; - $this->callable = $callable; + parent::__construct($name, $callable, $options); + $this->options = array_merge([ - 'is_variadic' => false, 'node_class' => TestExpression::class, - 'deprecated' => false, - 'alternative' => null, 'one_mandatory_argument' => false, - ], $options); - } - - public function getName(): string - { - return $this->name; - } - - /** - * Returns the callable to execute for this test. - * - * @return callable|array{class-string, string}|null - */ - public function getCallable() - { - return $this->callable; + ], $this->options); } - public function getNodeClass(): string + public function getType(): string { - return $this->options['node_class']; + return 'test'; } - public function setArguments(array $arguments): void + public function needsCharset(): bool { - $this->arguments = $arguments; + return false; } - public function getArguments(): array + public function needsEnvironment(): bool { - return $this->arguments; + return false; } - public function isVariadic(): bool + public function needsContext(): bool { - return (bool) $this->options['is_variadic']; + return false; } - public function isDeprecated(): bool - { - return (bool) $this->options['deprecated']; - } - - public function getDeprecatedVersion(): string - { - return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; - } - - public function getAlternative(): ?string + public function hasOneMandatoryArgument(): bool { - return $this->options['alternative']; + return (bool) $this->options['one_mandatory_argument']; } - public function hasOneMandatoryArgument(): bool + public function getMinimalNumberOfRequiredArguments(): int { - return (bool) $this->options['one_mandatory_argument']; + return parent::getMinimalNumberOfRequiredArguments() + 1; } } diff --git a/app/vendor/twig/twig/src/Util/CallableArgumentsExtractor.php b/app/vendor/twig/twig/src/Util/CallableArgumentsExtractor.php new file mode 100644 index 000000000..d8625169d --- /dev/null +++ b/app/vendor/twig/twig/src/Util/CallableArgumentsExtractor.php @@ -0,0 +1,219 @@ + + * + * @internal + */ +final class CallableArgumentsExtractor +{ + private ReflectionCallable $rc; + + public function __construct( + private Node $node, + private TwigCallableInterface $twigCallable, + ) { + $this->rc = new ReflectionCallable($twigCallable); + } + + /** + * @return array + */ + public function extractArguments(Node $arguments): array + { + $extractedArguments = []; + $extractedArgumentNameMap = []; + $named = false; + foreach ($arguments as $name => $node) { + if (!\is_int($name)) { + $named = true; + } elseif ($named) { + throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + $extractedArguments[$normalizedName = $this->normalizeName($name)] = $node; + $extractedArgumentNameMap[$normalizedName] = $name; + } + + if (!$named && !$this->twigCallable->isVariadic()) { + $min = $this->twigCallable->getMinimalNumberOfRequiredArguments(); + if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) { + $argName = $this->toSnakeCase($this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName()); + + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $argName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + return $extractedArguments; + } + + if (!$callable = $this->twigCallable->getCallable()) { + if ($named) { + throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); + } + + throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); + } + + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters(); + $arguments = []; + $callableParameterNames = []; + $missingArguments = []; + $optionalArguments = []; + $pos = 0; + foreach ($callableParameters as $callableParameter) { + $callableParameterName = $callableParameter->name; + if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) { + if ('start' === $callableParameterName) { + $callableParameterName = 'low'; + } elseif ('end' === $callableParameterName) { + $callableParameterName = 'high'; + } + } + + $callableParameterNames[] = $callableParameterName; + $normalizedCallableParameterName = $this->normalizeName($callableParameterName); + + if (\array_key_exists($normalizedCallableParameterName, $extractedArguments)) { + if (\array_key_exists($pos, $extractedArguments)) { + throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + if (\count($missingArguments)) { + throw new SyntaxError(\sprintf( + 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', + $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + ), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $extractedArguments[$normalizedCallableParameterName]; + unset($extractedArguments[$normalizedCallableParameterName]); + $optionalArguments = []; + } elseif (\array_key_exists($pos, $extractedArguments)) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $extractedArguments[$pos]; + unset($extractedArguments[$pos]); + $optionalArguments = []; + ++$pos; + } elseif ($callableParameter->isDefaultValueAvailable()) { + $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), $this->node->getTemplateLine()); + } elseif ($callableParameter->isOptional()) { + if (!$extractedArguments) { + break; + } + + $missingArguments[] = $callableParameterName; + } else { + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->toSnakeCase($callableParameterName), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + } + + if ($this->twigCallable->isVariadic()) { + $arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], $this->node->getTemplateLine()) : new ArrayExpression([], $this->node->getTemplateLine()); + foreach ($extractedArguments as $key => $value) { + if (\is_int($key)) { + $arbitraryArguments->addElement($value); + } else { + $originalKey = $extractedArgumentNameMap[$key]; + if ($originalKey !== $this->toSnakeCase($originalKey)) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Using "snake_case" for variadic arguments is required for a smooth upgrade with Twig 4.0; rename "%s" to "%s" in "%s" at line %d.', $originalKey, $this->toSnakeCase($originalKey), $this->node->getSourceContext()->getName(), $this->node->getTemplateLine())); + } + $arbitraryArguments->addElement($value, new ConstantExpression($this->toSnakeCase($originalKey), $this->node->getTemplateLine())); + // I Twig 4.0, don't convert the key: + // $arbitraryArguments->addElement($value, new ConstantExpression($originalKey, $this->node->getTemplateLine())); + } + unset($extractedArguments[$key]); + } + + if ($arbitraryArguments->count()) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $arbitraryArguments; + } + } + + if ($extractedArguments) { + $unknownArgument = null; + foreach ($extractedArguments as $extractedArgument) { + if ($extractedArgument instanceof Node) { + $unknownArgument = $extractedArgument; + break; + } + } + + throw new SyntaxError( + \sprintf( + 'Unknown argument%s "%s" for %s "%s(%s)".', + \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)) + ), + $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(), + $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext() + ); + } + + return $arguments; + } + + private function normalizeName(string $name): string + { + return strtolower(str_replace('_', '', $name)); + } + + private function toSnakeCase(string $name): string + { + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z0-9])([A-Z])/'], '\1_\2', $name)); + } + + private function getCallableParameters(): array + { + $parameters = $this->rc->getReflector()->getParameters(); + if ($this->node->hasNode('node')) { + array_shift($parameters); + } + if ($this->twigCallable->needsCharset()) { + array_shift($parameters); + } + if ($this->twigCallable->needsEnvironment()) { + array_shift($parameters); + } + if ($this->twigCallable->needsContext()) { + array_shift($parameters); + } + foreach ($this->twigCallable->getArguments() as $argument) { + array_shift($parameters); + } + + $isPhpVariadic = false; + if ($this->twigCallable->isVariadic()) { + $argument = end($parameters); + $isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName(); + if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) { + array_pop($parameters); + } elseif ($argument && $argument->isVariadic()) { + array_pop($parameters); + $isPhpVariadic = true; + } else { + throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $this->rc->getName(), $this->twigCallable->getType(), $this->twigCallable->getName())); + } + } + + return [$parameters, $isPhpVariadic]; + } +} diff --git a/app/vendor/twig/twig/src/Util/DeprecationCollector.php b/app/vendor/twig/twig/src/Util/DeprecationCollector.php index 378b666bd..0ea26ed4b 100644 --- a/app/vendor/twig/twig/src/Util/DeprecationCollector.php +++ b/app/vendor/twig/twig/src/Util/DeprecationCollector.php @@ -20,11 +20,9 @@ */ final class DeprecationCollector { - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; + public function __construct( + private Environment $twig, + ) { } /** @@ -60,6 +58,8 @@ public function collect(\Traversable $iterator): array if (\E_USER_DEPRECATED === $type) { $deprecations[] = $msg; } + + return false; }); foreach ($iterator as $name => $contents) { diff --git a/app/vendor/twig/twig/src/Util/ReflectionCallable.php b/app/vendor/twig/twig/src/Util/ReflectionCallable.php new file mode 100644 index 000000000..0298e291d --- /dev/null +++ b/app/vendor/twig/twig/src/Util/ReflectionCallable.php @@ -0,0 +1,95 @@ + + * + * @internal + */ +final class ReflectionCallable +{ + private $reflector; + private $callable; + private $name; + + public function __construct( + TwigCallableInterface $twigCallable, + ) { + $callable = $twigCallable->getCallable(); + if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { + $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; + } + + if (\is_array($callable) && method_exists($callable[0], $callable[1])) { + $this->reflector = $r = new \ReflectionMethod($callable[0], $callable[1]); + $this->callable = $callable; + $this->name = $r->class.'::'.$r->name; + + return; + } + + $checkVisibility = $callable instanceof \Closure; + try { + $closure = \Closure::fromCallable($callable); + } catch (\TypeError $e) { + throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $twigCallable->getType(), $twigCallable->getName()), 0, $e); + } + $this->reflector = $r = new \ReflectionFunction($closure); + + if (str_contains($r->name, '{closure')) { + $this->callable = $callable; + $this->name = 'Closure'; + + return; + } + + if ($object = $r->getClosureThis()) { + $callable = [$object, $r->name]; + $this->name = get_debug_type($object).'::'.$r->name; + } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { + $callable = [$class->name, $r->name]; + $this->name = $class->name.'::'.$r->name; + } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { + $callable = [\is_array($callable) ? $callable[0] : $class->name, $r->name]; + $this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name; + } else { + $callable = $this->name = $r->name; + } + + if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) { + $callable = $r->getClosure(); + } + + $this->callable = $callable; + } + + public function getReflector(): \ReflectionFunctionAbstract + { + return $this->reflector; + } + + /** + * @return callable + */ + public function getCallable() + { + return $this->callable; + } + + public function getName(): string + { + return $this->name; + } +}