File manager - Edit - /home/autoph/public_html/projects/Rating-AutoHub/public/css/symfony.tar
Back
polyfill-uuid/bootstrap.php 0000644 00000005443 15025017654 0012103 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Uuid as p; if (extension_loaded('uuid')) { return; } if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; } if (!defined('UUID_VARIANT_NCS')) { define('UUID_VARIANT_NCS', 0); } if (!defined('UUID_VARIANT_DCE')) { define('UUID_VARIANT_DCE', 1); } if (!defined('UUID_VARIANT_MICROSOFT')) { define('UUID_VARIANT_MICROSOFT', 2); } if (!defined('UUID_VARIANT_OTHER')) { define('UUID_VARIANT_OTHER', 3); } if (!defined('UUID_TYPE_DEFAULT')) { define('UUID_TYPE_DEFAULT', 0); } if (!defined('UUID_TYPE_TIME')) { define('UUID_TYPE_TIME', 1); } if (!defined('UUID_TYPE_MD5')) { define('UUID_TYPE_MD5', 3); } if (!defined('UUID_TYPE_DCE')) { define('UUID_TYPE_DCE', 4); // Deprecated alias } if (!defined('UUID_TYPE_NAME')) { define('UUID_TYPE_NAME', 1); // Deprecated alias } if (!defined('UUID_TYPE_RANDOM')) { define('UUID_TYPE_RANDOM', 4); } if (!defined('UUID_TYPE_SHA1')) { define('UUID_TYPE_SHA1', 5); } if (!defined('UUID_TYPE_NULL')) { define('UUID_TYPE_NULL', -1); } if (!defined('UUID_TYPE_INVALID')) { define('UUID_TYPE_INVALID', -42); } if (!function_exists('uuid_create')) { function uuid_create($uuid_type = \UUID_TYPE_DEFAULT) { return p\Uuid::uuid_create($uuid_type); } } if (!function_exists('uuid_generate_md5')) { function uuid_generate_md5($uuid_ns, $name) { return p\Uuid::uuid_generate_md5($uuid_ns, $name); } } if (!function_exists('uuid_generate_sha1')) { function uuid_generate_sha1($uuid_ns, $name) { return p\Uuid::uuid_generate_sha1($uuid_ns, $name); } } if (!function_exists('uuid_is_valid')) { function uuid_is_valid($uuid) { return p\Uuid::uuid_is_valid($uuid); } } if (!function_exists('uuid_compare')) { function uuid_compare($uuid1, $uuid2) { return p\Uuid::uuid_compare($uuid1, $uuid2); } } if (!function_exists('uuid_is_null')) { function uuid_is_null($uuid) { return p\Uuid::uuid_is_null($uuid); } } if (!function_exists('uuid_type')) { function uuid_type($uuid) { return p\Uuid::uuid_type($uuid); } } if (!function_exists('uuid_variant')) { function uuid_variant($uuid) { return p\Uuid::uuid_variant($uuid); } } if (!function_exists('uuid_time')) { function uuid_time($uuid) { return p\Uuid::uuid_time($uuid); } } if (!function_exists('uuid_mac')) { function uuid_mac($uuid) { return p\Uuid::uuid_mac($uuid); } } if (!function_exists('uuid_parse')) { function uuid_parse($uuid) { return p\Uuid::uuid_parse($uuid); } } if (!function_exists('uuid_unparse')) { function uuid_unparse($uuid) { return p\Uuid::uuid_unparse($uuid); } } polyfill-uuid/composer.json 0000644 00000002003 15025017654 0012064 0 ustar 00 { "name": "symfony/polyfill-uuid", "type": "library", "description": "Symfony polyfill for uuid functions", "keywords": ["polyfill", "compatibility", "portable", "uuid"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Grégoire Pineau", "email": "lyrixx@lyrixx.info" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.1" }, "provide": { "ext-uuid": "*" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Uuid\\": "" }, "files": [ "bootstrap.php" ] }, "suggest": { "ext-uuid": "For best performance" }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } polyfill-uuid/README.md 0000644 00000000533 15025017654 0010627 0 ustar 00 Symfony Polyfill / Uuid ======================== This component provides `uuid_*` functions to users who run PHP versions without the uuid extension. More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). polyfill-uuid/LICENSE 0000644 00000002051 15025017654 0010352 0 ustar 00 Copyright (c) 2018-2019 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. polyfill-uuid/bootstrap80.php 0000644 00000005753 15025017654 0012257 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Uuid as p; if (!defined('UUID_VARIANT_NCS')) { define('UUID_VARIANT_NCS', 0); } if (!defined('UUID_VARIANT_DCE')) { define('UUID_VARIANT_DCE', 1); } if (!defined('UUID_VARIANT_MICROSOFT')) { define('UUID_VARIANT_MICROSOFT', 2); } if (!defined('UUID_VARIANT_OTHER')) { define('UUID_VARIANT_OTHER', 3); } if (!defined('UUID_TYPE_DEFAULT')) { define('UUID_TYPE_DEFAULT', 0); } if (!defined('UUID_TYPE_TIME')) { define('UUID_TYPE_TIME', 1); } if (!defined('UUID_TYPE_MD5')) { define('UUID_TYPE_MD5', 3); } if (!defined('UUID_TYPE_DCE')) { define('UUID_TYPE_DCE', 4); // Deprecated alias } if (!defined('UUID_TYPE_NAME')) { define('UUID_TYPE_NAME', 1); // Deprecated alias } if (!defined('UUID_TYPE_RANDOM')) { define('UUID_TYPE_RANDOM', 4); } if (!defined('UUID_TYPE_SHA1')) { define('UUID_TYPE_SHA1', 5); } if (!defined('UUID_TYPE_NULL')) { define('UUID_TYPE_NULL', -1); } if (!defined('UUID_TYPE_INVALID')) { define('UUID_TYPE_INVALID', -42); } if (!function_exists('uuid_create')) { function uuid_create(?int $uuid_type = \UUID_TYPE_DEFAULT): string { return p\Uuid::uuid_create((int) $uuid_type); } } if (!function_exists('uuid_generate_md5')) { function uuid_generate_md5(?string $uuid_ns, ?string $name): string { return p\Uuid::uuid_generate_md5((string) $uuid_ns, (string) $name); } } if (!function_exists('uuid_generate_sha1')) { function uuid_generate_sha1(?string $uuid_ns, ?string $name): string { return p\Uuid::uuid_generate_sha1((string) $uuid_ns, (string) $name); } } if (!function_exists('uuid_is_valid')) { function uuid_is_valid(?string $uuid): bool { return p\Uuid::uuid_is_valid((string) $uuid); } } if (!function_exists('uuid_compare')) { function uuid_compare(?string $uuid1, ?string $uuid2): int { return p\Uuid::uuid_compare((string) $uuid1, (string) $uuid2); } } if (!function_exists('uuid_is_null')) { function uuid_is_null(?string $uuid): bool { return p\Uuid::uuid_is_null((string) $uuid); } } if (!function_exists('uuid_type')) { function uuid_type(?string $uuid): int { return p\Uuid::uuid_type((string) $uuid); } } if (!function_exists('uuid_variant')) { function uuid_variant(?string $uuid): int { return p\Uuid::uuid_variant((string) $uuid); } } if (!function_exists('uuid_time')) { function uuid_time(?string $uuid): int { return p\Uuid::uuid_time((string) $uuid); } } if (!function_exists('uuid_mac')) { function uuid_mac(?string $uuid): string { return p\Uuid::uuid_mac((string) $uuid); } } if (!function_exists('uuid_parse')) { function uuid_parse(?string $uuid): string { return p\Uuid::uuid_parse((string) $uuid); } } if (!function_exists('uuid_unparse')) { function uuid_unparse(?string $uuid): string { return p\Uuid::uuid_unparse((string) $uuid); } } polyfill-uuid/Uuid.php 0000644 00000041336 15025017654 0010775 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Uuid; /** * @internal * * @author Grégoire Pineau <lyrixx@lyrixx.info> */ final class Uuid { public const UUID_VARIANT_NCS = 0; public const UUID_VARIANT_DCE = 1; public const UUID_VARIANT_MICROSOFT = 2; public const UUID_VARIANT_OTHER = 3; public const UUID_TYPE_DEFAULT = 0; public const UUID_TYPE_TIME = 1; public const UUID_TYPE_MD5 = 3; public const UUID_TYPE_DCE = 4; // Deprecated alias public const UUID_TYPE_NAME = 1; // Deprecated alias public const UUID_TYPE_RANDOM = 4; public const UUID_TYPE_SHA1 = 5; public const UUID_TYPE_NULL = -1; public const UUID_TYPE_INVALID = -42; // https://tools.ietf.org/html/rfc4122#section-4.1.4 // 0x01b21dd213814000 is the number of 100-ns intervals between the // UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00. public const TIME_OFFSET_INT = 0x01B21DD213814000; public const TIME_OFFSET_BIN = "\x01\xb2\x1d\xd2\x13\x81\x40\x00"; public const TIME_OFFSET_COM = "\xfe\x4d\xe2\x2d\xec\x7e\xc0\x00"; public static function uuid_create($uuid_type = \UUID_TYPE_DEFAULT) { if (!is_numeric($uuid_type) && null !== $uuid_type) { trigger_error(sprintf('uuid_create() expects parameter 1 to be int, %s given', \gettype($uuid_type)), \E_USER_WARNING); return null; } switch ((int) $uuid_type) { case self::UUID_TYPE_NAME: case self::UUID_TYPE_TIME: return self::uuid_generate_time(); case self::UUID_TYPE_DCE: case self::UUID_TYPE_RANDOM: case self::UUID_TYPE_DEFAULT: return self::uuid_generate_random(); default: trigger_error(sprintf("Unknown/invalid UUID type '%d' requested, using default type instead", $uuid_type), \E_USER_WARNING); return self::uuid_generate_random(); } } public static function uuid_generate_md5($uuid_ns, $name) { if (!\is_string($uuid_ns = self::toString($uuid_ns))) { trigger_error(sprintf('uuid_generate_md5() expects parameter 1 to be string, %s given', \gettype($uuid_ns)), \E_USER_WARNING); return null; } if (!\is_string($name = self::toString($name))) { trigger_error(sprintf('uuid_generate_md5() expects parameter 2 to be string, %s given', \gettype($name)), \E_USER_WARNING); return null; } if (!self::isValid($uuid_ns)) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('uuid_generate_md5(): Argument #1 ($uuid_ns) UUID expected'); } $hash = md5(hex2bin(str_replace('-', '', $uuid_ns)).$name); return sprintf('%08s-%04s-3%03s-%04x-%012s', // 32 bits for "time_low" substr($hash, 0, 8), // 16 bits for "time_mid" substr($hash, 8, 4), // 16 bits for "time_hi_and_version", // four most significant bits holds version number 3 substr($hash, 13, 3), // 16 bits: // * 8 bits for "clk_seq_hi_res", // * 8 bits for "clk_seq_low", hexdec(substr($hash, 16, 4)) & 0x3FFF | 0x8000, // 48 bits for "node" substr($hash, 20, 12) ); } public static function uuid_generate_sha1($uuid_ns, $name) { if (!\is_string($uuid_ns = self::toString($uuid_ns))) { trigger_error(sprintf('uuid_generate_sha1() expects parameter 1 to be string, %s given', \gettype($uuid_ns)), \E_USER_WARNING); return null; } if (!\is_string($name = self::toString($name))) { trigger_error(sprintf('uuid_generate_sha1() expects parameter 2 to be string, %s given', \gettype($name)), \E_USER_WARNING); return null; } if (!self::isValid($uuid_ns)) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('uuid_generate_sha1(): Argument #1 ($uuid_ns) UUID expected'); } $hash = sha1(hex2bin(str_replace('-', '', $uuid_ns)).$name); return sprintf('%08s-%04s-5%03s-%04x-%012s', // 32 bits for "time_low" substr($hash, 0, 8), // 16 bits for "time_mid" substr($hash, 8, 4), // 16 bits for "time_hi_and_version", // four most significant bits holds version number 5 substr($hash, 13, 3), // 16 bits: // * 8 bits for "clk_seq_hi_res", // * 8 bits for "clk_seq_low", // WARNING: On old libuuid version, there is a bug. 0x0fff is used instead of 0x3fff // See https://github.com/karelzak/util-linux/commit/d6ddf07d31dfdc894eb8e7e6842aa856342c526e hexdec(substr($hash, 16, 4)) & 0x3FFF | 0x8000, // 48 bits for "node" substr($hash, 20, 12) ); } public static function uuid_is_valid($uuid) { if (!\is_string($uuid = self::toString($uuid))) { trigger_error(sprintf('uuid_is_valid() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); return null; } return self::isValid($uuid); } public static function uuid_compare($uuid1, $uuid2) { if (!\is_string($uuid1 = self::toString($uuid1))) { trigger_error(sprintf('uuid_compare() expects parameter 1 to be string, %s given', \gettype($uuid1)), \E_USER_WARNING); return null; } if (!\is_string($uuid2 = self::toString($uuid2))) { trigger_error(sprintf('uuid_compare() expects parameter 2 to be string, %s given', \gettype($uuid2)), \E_USER_WARNING); return null; } if (!self::isValid($uuid1)) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('uuid_compare(): Argument #1 ($uuid1) UUID expected'); } if (!self::isValid($uuid2)) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('uuid_compare(): Argument #2 ($uuid2) UUID expected'); } return strcasecmp($uuid1, $uuid2); } public static function uuid_is_null($uuid) { if (!\is_string($uuid = self::toString($uuid))) { trigger_error(sprintf('uuid_is_null() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); return null; } if (80000 <= \PHP_VERSION_ID && !self::isValid($uuid)) { throw new \ValueError('uuid_is_null(): Argument #1 ($uuid) UUID expected'); } return '00000000-0000-0000-0000-000000000000' === $uuid; } public static function uuid_type($uuid) { if (!\is_string($uuid = self::toString($uuid))) { trigger_error(sprintf('uuid_type() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); return null; } if ('00000000-0000-0000-0000-000000000000' === $uuid) { return self::UUID_TYPE_NULL; } if (null === $parsed = self::parse($uuid)) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('uuid_type(): Argument #1 ($uuid) UUID expected'); } return $parsed['version']; } public static function uuid_variant($uuid) { if (!\is_string($uuid = self::toString($uuid))) { trigger_error(sprintf('uuid_variant() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); return null; } if ('00000000-0000-0000-0000-000000000000' === $uuid) { return self::UUID_TYPE_NULL; } if (null === $parsed = self::parse($uuid)) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('uuid_variant(): Argument #1 ($uuid) UUID expected'); } if (($parsed['clock_seq'] & 0x8000) === 0) { return self::UUID_VARIANT_NCS; } if (($parsed['clock_seq'] & 0x4000) === 0) { return self::UUID_VARIANT_DCE; } if (($parsed['clock_seq'] & 0x2000) === 0) { return self::UUID_VARIANT_MICROSOFT; } return self::UUID_VARIANT_OTHER; } public static function uuid_time($uuid) { if (!\is_string($uuid = self::toString($uuid))) { trigger_error(sprintf('uuid_time() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); return null; } $parsed = self::parse($uuid); if (self::UUID_TYPE_TIME !== ($parsed['version'] ?? null)) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('uuid_time(): Argument #1 ($uuid) UUID DCE TIME expected'); } if (\PHP_INT_SIZE >= 8) { return intdiv(hexdec($parsed['time']) - self::TIME_OFFSET_INT, 10000000); } $time = str_pad(hex2bin($parsed['time']), 8, "\0", \STR_PAD_LEFT); $time = self::binaryAdd($time, self::TIME_OFFSET_COM); $time[0] = $time[0] & "\x7F"; return (int) substr(self::toDecimal($time), 0, -7); } public static function uuid_mac($uuid) { if (!\is_string($uuid = self::toString($uuid))) { trigger_error(sprintf('uuid_mac() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); return null; } $parsed = self::parse($uuid); if (self::UUID_TYPE_TIME !== ($parsed['version'] ?? null)) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('uuid_mac(): Argument #1 ($uuid) UUID DCE TIME expected'); } return strtr($parsed['node'], 'ABCDEF', 'abcdef'); } public static function uuid_parse($uuid) { if (!\is_string($uuid = self::toString($uuid))) { trigger_error(sprintf('uuid_parse() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); return null; } if (!self::isValid($uuid)) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('uuid_parse(): Argument #1 ($uuid) UUID expected'); } return hex2bin(str_replace('-', '', $uuid)); } public static function uuid_unparse($bytes) { if (!\is_string($bytes = self::toString($bytes))) { trigger_error(sprintf('uuid_unparse() expects parameter 1 to be string, %s given', \gettype($bytes)), \E_USER_WARNING); return null; } if (16 !== \strlen($bytes)) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('uuid_unparse(): Argument #1 ($uuid) UUID expected'); } $uuid = bin2hex($bytes); $uuid = substr_replace($uuid, '-', 8, 0); $uuid = substr_replace($uuid, '-', 13, 0); $uuid = substr_replace($uuid, '-', 18, 0); return substr_replace($uuid, '-', 23, 0); } private static function uuid_generate_random() { $uuid = bin2hex(random_bytes(16)); return sprintf('%08s-%04s-4%03s-%04x-%012s', // 32 bits for "time_low" substr($uuid, 0, 8), // 16 bits for "time_mid" substr($uuid, 8, 4), // 16 bits for "time_hi_and_version", // four most significant bits holds version number 4 substr($uuid, 13, 3), // 16 bits: // * 8 bits for "clk_seq_hi_res", // * 8 bits for "clk_seq_low", // two most significant bits holds zero and one for variant DCE1.1 hexdec(substr($uuid, 16, 4)) & 0x3FFF | 0x8000, // 48 bits for "node" substr($uuid, 20, 12) ); } /** * @see http://tools.ietf.org/html/rfc4122#section-4.2.2 */ private static function uuid_generate_time() { $time = microtime(false); $time = substr($time, 11).substr($time, 2, 7); if (\PHP_INT_SIZE >= 8) { $time = str_pad(dechex($time + self::TIME_OFFSET_INT), 16, '0', \STR_PAD_LEFT); } else { $time = str_pad(self::toBinary($time), 8, "\0", \STR_PAD_LEFT); $time = self::binaryAdd($time, self::TIME_OFFSET_BIN); $time = bin2hex($time); } // https://tools.ietf.org/html/rfc4122#section-4.1.5 // We are using a random data for the sake of simplicity: since we are // not able to get a super precise timeOfDay as a unique sequence $clockSeq = random_int(0, 0x3FFF); static $node; if (null === $node) { if (\function_exists('apcu_fetch')) { $node = apcu_fetch('__symfony_uuid_node'); if (false === $node) { $node = sprintf('%06x%06x', random_int(0, 0xFFFFFF) | 0x010000, random_int(0, 0xFFFFFF) ); apcu_store('__symfony_uuid_node', $node); } } else { $node = sprintf('%06x%06x', random_int(0, 0xFFFFFF) | 0x010000, random_int(0, 0xFFFFFF) ); } } return sprintf('%08s-%04s-1%03s-%04x-%012s', // 32 bits for "time_low" substr($time, -8), // 16 bits for "time_mid" substr($time, -12, 4), // 16 bits for "time_hi_and_version", // four most significant bits holds version number 1 substr($time, -15, 3), // 16 bits: // * 8 bits for "clk_seq_hi_res", // * 8 bits for "clk_seq_low", // two most significant bits holds zero and one for variant DCE1.1 $clockSeq | 0x8000, // 48 bits for "node" $node ); } private static function isValid($uuid) { return (bool) preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $uuid); } private static function parse($uuid) { if (!preg_match('{^(?<time_low>[0-9a-f]{8})-(?<time_mid>[0-9a-f]{4})-(?<version>[0-9a-f])(?<time_hi>[0-9a-f]{3})-(?<clock_seq>[0-9a-f]{4})-(?<node>[0-9a-f]{12})$}Di', $uuid, $matches)) { return null; } return [ 'time' => '0'.$matches['time_hi'].$matches['time_mid'].$matches['time_low'], 'version' => hexdec($matches['version']), 'clock_seq' => hexdec($matches['clock_seq']), 'node' => $matches['node'], ]; } private static function toString($v) { if (\is_string($v) || null === $v || (\is_object($v) ? method_exists($v, '__toString') : \is_scalar($v))) { return (string) $v; } return $v; } private static function toBinary($digits) { $bytes = ''; $count = \strlen($digits); while ($count) { $quotient = []; $remainder = 0; for ($i = 0; $i !== $count; ++$i) { $carry = $digits[$i] + $remainder * 10; $digit = $carry >> 8; $remainder = $carry & 0xFF; if ($digit || $quotient) { $quotient[] = $digit; } } $bytes = \chr($remainder).$bytes; $count = \count($digits = $quotient); } return $bytes; } private static function toDecimal($bytes) { $digits = ''; $bytes = array_values(unpack('C*', $bytes)); while ($count = \count($bytes)) { $quotient = []; $remainder = 0; for ($i = 0; $i !== $count; ++$i) { $carry = $bytes[$i] + ($remainder << 8); $digit = (int) ($carry / 10); $remainder = $carry % 10; if ($digit || $quotient) { $quotient[] = $digit; } } $digits = $remainder.$digits; $bytes = $quotient; } return $digits; } private static function binaryAdd($a, $b) { $sum = 0; for ($i = 7; 0 <= $i; --$i) { $sum += \ord($a[$i]) + \ord($b[$i]); $a[$i] = \chr($sum & 0xFF); $sum >>= 8; } return $a; } } yaml/Yaml.php 0000644 00000005515 15025017654 0007134 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; use Symfony\Component\Yaml\Exception\ParseException; /** * Yaml offers convenience methods to load and dump YAML. * * @author Fabien Potencier <fabien@symfony.com> * * @final */ class Yaml { public const DUMP_OBJECT = 1; public const PARSE_EXCEPTION_ON_INVALID_TYPE = 2; public const PARSE_OBJECT = 4; public const PARSE_OBJECT_FOR_MAP = 8; public const DUMP_EXCEPTION_ON_INVALID_TYPE = 16; public const PARSE_DATETIME = 32; public const DUMP_OBJECT_AS_MAP = 64; public const DUMP_MULTI_LINE_LITERAL_BLOCK = 128; public const PARSE_CONSTANT = 256; public const PARSE_CUSTOM_TAGS = 512; public const DUMP_EMPTY_ARRAY_AS_SEQUENCE = 1024; public const DUMP_NULL_AS_TILDE = 2048; /** * Parses a YAML file into a PHP value. * * Usage: * * $array = Yaml::parseFile('config.yml'); * print_r($array); * * @param string $filename The path to the YAML file to be parsed * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior * * @throws ParseException If the file could not be read or the YAML is not valid */ public static function parseFile(string $filename, int $flags = 0): mixed { $yaml = new Parser(); return $yaml->parseFile($filename, $flags); } /** * Parses YAML into a PHP value. * * Usage: * <code> * $array = Yaml::parse(file_get_contents('config.yml')); * print_r($array); * </code> * * @param string $input A string containing YAML * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior * * @throws ParseException If the YAML is not valid */ public static function parse(string $input, int $flags = 0): mixed { $yaml = new Parser(); return $yaml->parse($input, $flags); } /** * Dumps a PHP value to a YAML string. * * The dump method, when supplied with an array, will do its best * to convert the array into friendly YAML. * * @param mixed $input The PHP value * @param int $inline The level where you switch to inline YAML * @param int $indent The amount of spaces to use for indentation of nested nodes * @param int $flags A bit field of DUMP_* constants to customize the dumped YAML string */ public static function dump(mixed $input, int $inline = 2, int $indent = 4, int $flags = 0): string { $yaml = new Dumper($indent); return $yaml->dump($input, $inline, 0, $flags); } } yaml/Escaper.php 0000644 00000007367 15025017654 0007623 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; /** * Escaper encapsulates escaping rules for single and double-quoted * YAML strings. * * @author Matthew Lewinski <matthew@lewinski.org> * * @internal */ class Escaper { // Characters that would cause a dumped string to require double quoting. public const REGEX_CHARACTER_TO_ESCAPE = "[\\x00-\\x1f]|\x7f|\xc2\x85|\xc2\xa0|\xe2\x80\xa8|\xe2\x80\xa9"; // Mapping arrays for escaping a double quoted string. The backslash is // first to ensure proper escaping because str_replace operates iteratively // on the input arrays. This ordering of the characters avoids the use of strtr, // which performs more slowly. private const ESCAPEES = ['\\', '\\\\', '\\"', '"', "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08", "\x09", "\x0a", "\x0b", "\x0c", "\x0d", "\x0e", "\x0f", "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17", "\x18", "\x19", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f", "\x7f", "\xc2\x85", "\xc2\xa0", "\xe2\x80\xa8", "\xe2\x80\xa9", ]; private const ESCAPED = ['\\\\', '\\"', '\\\\', '\\"', '\\0', '\\x01', '\\x02', '\\x03', '\\x04', '\\x05', '\\x06', '\\a', '\\b', '\\t', '\\n', '\\v', '\\f', '\\r', '\\x0e', '\\x0f', '\\x10', '\\x11', '\\x12', '\\x13', '\\x14', '\\x15', '\\x16', '\\x17', '\\x18', '\\x19', '\\x1a', '\\e', '\\x1c', '\\x1d', '\\x1e', '\\x1f', '\\x7f', '\\N', '\\_', '\\L', '\\P', ]; /** * Determines if a PHP value would require double quoting in YAML. * * @param string $value A PHP value */ public static function requiresDoubleQuoting(string $value): bool { return 0 < preg_match('/'.self::REGEX_CHARACTER_TO_ESCAPE.'/u', $value); } /** * Escapes and surrounds a PHP value with double quotes. * * @param string $value A PHP value */ public static function escapeWithDoubleQuotes(string $value): string { return sprintf('"%s"', str_replace(self::ESCAPEES, self::ESCAPED, $value)); } /** * Determines if a PHP value would require single quoting in YAML. * * @param string $value A PHP value */ public static function requiresSingleQuoting(string $value): bool { // Determines if a PHP value is entirely composed of a value that would // require single quoting in YAML. if (\in_array(strtolower($value), ['null', '~', 'true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off'])) { return true; } // Determines if the PHP value contains any single characters that would // cause it to require single quoting in YAML. return 0 < preg_match('/[ \s \' " \: \{ \} \[ \] , & \* \# \?] | \A[ \- ? | < > = ! % @ ` \p{Zs}]/xu', $value); } /** * Escapes and surrounds a PHP value with single quotes. * * @param string $value A PHP value */ public static function escapeWithSingleQuotes(string $value): string { return sprintf("'%s'", str_replace('\'', '\'\'', $value)); } } yaml/Dumper.php 0000644 00000015765 15025017654 0007476 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; use Symfony\Component\Yaml\Tag\TaggedValue; /** * Dumper dumps PHP variables to YAML strings. * * @author Fabien Potencier <fabien@symfony.com> * * @final */ class Dumper { /** * The amount of spaces to use for indentation of nested nodes. */ private int $indentation; public function __construct(int $indentation = 4) { if ($indentation < 1) { throw new \InvalidArgumentException('The indentation must be greater than zero.'); } $this->indentation = $indentation; } /** * Dumps a PHP value to YAML. * * @param mixed $input The PHP value * @param int $inline The level where you switch to inline YAML * @param int $indent The level of indentation (used internally) * @param int $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string */ public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags = 0): string { $output = ''; $prefix = $indent ? str_repeat(' ', $indent) : ''; $dumpObjectAsInlineMap = true; if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($input instanceof \ArrayObject || $input instanceof \stdClass)) { $dumpObjectAsInlineMap = empty((array) $input); } if ($inline <= 0 || (!\is_array($input) && !$input instanceof TaggedValue && $dumpObjectAsInlineMap) || empty($input)) { $output .= $prefix.Inline::dump($input, $flags); } elseif ($input instanceof TaggedValue) { $output .= $this->dumpTaggedValue($input, $inline, $indent, $flags, $prefix); } else { $dumpAsMap = Inline::isHash($input); foreach ($input as $key => $value) { if ('' !== $output && "\n" !== $output[-1]) { $output .= "\n"; } if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value) && false !== strpos($value, "\n") && false === strpos($value, "\r")) { // If the first line starts with a space character, the spec requires a blockIndicationIndicator // http://www.yaml.org/spec/1.2/spec.html#id2793979 $blockIndentationIndicator = (' ' === substr($value, 0, 1)) ? (string) $this->indentation : ''; if (isset($value[-2]) && "\n" === $value[-2] && "\n" === $value[-1]) { $blockChompingIndicator = '+'; } elseif ("\n" === $value[-1]) { $blockChompingIndicator = ''; } else { $blockChompingIndicator = '-'; } $output .= sprintf('%s%s%s |%s%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', '', $blockIndentationIndicator, $blockChompingIndicator); foreach (explode("\n", $value) as $row) { if ('' === $row) { $output .= "\n"; } else { $output .= sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); } } continue; } if ($value instanceof TaggedValue) { $output .= sprintf('%s%s !%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', $value->getTag()); if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && false !== strpos($value->getValue(), "\n") && false === strpos($value->getValue(), "\r\n")) { // If the first line starts with a space character, the spec requires a blockIndicationIndicator // http://www.yaml.org/spec/1.2/spec.html#id2793979 $blockIndentationIndicator = (' ' === substr($value->getValue(), 0, 1)) ? (string) $this->indentation : ''; $output .= sprintf(' |%s', $blockIndentationIndicator); foreach (explode("\n", $value->getValue()) as $row) { $output .= sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); } continue; } if ($inline - 1 <= 0 || null === $value->getValue() || \is_scalar($value->getValue())) { $output .= ' '.$this->dump($value->getValue(), $inline - 1, 0, $flags)."\n"; } else { $output .= "\n"; $output .= $this->dump($value->getValue(), $inline - 1, $dumpAsMap ? $indent + $this->indentation : $indent + 2, $flags); } continue; } $dumpObjectAsInlineMap = true; if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($value instanceof \ArrayObject || $value instanceof \stdClass)) { $dumpObjectAsInlineMap = empty((array) $value); } $willBeInlined = $inline - 1 <= 0 || !\is_array($value) && $dumpObjectAsInlineMap || empty($value); $output .= sprintf('%s%s%s%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', $willBeInlined ? ' ' : "\n", $this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags) ).($willBeInlined ? "\n" : ''); } } return $output; } private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, int $flags, string $prefix): string { $output = sprintf('%s!%s', $prefix ? $prefix.' ' : '', $value->getTag()); if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && false !== strpos($value->getValue(), "\n") && false === strpos($value->getValue(), "\r\n")) { // If the first line starts with a space character, the spec requires a blockIndicationIndicator // http://www.yaml.org/spec/1.2/spec.html#id2793979 $blockIndentationIndicator = (' ' === substr($value->getValue(), 0, 1)) ? (string) $this->indentation : ''; $output .= sprintf(' |%s', $blockIndentationIndicator); foreach (explode("\n", $value->getValue()) as $row) { $output .= sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); } return $output; } if ($inline - 1 <= 0 || null === $value->getValue() || \is_scalar($value->getValue())) { return $output.' '.$this->dump($value->getValue(), $inline - 1, 0, $flags)."\n"; } return $output."\n".$this->dump($value->getValue(), $inline - 1, $indent, $flags); } } yaml/composer.json 0000644 00000001727 15025017654 0010244 0 ustar 00 { "name": "symfony/yaml", "type": "library", "description": "Loads and dumps YAML files", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.0.2", "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "symfony/console": "^5.4|^6.0" }, "conflict": { "symfony/console": "<5.4" }, "suggest": { "symfony/console": "For validating YAML files using the lint command" }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "bin": [ "Resources/bin/yaml-lint" ], "minimum-stability": "dev" } yaml/CHANGELOG.md 0000644 00000015466 15025017654 0007340 0 ustar 00 CHANGELOG ========= 5.4 --- * Add new `lint:yaml dirname --exclude=/dirname/foo.yaml --exclude=/dirname/bar.yaml` option to exclude one or more specific files from multiple file list * Allow negatable for the parse tags option with `--no-parse-tags` 5.3 --- * Added `github` format support & autodetection to render errors as annotations when running the YAML linter command in a Github Action environment. 5.1.0 ----- * Added support for parsing numbers prefixed with `0o` as octal numbers. * Deprecated support for parsing numbers starting with `0` as octal numbers. They will be parsed as strings as of Symfony 6.0. Prefix numbers with `0o` so that they are parsed as octal numbers. Before: ```yaml Yaml::parse('072'); ``` After: ```yaml Yaml::parse('0o72'); ``` * Added `yaml-lint` binary. * Deprecated using the `!php/object` and `!php/const` tags without a value. 5.0.0 ----- * Removed support for mappings inside multi-line strings. * removed support for implicit STDIN usage in the `lint:yaml` command, use `lint:yaml -` (append a dash) instead to make it explicit. 4.4.0 ----- * Added support for parsing the inline notation spanning multiple lines. * Added support to dump `null` as `~` by using the `Yaml::DUMP_NULL_AS_TILDE` flag. * deprecated accepting STDIN implicitly when using the `lint:yaml` command, use `lint:yaml -` (append a dash) instead to make it explicit. 4.3.0 ----- * Using a mapping inside a multi-line string is deprecated and will throw a `ParseException` in 5.0. 4.2.0 ----- * added support for multiple files or directories in `LintCommand` 4.0.0 ----- * The behavior of the non-specific tag `!` is changed and now forces non-evaluating your values. * complex mappings will throw a `ParseException` * support for the comma as a group separator for floats has been dropped, use the underscore instead * support for the `!!php/object` tag has been dropped, use the `!php/object` tag instead * duplicate mapping keys throw a `ParseException` * non-string mapping keys throw a `ParseException`, use the `Yaml::PARSE_KEYS_AS_STRINGS` flag to cast them to strings * `%` at the beginning of an unquoted string throw a `ParseException` * mappings with a colon (`:`) that is not followed by a whitespace throw a `ParseException` * the `Dumper::setIndentation()` method has been removed * being able to pass boolean options to the `Yaml::parse()`, `Yaml::dump()`, `Parser::parse()`, and `Dumper::dump()` methods to configure the behavior of the parser and dumper is no longer supported, pass bitmask flags instead * the constructor arguments of the `Parser` class have been removed * the `Inline` class is internal and no longer part of the BC promise * removed support for the `!str` tag, use the `!!str` tag instead * added support for tagged scalars. ```yml Yaml::parse('!foo bar', Yaml::PARSE_CUSTOM_TAGS); // returns TaggedValue('foo', 'bar'); ``` 3.4.0 ----- * added support for parsing YAML files using the `Yaml::parseFile()` or `Parser::parseFile()` method * the `Dumper`, `Parser`, and `Yaml` classes are marked as final * Deprecated the `!php/object:` tag which will be replaced by the `!php/object` tag (without the colon) in 4.0. * Deprecated the `!php/const:` tag which will be replaced by the `!php/const` tag (without the colon) in 4.0. * Support for the `!str` tag is deprecated, use the `!!str` tag instead. * Deprecated using the non-specific tag `!` as its behavior will change in 4.0. It will force non-evaluating your values in 4.0. Use plain integers or `!!float` instead. 3.3.0 ----- * Starting an unquoted string with a question mark followed by a space is deprecated and will throw a `ParseException` in Symfony 4.0. * Deprecated support for implicitly parsing non-string mapping keys as strings. Mapping keys that are no strings will lead to a `ParseException` in Symfony 4.0. Use quotes to opt-in for keys to be parsed as strings. Before: ```php $yaml = <<<YAML null: null key true: boolean true 2.0: float key YAML; Yaml::parse($yaml); ``` After: ```php $yaml = <<<YAML "null": null key "true": boolean true "2.0": float key YAML; Yaml::parse($yaml); ``` * Omitted mapping values will be parsed as `null`. * Omitting the key of a mapping is deprecated and will throw a `ParseException` in Symfony 4.0. * Added support for dumping empty PHP arrays as YAML sequences: ```php Yaml::dump([], 0, 0, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); ``` 3.2.0 ----- * Mappings with a colon (`:`) that is not followed by a whitespace are deprecated when the mapping key is not quoted and will lead to a `ParseException` in Symfony 4.0 (e.g. `foo:bar` must be `foo: bar`). * Added support for parsing PHP constants: ```php Yaml::parse('!php/const:PHP_INT_MAX', Yaml::PARSE_CONSTANT); ``` * Support for silently ignoring duplicate mapping keys in YAML has been deprecated and will lead to a `ParseException` in Symfony 4.0. 3.1.0 ----- * Added support to dump `stdClass` and `ArrayAccess` objects as YAML mappings through the `Yaml::DUMP_OBJECT_AS_MAP` flag. * Strings that are not UTF-8 encoded will be dumped as base64 encoded binary data. * Added support for dumping multi line strings as literal blocks. * Added support for parsing base64 encoded binary data when they are tagged with the `!!binary` tag. * Added support for parsing timestamps as `\DateTime` objects: ```php Yaml::parse('2001-12-15 21:59:43.10 -5', Yaml::PARSE_DATETIME); ``` * `\DateTime` and `\DateTimeImmutable` objects are dumped as YAML timestamps. * Deprecated usage of `%` at the beginning of an unquoted string. * Added support for customizing the YAML parser behavior through an optional bit field: ```php Yaml::parse('{ "foo": "bar", "fiz": "cat" }', Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE | Yaml::PARSE_OBJECT | Yaml::PARSE_OBJECT_FOR_MAP); ``` * Added support for customizing the dumped YAML string through an optional bit field: ```php Yaml::dump(['foo' => new A(), 'bar' => 1], 0, 0, Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE | Yaml::DUMP_OBJECT); ``` 3.0.0 ----- * Yaml::parse() now throws an exception when a blackslash is not escaped in double-quoted strings 2.8.0 ----- * Deprecated usage of a colon in an unquoted mapping value * Deprecated usage of @, \`, | and > at the beginning of an unquoted string * When surrounding strings with double-quotes, you must now escape `\` characters. Not escaping those characters (when surrounded by double-quotes) is deprecated. Before: ```yml class: "Foo\Var" ``` After: ```yml class: "Foo\\Var" ``` 2.1.0 ----- * Yaml::parse() does not evaluate loaded files as PHP files by default anymore (call Yaml::enablePhpParsing() to get back the old behavior) yaml/Exception/DumpException.php 0000644 00000000707 15025017654 0012752 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml\Exception; /** * Exception class thrown when an error occurs during dumping. * * @author Fabien Potencier <fabien@symfony.com> */ class DumpException extends RuntimeException { } yaml/Exception/ExceptionInterface.php 0000644 00000000716 15025017654 0013745 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml\Exception; /** * Exception interface for all exceptions thrown by the component. * * @author Fabien Potencier <fabien@symfony.com> */ interface ExceptionInterface extends \Throwable { } yaml/Exception/ParseException.php 0000644 00000006106 15025017654 0013116 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml\Exception; /** * Exception class thrown when an error occurs during parsing. * * @author Fabien Potencier <fabien@symfony.com> */ class ParseException extends RuntimeException { private ?string $parsedFile; private int $parsedLine; private ?string $snippet; private string $rawMessage; /** * @param string $message The error message * @param int $parsedLine The line where the error occurred * @param string|null $snippet The snippet of code near the problem * @param string|null $parsedFile The file name where the error occurred */ public function __construct(string $message, int $parsedLine = -1, string $snippet = null, string $parsedFile = null, \Throwable $previous = null) { $this->parsedFile = $parsedFile; $this->parsedLine = $parsedLine; $this->snippet = $snippet; $this->rawMessage = $message; $this->updateRepr(); parent::__construct($this->message, 0, $previous); } /** * Gets the snippet of code near the error. */ public function getSnippet(): string { return $this->snippet; } /** * Sets the snippet of code near the error. */ public function setSnippet(string $snippet) { $this->snippet = $snippet; $this->updateRepr(); } /** * Gets the filename where the error occurred. * * This method returns null if a string is parsed. */ public function getParsedFile(): string { return $this->parsedFile; } /** * Sets the filename where the error occurred. */ public function setParsedFile(string $parsedFile) { $this->parsedFile = $parsedFile; $this->updateRepr(); } /** * Gets the line where the error occurred. */ public function getParsedLine(): int { return $this->parsedLine; } /** * Sets the line where the error occurred. */ public function setParsedLine(int $parsedLine) { $this->parsedLine = $parsedLine; $this->updateRepr(); } private function updateRepr() { $this->message = $this->rawMessage; $dot = false; if ('.' === substr($this->message, -1)) { $this->message = substr($this->message, 0, -1); $dot = true; } if (null !== $this->parsedFile) { $this->message .= sprintf(' in %s', json_encode($this->parsedFile, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE)); } if ($this->parsedLine >= 0) { $this->message .= sprintf(' at line %d', $this->parsedLine); } if ($this->snippet) { $this->message .= sprintf(' (near "%s")', $this->snippet); } if ($dot) { $this->message .= '.'; } } } yaml/Exception/RuntimeException.php 0000644 00000000745 15025017654 0013472 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml\Exception; /** * Exception class thrown when an error occurs during parsing. * * @author Romain Neutron <imprec@gmail.com> */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } yaml/Unescaper.php 0000644 00000007220 15025017654 0010152 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; use Symfony\Component\Yaml\Exception\ParseException; /** * Unescaper encapsulates unescaping rules for single and double-quoted * YAML strings. * * @author Matthew Lewinski <matthew@lewinski.org> * * @internal */ class Unescaper { /** * Regex fragment that matches an escaped character in a double quoted string. */ public const REGEX_ESCAPED_CHARACTER = '\\\\(x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|.)'; /** * Unescapes a single quoted string. * * @param string $value A single quoted string */ public function unescapeSingleQuotedString(string $value): string { return str_replace('\'\'', '\'', $value); } /** * Unescapes a double quoted string. * * @param string $value A double quoted string */ public function unescapeDoubleQuotedString(string $value): string { $callback = function ($match) { return $this->unescapeCharacter($match[0]); }; // evaluate the string return preg_replace_callback('/'.self::REGEX_ESCAPED_CHARACTER.'/u', $callback, $value); } /** * Unescapes a character that was found in a double-quoted string. * * @param string $value An escaped character */ private function unescapeCharacter(string $value): string { switch ($value[1]) { case '0': return "\x0"; case 'a': return "\x7"; case 'b': return "\x8"; case 't': return "\t"; case "\t": return "\t"; case 'n': return "\n"; case 'v': return "\xB"; case 'f': return "\xC"; case 'r': return "\r"; case 'e': return "\x1B"; case ' ': return ' '; case '"': return '"'; case '/': return '/'; case '\\': return '\\'; case 'N': // U+0085 NEXT LINE return "\xC2\x85"; case '_': // U+00A0 NO-BREAK SPACE return "\xC2\xA0"; case 'L': // U+2028 LINE SEPARATOR return "\xE2\x80\xA8"; case 'P': // U+2029 PARAGRAPH SEPARATOR return "\xE2\x80\xA9"; case 'x': return self::utf8chr(hexdec(substr($value, 2, 2))); case 'u': return self::utf8chr(hexdec(substr($value, 2, 4))); case 'U': return self::utf8chr(hexdec(substr($value, 2, 8))); default: throw new ParseException(sprintf('Found unknown escape character "%s".', $value)); } } /** * Get the UTF-8 character for the given code point. */ private static function utf8chr(int $c): string { if (0x80 > $c %= 0x200000) { return \chr($c); } if (0x800 > $c) { return \chr(0xC0 | $c >> 6).\chr(0x80 | $c & 0x3F); } if (0x10000 > $c) { return \chr(0xE0 | $c >> 12).\chr(0x80 | $c >> 6 & 0x3F).\chr(0x80 | $c & 0x3F); } return \chr(0xF0 | $c >> 18).\chr(0x80 | $c >> 12 & 0x3F).\chr(0x80 | $c >> 6 & 0x3F).\chr(0x80 | $c & 0x3F); } } yaml/Command/LintCommand.php 0000644 00000024245 15025017654 0012016 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\CI\GithubActionReporter; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser; use Symfony\Component\Yaml\Yaml; /** * Validates YAML files syntax and outputs encountered errors. * * @author Grégoire Pineau <lyrixx@lyrixx.info> * @author Robin Chalas <robin.chalas@gmail.com> */ #[AsCommand(name: 'lint:yaml', description: 'Lint a YAML file and outputs encountered errors')] class LintCommand extends Command { private $parser; private ?string $format = null; private bool $displayCorrectFiles; private ?\Closure $directoryIteratorProvider; private ?\Closure $isReadableProvider; public function __construct(string $name = null, callable $directoryIteratorProvider = null, callable $isReadableProvider = null) { parent::__construct($name); $this->directoryIteratorProvider = null === $directoryIteratorProvider || $directoryIteratorProvider instanceof \Closure ? $directoryIteratorProvider : \Closure::fromCallable($directoryIteratorProvider); $this->isReadableProvider = null === $isReadableProvider || $isReadableProvider instanceof \Closure ? $isReadableProvider : \Closure::fromCallable($isReadableProvider); } /** * {@inheritdoc} */ protected function configure() { $this ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format') ->addOption('exclude', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to exclude') ->addOption('parse-tags', null, InputOption::VALUE_NEGATABLE, 'Parse custom tags', null) ->setHelp(<<<EOF The <info>%command.name%</info> command lints a YAML file and outputs to STDOUT the first encountered syntax error. You can validates YAML contents passed from STDIN: <info>cat filename | php %command.full_name% -</info> You can also validate the syntax of a file: <info>php %command.full_name% filename</info> Or of a whole directory: <info>php %command.full_name% dirname</info> <info>php %command.full_name% dirname --format=json</info> You can also exclude one or more specific files: <info>php %command.full_name% dirname --exclude="dirname/foo.yaml" --exclude="dirname/bar.yaml"</info> EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $filenames = (array) $input->getArgument('filename'); $excludes = $input->getOption('exclude'); $this->format = $input->getOption('format'); $flags = $input->getOption('parse-tags'); if ('github' === $this->format && !class_exists(GithubActionReporter::class)) { throw new \InvalidArgumentException('The "github" format is only available since "symfony/console" >= 5.3.'); } if (null === $this->format) { // Autodetect format according to CI environment $this->format = class_exists(GithubActionReporter::class) && GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt'; } $flags = $flags ? Yaml::PARSE_CUSTOM_TAGS : 0; $this->displayCorrectFiles = $output->isVerbose(); if (['-'] === $filenames) { return $this->display($io, [$this->validate(file_get_contents('php://stdin'), $flags)]); } if (!$filenames) { throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); } $filesInfo = []; foreach ($filenames as $filename) { if (!$this->isReadable($filename)) { throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); } foreach ($this->getFiles($filename) as $file) { if (!\in_array($file->getPathname(), $excludes, true)) { $filesInfo[] = $this->validate(file_get_contents($file), $flags, $file); } } } return $this->display($io, $filesInfo); } private function validate(string $content, int $flags, string $file = null) { $prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler) { if (\E_USER_DEPRECATED === $level) { throw new ParseException($message, $this->getParser()->getRealCurrentLineNb() + 1); } return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false; }); try { $this->getParser()->parse($content, Yaml::PARSE_CONSTANT | $flags); } catch (ParseException $e) { return ['file' => $file, 'line' => $e->getParsedLine(), 'valid' => false, 'message' => $e->getMessage()]; } finally { restore_error_handler(); } return ['file' => $file, 'valid' => true]; } private function display(SymfonyStyle $io, array $files): int { switch ($this->format) { case 'txt': return $this->displayTxt($io, $files); case 'json': return $this->displayJson($io, $files); case 'github': return $this->displayTxt($io, $files, true); default: throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format)); } } private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false): int { $countFiles = \count($filesInfo); $erroredFiles = 0; $suggestTagOption = false; if ($errorAsGithubAnnotations) { $githubReporter = new GithubActionReporter($io); } foreach ($filesInfo as $info) { if ($info['valid'] && $this->displayCorrectFiles) { $io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); } elseif (!$info['valid']) { ++$erroredFiles; $io->text('<error> ERROR </error>'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); $io->text(sprintf('<error> >> %s</error>', $info['message'])); if (false !== strpos($info['message'], 'PARSE_CUSTOM_TAGS')) { $suggestTagOption = true; } if ($errorAsGithubAnnotations) { $githubReporter->error($info['message'], $info['file'] ?? 'php://stdin', $info['line']); } } } if (0 === $erroredFiles) { $io->success(sprintf('All %d YAML files contain valid syntax.', $countFiles)); } else { $io->warning(sprintf('%d YAML files have valid syntax and %d contain errors.%s', $countFiles - $erroredFiles, $erroredFiles, $suggestTagOption ? ' Use the --parse-tags option if you want parse custom tags.' : '')); } return min($erroredFiles, 1); } private function displayJson(SymfonyStyle $io, array $filesInfo): int { $errors = 0; array_walk($filesInfo, function (&$v) use (&$errors) { $v['file'] = (string) $v['file']; if (!$v['valid']) { ++$errors; } if (isset($v['message']) && false !== strpos($v['message'], 'PARSE_CUSTOM_TAGS')) { $v['message'] .= ' Use the --parse-tags option if you want parse custom tags.'; } }); $io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); return min($errors, 1); } private function getFiles(string $fileOrDirectory): iterable { if (is_file($fileOrDirectory)) { yield new \SplFileInfo($fileOrDirectory); return; } foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) { if (!\in_array($file->getExtension(), ['yml', 'yaml'])) { continue; } yield $file; } } private function getParser(): Parser { return $this->parser ??= new Parser(); } private function getDirectoryIterator(string $directory): iterable { $default = function ($directory) { return new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), \RecursiveIteratorIterator::LEAVES_ONLY ); }; if (null !== $this->directoryIteratorProvider) { return ($this->directoryIteratorProvider)($directory, $default); } return $default($directory); } private function isReadable(string $fileOrDirectory): bool { $default = function ($fileOrDirectory) { return is_readable($fileOrDirectory); }; if (null !== $this->isReadableProvider) { return ($this->isReadableProvider)($fileOrDirectory, $default); } return $default($fileOrDirectory); } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues(['txt', 'json', 'github']); } } } yaml/README.md 0000644 00000000704 15025017654 0006773 0 ustar 00 Yaml Component ============== The Yaml component loads and dumps YAML files. Resources --------- * [Documentation](https://symfony.com/doc/current/components/yaml.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) yaml/LICENSE 0000644 00000002051 15025017654 0006516 0 ustar 00 Copyright (c) 2004-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. yaml/Inline.php 0000644 00000076040 15025017654 0007451 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; use Symfony\Component\Yaml\Exception\DumpException; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Tag\TaggedValue; /** * Inline implements a YAML parser/dumper for the YAML inline syntax. * * @author Fabien Potencier <fabien@symfony.com> * * @internal */ class Inline { public const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+)"|\'([^\']*+(?:\'\'[^\']*+)*+)\')'; public static int $parsedLineNumber = -1; public static ?string $parsedFilename = null; private static bool $exceptionOnInvalidType = false; private static bool $objectSupport = false; private static bool $objectForMap = false; private static bool $constantSupport = false; public static function initialize(int $flags, int $parsedLineNumber = null, string $parsedFilename = null) { self::$exceptionOnInvalidType = (bool) (Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE & $flags); self::$objectSupport = (bool) (Yaml::PARSE_OBJECT & $flags); self::$objectForMap = (bool) (Yaml::PARSE_OBJECT_FOR_MAP & $flags); self::$constantSupport = (bool) (Yaml::PARSE_CONSTANT & $flags); self::$parsedFilename = $parsedFilename; if (null !== $parsedLineNumber) { self::$parsedLineNumber = $parsedLineNumber; } } /** * Converts a YAML string to a PHP value. * * @param int $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior * @param array $references Mapping of variable names to values * * @throws ParseException */ public static function parse(string $value = null, int $flags = 0, array &$references = []): mixed { self::initialize($flags); $value = trim($value); if ('' === $value) { return ''; } $i = 0; $tag = self::parseTag($value, $i, $flags); switch ($value[$i]) { case '[': $result = self::parseSequence($value, $flags, $i, $references); ++$i; break; case '{': $result = self::parseMapping($value, $flags, $i, $references); ++$i; break; default: $result = self::parseScalar($value, $flags, null, $i, true, $references); } // some comments are allowed at the end if (preg_replace('/\s*#.*$/A', '', substr($value, $i))) { throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)), self::$parsedLineNumber + 1, $value, self::$parsedFilename); } if (null !== $tag && '' !== $tag) { return new TaggedValue($tag, $result); } return $result; } /** * Dumps a given PHP variable to a YAML string. * * @param mixed $value The PHP variable to convert * @param int $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string * * @throws DumpException When trying to dump PHP resource */ public static function dump(mixed $value, int $flags = 0): string { switch (true) { case \is_resource($value): if (Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE & $flags) { throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value))); } return self::dumpNull($flags); case $value instanceof \DateTimeInterface: return $value->format('c'); case $value instanceof \UnitEnum: return sprintf('!php/const %s::%s', \get_class($value), $value->name); case \is_object($value): if ($value instanceof TaggedValue) { return '!'.$value->getTag().' '.self::dump($value->getValue(), $flags); } if (Yaml::DUMP_OBJECT & $flags) { return '!php/object '.self::dump(serialize($value)); } if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($value instanceof \stdClass || $value instanceof \ArrayObject)) { $output = []; foreach ($value as $key => $val) { $output[] = sprintf('%s: %s', self::dump($key, $flags), self::dump($val, $flags)); } return sprintf('{ %s }', implode(', ', $output)); } if (Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE & $flags) { throw new DumpException('Object support when dumping a YAML file has been disabled.'); } return self::dumpNull($flags); case \is_array($value): return self::dumpArray($value, $flags); case null === $value: return self::dumpNull($flags); case true === $value: return 'true'; case false === $value: return 'false'; case \is_int($value): return $value; case is_numeric($value) && false === strpbrk($value, "\f\n\r\t\v"): $locale = setlocale(\LC_NUMERIC, 0); if (false !== $locale) { setlocale(\LC_NUMERIC, 'C'); } if (\is_float($value)) { $repr = (string) $value; if (is_infinite($value)) { $repr = str_ireplace('INF', '.Inf', $repr); } elseif (floor($value) == $value && $repr == $value) { // Preserve float data type since storing a whole number will result in integer value. if (false === strpos($repr, 'E')) { $repr = $repr.'.0'; } } } else { $repr = \is_string($value) ? "'$value'" : (string) $value; } if (false !== $locale) { setlocale(\LC_NUMERIC, $locale); } return $repr; case '' == $value: return "''"; case self::isBinaryString($value): return '!!binary '.base64_encode($value); case Escaper::requiresDoubleQuoting($value): return Escaper::escapeWithDoubleQuotes($value); case Escaper::requiresSingleQuoting($value): case Parser::preg_match('{^[0-9]+[_0-9]*$}', $value): case Parser::preg_match(self::getHexRegex(), $value): case Parser::preg_match(self::getTimestampRegex(), $value): return Escaper::escapeWithSingleQuotes($value); default: return $value; } } /** * Check if given array is hash or just normal indexed array. */ public static function isHash(array|\ArrayObject|\stdClass $value): bool { if ($value instanceof \stdClass || $value instanceof \ArrayObject) { return true; } $expectedKey = 0; foreach ($value as $key => $val) { if ($key !== $expectedKey++) { return true; } } return false; } /** * Dumps a PHP array to a YAML string. * * @param array $value The PHP array to dump * @param int $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string */ private static function dumpArray(array $value, int $flags): string { // array if (($value || Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE & $flags) && !self::isHash($value)) { $output = []; foreach ($value as $val) { $output[] = self::dump($val, $flags); } return sprintf('[%s]', implode(', ', $output)); } // hash $output = []; foreach ($value as $key => $val) { $output[] = sprintf('%s: %s', self::dump($key, $flags), self::dump($val, $flags)); } return sprintf('{ %s }', implode(', ', $output)); } private static function dumpNull(int $flags): string { if (Yaml::DUMP_NULL_AS_TILDE & $flags) { return '~'; } return 'null'; } /** * Parses a YAML scalar. * * @throws ParseException When malformed inline YAML string is parsed */ public static function parseScalar(string $scalar, int $flags = 0, array $delimiters = null, int &$i = 0, bool $evaluate = true, array &$references = [], bool &$isQuoted = null): mixed { if (\in_array($scalar[$i], ['"', "'"], true)) { // quoted scalar $isQuoted = true; $output = self::parseQuotedScalar($scalar, $i); if (null !== $delimiters) { $tmp = ltrim(substr($scalar, $i), " \n"); if ('' === $tmp) { throw new ParseException(sprintf('Unexpected end of line, expected one of "%s".', implode('', $delimiters)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } if (!\in_array($tmp[0], $delimiters)) { throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } } } else { // "normal" string $isQuoted = false; if (!$delimiters) { $output = substr($scalar, $i); $i += \strlen($output); // remove comments if (Parser::preg_match('/[ \t]+#/', $output, $match, \PREG_OFFSET_CAPTURE)) { $output = substr($output, 0, $match[0][1]); } } elseif (Parser::preg_match('/^(.*?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) { $output = $match[1]; $i += \strlen($output); $output = trim($output); } else { throw new ParseException(sprintf('Malformed inline YAML string: "%s".', $scalar), self::$parsedLineNumber + 1, null, self::$parsedFilename); } // a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >) if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0] || '%' === $output[0])) { throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0]), self::$parsedLineNumber + 1, $output, self::$parsedFilename); } if ($evaluate) { $output = self::evaluateScalar($output, $flags, $references, $isQuoted); } } return $output; } /** * Parses a YAML quoted scalar. * * @throws ParseException When malformed inline YAML string is parsed */ private static function parseQuotedScalar(string $scalar, int &$i = 0): string { if (!Parser::preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) { throw new ParseException(sprintf('Malformed inline YAML string: "%s".', substr($scalar, $i)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } $output = substr($match[0], 1, -1); $unescaper = new Unescaper(); if ('"' == $scalar[$i]) { $output = $unescaper->unescapeDoubleQuotedString($output); } else { $output = $unescaper->unescapeSingleQuotedString($output); } $i += \strlen($match[0]); return $output; } /** * Parses a YAML sequence. * * @throws ParseException When malformed inline YAML string is parsed */ private static function parseSequence(string $sequence, int $flags, int &$i = 0, array &$references = []): array { $output = []; $len = \strlen($sequence); ++$i; // [foo, bar, ...] while ($i < $len) { if (']' === $sequence[$i]) { return $output; } if (',' === $sequence[$i] || ' ' === $sequence[$i]) { ++$i; continue; } $tag = self::parseTag($sequence, $i, $flags); switch ($sequence[$i]) { case '[': // nested sequence $value = self::parseSequence($sequence, $flags, $i, $references); break; case '{': // nested mapping $value = self::parseMapping($sequence, $flags, $i, $references); break; default: $value = self::parseScalar($sequence, $flags, [',', ']'], $i, null === $tag, $references, $isQuoted); // the value can be an array if a reference has been resolved to an array var if (\is_string($value) && !$isQuoted && false !== strpos($value, ': ')) { // embedded mapping? try { $pos = 0; $value = self::parseMapping('{'.$value.'}', $flags, $pos, $references); } catch (\InvalidArgumentException $e) { // no, it's not } } if (!$isQuoted && \is_string($value) && '' !== $value && '&' === $value[0] && Parser::preg_match(Parser::REFERENCE_PATTERN, $value, $matches)) { $references[$matches['ref']] = $matches['value']; $value = $matches['value']; } --$i; } if (null !== $tag && '' !== $tag) { $value = new TaggedValue($tag, $value); } $output[] = $value; ++$i; } throw new ParseException(sprintf('Malformed inline YAML string: "%s".', $sequence), self::$parsedLineNumber + 1, null, self::$parsedFilename); } /** * Parses a YAML mapping. * * @throws ParseException When malformed inline YAML string is parsed */ private static function parseMapping(string $mapping, int $flags, int &$i = 0, array &$references = []): array|\stdClass { $output = []; $len = \strlen($mapping); ++$i; $allowOverwrite = false; // {foo: bar, bar:foo, ...} while ($i < $len) { switch ($mapping[$i]) { case ' ': case ',': case "\n": ++$i; continue 2; case '}': if (self::$objectForMap) { return (object) $output; } return $output; } // key $offsetBeforeKeyParsing = $i; $isKeyQuoted = \in_array($mapping[$i], ['"', "'"], true); $key = self::parseScalar($mapping, $flags, [':', ' '], $i, false); if ($offsetBeforeKeyParsing === $i) { throw new ParseException('Missing mapping key.', self::$parsedLineNumber + 1, $mapping); } if ('!php/const' === $key) { $key .= ' '.self::parseScalar($mapping, $flags, [':'], $i, false); $key = self::evaluateScalar($key, $flags); } if (false === $i = strpos($mapping, ':', $i)) { break; } if (!$isKeyQuoted) { $evaluatedKey = self::evaluateScalar($key, $flags, $references); if ('' !== $key && $evaluatedKey !== $key && !\is_string($evaluatedKey) && !\is_int($evaluatedKey)) { throw new ParseException('Implicit casting of incompatible mapping keys to strings is not supported. Quote your evaluable mapping keys instead.', self::$parsedLineNumber + 1, $mapping); } } if (!$isKeyQuoted && (!isset($mapping[$i + 1]) || !\in_array($mapping[$i + 1], [' ', ',', '[', ']', '{', '}', "\n"], true))) { throw new ParseException('Colons must be followed by a space or an indication character (i.e. " ", ",", "[", "]", "{", "}").', self::$parsedLineNumber + 1, $mapping); } if ('<<' === $key) { $allowOverwrite = true; } while ($i < $len) { if (':' === $mapping[$i] || ' ' === $mapping[$i] || "\n" === $mapping[$i]) { ++$i; continue; } $tag = self::parseTag($mapping, $i, $flags); switch ($mapping[$i]) { case '[': // nested sequence $value = self::parseSequence($mapping, $flags, $i, $references); // Spec: Keys MUST be unique; first one wins. // Parser cannot abort this mapping earlier, since lines // are processed sequentially. // But overwriting is allowed when a merge node is used in current block. if ('<<' === $key) { foreach ($value as $parsedValue) { $output += $parsedValue; } } elseif ($allowOverwrite || !isset($output[$key])) { if (null !== $tag) { $output[$key] = new TaggedValue($tag, $value); } else { $output[$key] = $value; } } elseif (isset($output[$key])) { throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping); } break; case '{': // nested mapping $value = self::parseMapping($mapping, $flags, $i, $references); // Spec: Keys MUST be unique; first one wins. // Parser cannot abort this mapping earlier, since lines // are processed sequentially. // But overwriting is allowed when a merge node is used in current block. if ('<<' === $key) { $output += $value; } elseif ($allowOverwrite || !isset($output[$key])) { if (null !== $tag) { $output[$key] = new TaggedValue($tag, $value); } else { $output[$key] = $value; } } elseif (isset($output[$key])) { throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping); } break; default: $value = self::parseScalar($mapping, $flags, [',', '}', "\n"], $i, null === $tag, $references, $isValueQuoted); // Spec: Keys MUST be unique; first one wins. // Parser cannot abort this mapping earlier, since lines // are processed sequentially. // But overwriting is allowed when a merge node is used in current block. if ('<<' === $key) { $output += $value; } elseif ($allowOverwrite || !isset($output[$key])) { if (!$isValueQuoted && \is_string($value) && '' !== $value && '&' === $value[0] && Parser::preg_match(Parser::REFERENCE_PATTERN, $value, $matches)) { $references[$matches['ref']] = $matches['value']; $value = $matches['value']; } if (null !== $tag) { $output[$key] = new TaggedValue($tag, $value); } else { $output[$key] = $value; } } elseif (isset($output[$key])) { throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping); } --$i; } ++$i; continue 2; } } throw new ParseException(sprintf('Malformed inline YAML string: "%s".', $mapping), self::$parsedLineNumber + 1, null, self::$parsedFilename); } /** * Evaluates scalars and replaces magic values. * * @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved */ private static function evaluateScalar(string $scalar, int $flags, array &$references = [], bool &$isQuotedString = null): mixed { $isQuotedString = false; $scalar = trim($scalar); if (0 === strpos($scalar, '*')) { if (false !== $pos = strpos($scalar, '#')) { $value = substr($scalar, 1, $pos - 2); } else { $value = substr($scalar, 1); } // an unquoted * if (false === $value || '' === $value) { throw new ParseException('A reference must contain at least one character.', self::$parsedLineNumber + 1, $value, self::$parsedFilename); } if (!\array_key_exists($value, $references)) { throw new ParseException(sprintf('Reference "%s" does not exist.', $value), self::$parsedLineNumber + 1, $value, self::$parsedFilename); } return $references[$value]; } $scalarLower = strtolower($scalar); switch (true) { case 'null' === $scalarLower: case '' === $scalar: case '~' === $scalar: return null; case 'true' === $scalarLower: return true; case 'false' === $scalarLower: return false; case '!' === $scalar[0]: switch (true) { case 0 === strpos($scalar, '!!str '): $s = (string) substr($scalar, 6); if (\in_array($s[0] ?? '', ['"', "'"], true)) { $isQuotedString = true; $s = self::parseQuotedScalar($s); } return $s; case 0 === strpos($scalar, '! '): return substr($scalar, 2); case 0 === strpos($scalar, '!php/object'): if (self::$objectSupport) { if (!isset($scalar[12])) { throw new ParseException('Missing value for tag "!php/object".', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } return unserialize(self::parseScalar(substr($scalar, 12))); } if (self::$exceptionOnInvalidType) { throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } return null; case 0 === strpos($scalar, '!php/const'): if (self::$constantSupport) { if (!isset($scalar[11])) { throw new ParseException('Missing value for tag "!php/const".', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } $i = 0; if (\defined($const = self::parseScalar(substr($scalar, 11), 0, null, $i, false))) { return \constant($const); } throw new ParseException(sprintf('The constant "%s" is not defined.', $const), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } if (self::$exceptionOnInvalidType) { throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Did you forget to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } return null; case 0 === strpos($scalar, '!!float '): return (float) substr($scalar, 8); case 0 === strpos($scalar, '!!binary '): return self::evaluateBinaryScalar(substr($scalar, 9)); } throw new ParseException(sprintf('The string "%s" could not be parsed as it uses an unsupported built-in tag.', $scalar), self::$parsedLineNumber, $scalar, self::$parsedFilename); case preg_match('/^(?:\+|-)?0o(?P<value>[0-7_]++)$/', $scalar, $matches): $value = str_replace('_', '', $matches['value']); if ('-' === $scalar[0]) { return -octdec($value); } return octdec($value); case \in_array($scalar[0], ['+', '-', '.'], true) || is_numeric($scalar[0]): if (Parser::preg_match('{^[+-]?[0-9][0-9_]*$}', $scalar)) { $scalar = str_replace('_', '', $scalar); } switch (true) { case ctype_digit($scalar): case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)): $cast = (int) $scalar; return ($scalar === (string) $cast) ? $cast : $scalar; case is_numeric($scalar): case Parser::preg_match(self::getHexRegex(), $scalar): $scalar = str_replace('_', '', $scalar); return '0x' === $scalar[0].$scalar[1] ? hexdec($scalar) : (float) $scalar; case '.inf' === $scalarLower: case '.nan' === $scalarLower: return -log(0); case '-.inf' === $scalarLower: return log(0); case Parser::preg_match('/^(-|\+)?[0-9][0-9_]*(\.[0-9_]+)?$/', $scalar): return (float) str_replace('_', '', $scalar); case Parser::preg_match(self::getTimestampRegex(), $scalar): // When no timezone is provided in the parsed date, YAML spec says we must assume UTC. $time = new \DateTime($scalar, new \DateTimeZone('UTC')); if (Yaml::PARSE_DATETIME & $flags) { return $time; } try { if (false !== $scalar = $time->getTimestamp()) { return $scalar; } } catch (\ValueError $e) { // no-op } return $time->format('U'); } } return (string) $scalar; } private static function parseTag(string $value, int &$i, int $flags): ?string { if ('!' !== $value[$i]) { return null; } $tagLength = strcspn($value, " \t\n[]{},", $i + 1); $tag = substr($value, $i + 1, $tagLength); $nextOffset = $i + $tagLength + 1; $nextOffset += strspn($value, ' ', $nextOffset); if ('' === $tag && (!isset($value[$nextOffset]) || \in_array($value[$nextOffset], [']', '}', ','], true))) { throw new ParseException('Using the unquoted scalar value "!" is not supported. You must quote it.', self::$parsedLineNumber + 1, $value, self::$parsedFilename); } // Is followed by a scalar and is a built-in tag if ('' !== $tag && (!isset($value[$nextOffset]) || !\in_array($value[$nextOffset], ['[', '{'], true)) && ('!' === $tag[0] || 'str' === $tag || 'php/const' === $tag || 'php/object' === $tag)) { // Manage in {@link self::evaluateScalar()} return null; } $i = $nextOffset; // Built-in tags if ('' !== $tag && '!' === $tag[0]) { throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename); } if ('' !== $tag && !isset($value[$i])) { throw new ParseException(sprintf('Missing value for tag "%s".', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename); } if ('' === $tag || Yaml::PARSE_CUSTOM_TAGS & $flags) { return $tag; } throw new ParseException(sprintf('Tags support is not enabled. Enable the "Yaml::PARSE_CUSTOM_TAGS" flag to use "!%s".', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename); } public static function evaluateBinaryScalar(string $scalar): string { $parsedBinaryData = self::parseScalar(preg_replace('/\s/', '', $scalar)); if (0 !== (\strlen($parsedBinaryData) % 4)) { throw new ParseException(sprintf('The normalized base64 encoded data (data without whitespace characters) length must be a multiple of four (%d bytes given).', \strlen($parsedBinaryData)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } if (!Parser::preg_match('#^[A-Z0-9+/]+={0,2}$#i', $parsedBinaryData)) { throw new ParseException(sprintf('The base64 encoded data (%s) contains invalid characters.', $parsedBinaryData), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } return base64_decode($parsedBinaryData, true); } private static function isBinaryString(string $value): bool { return !preg_match('//u', $value) || preg_match('/[^\x00\x07-\x0d\x1B\x20-\xff]/', $value); } /** * Gets a regex that matches a YAML date. * * @see http://www.yaml.org/spec/1.2/spec.html#id2761573 */ private static function getTimestampRegex(): string { return <<<EOF ~^ (?P<year>[0-9][0-9][0-9][0-9]) -(?P<month>[0-9][0-9]?) -(?P<day>[0-9][0-9]?) (?:(?:[Tt]|[ \t]+) (?P<hour>[0-9][0-9]?) :(?P<minute>[0-9][0-9]) :(?P<second>[0-9][0-9]) (?:\.(?P<fraction>[0-9]*))? (?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?) (?::(?P<tz_minute>[0-9][0-9]))?))?)? $~x EOF; } /** * Gets a regex that matches a YAML number in hexadecimal notation. */ private static function getHexRegex(): string { return '~^0x[0-9a-f_]++$~i'; } } yaml/Tag/TaggedValue.php 0000644 00000001330 15025017654 0011124 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml\Tag; /** * @author Nicolas Grekas <p@tchwork.com> * @author Guilhem N. <egetick@gmail.com> */ final class TaggedValue { private string $tag; private mixed $value; public function __construct(string $tag, mixed $value) { $this->tag = $tag; $this->value = $value; } public function getTag(): string { return $this->tag; } public function getValue() { return $this->value; } } yaml/Parser.php 0000644 00000142750 15025017654 0007471 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Tag\TaggedValue; /** * Parser parses YAML strings to convert them to PHP arrays. * * @author Fabien Potencier <fabien@symfony.com> * * @final */ class Parser { public const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)'; public const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?'; public const REFERENCE_PATTERN = '#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u'; private ?string $filename = null; private int $offset = 0; private int $numberOfParsedLines = 0; private ?int $totalNumberOfLines = null; private array $lines = []; private int $currentLineNb = -1; private string $currentLine = ''; private array $refs = []; private array $skippedLineNumbers = []; private array $locallySkippedLineNumbers = []; private array $refsBeingParsed = []; /** * Parses a YAML file into a PHP value. * * @param string $filename The path to the YAML file to be parsed * @param int $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior * * @throws ParseException If the file could not be read or the YAML is not valid */ public function parseFile(string $filename, int $flags = 0): mixed { if (!is_file($filename)) { throw new ParseException(sprintf('File "%s" does not exist.', $filename)); } if (!is_readable($filename)) { throw new ParseException(sprintf('File "%s" cannot be read.', $filename)); } $this->filename = $filename; try { return $this->parse(file_get_contents($filename), $flags); } finally { $this->filename = null; } } /** * Parses a YAML string to a PHP value. * * @param string $value A YAML string * @param int $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior * * @throws ParseException If the YAML is not valid */ public function parse(string $value, int $flags = 0): mixed { if (false === preg_match('//u', $value)) { throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename); } $this->refs = []; try { $data = $this->doParse($value, $flags); } finally { $this->refsBeingParsed = []; $this->offset = 0; $this->lines = []; $this->currentLine = ''; $this->numberOfParsedLines = 0; $this->refs = []; $this->skippedLineNumbers = []; $this->locallySkippedLineNumbers = []; $this->totalNumberOfLines = null; } return $data; } private function doParse(string $value, int $flags) { $this->currentLineNb = -1; $this->currentLine = ''; $value = $this->cleanup($value); $this->lines = explode("\n", $value); $this->numberOfParsedLines = \count($this->lines); $this->locallySkippedLineNumbers = []; if (null === $this->totalNumberOfLines) { $this->totalNumberOfLines = $this->numberOfParsedLines; } if (!$this->moveToNextLine()) { return null; } $data = []; $context = null; $allowOverwrite = false; while ($this->isCurrentLineEmpty()) { if (!$this->moveToNextLine()) { return null; } } // Resolves the tag and returns if end of the document if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, false)) && !$this->moveToNextLine()) { return new TaggedValue($tag, ''); } do { if ($this->isCurrentLineEmpty()) { continue; } // tab? if ("\t" === $this->currentLine[0]) { throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } Inline::initialize($flags, $this->getRealCurrentLineNb(), $this->filename); $isRef = $mergeNode = false; if ('-' === $this->currentLine[0] && self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) { if ($context && 'mapping' == $context) { throw new ParseException('You cannot define a sequence item when in a mapping.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } $context = 'sequence'; if (isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) { $isRef = $matches['ref']; $this->refsBeingParsed[] = $isRef; $values['value'] = $matches['value']; } if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) { throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } // array if (isset($values['value']) && 0 === strpos(ltrim($values['value'], ' '), '-')) { // Inline first child $currentLineNumber = $this->getRealCurrentLineNb(); $sequenceIndentation = \strlen($values['leadspaces']) + 1; $sequenceYaml = substr($this->currentLine, $sequenceIndentation); $sequenceYaml .= "\n".$this->getNextEmbedBlock($sequenceIndentation, true); $data[] = $this->parseBlock($currentLineNumber, rtrim($sequenceYaml), $flags); } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true) ?? '', $flags); } elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) { $data[] = new TaggedValue( $subTag, $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags) ); } else { if ( isset($values['leadspaces']) && ( '!' === $values['value'][0] || self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches) ) ) { // this is a compact notation element, add to next block and parse $block = $values['value']; if ($this->isNextLineIndented()) { $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + \strlen($values['leadspaces']) + 1); } $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags); } else { $data[] = $this->parseValue($values['value'], $flags, $context); } } if ($isRef) { $this->refs[$isRef] = end($data); array_pop($this->refsBeingParsed); } } elseif ( self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?[^ \'"\[\{!].*?)) *\:(( |\t)++(?P<value>.+))?$#u', rtrim($this->currentLine), $values) && (false === strpos($values['key'], ' #') || \in_array($values['key'][0], ['"', "'"])) ) { if ($context && 'sequence' == $context) { throw new ParseException('You cannot define a mapping item when in a sequence.', $this->currentLineNb + 1, $this->currentLine, $this->filename); } $context = 'mapping'; try { $key = Inline::parseScalar($values['key']); } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); throw $e; } if (!\is_string($key) && !\is_int($key)) { throw new ParseException((is_numeric($key) ? 'Numeric' : 'Non-string').' keys are not supported. Quote your evaluable mapping keys instead.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } // Convert float keys to strings, to avoid being converted to integers by PHP if (\is_float($key)) { $key = (string) $key; } if ('<<' === $key && (!isset($values['value']) || '&' !== $values['value'][0] || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) { $mergeNode = true; $allowOverwrite = true; if (isset($values['value'][0]) && '*' === $values['value'][0]) { $refName = substr(rtrim($values['value']), 1); if (!\array_key_exists($refName, $this->refs)) { if (false !== $pos = array_search($refName, $this->refsBeingParsed, true)) { throw new ParseException(sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, $pos), [$refName])), $refName), $this->currentLineNb + 1, $this->currentLine, $this->filename); } throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } $refValue = $this->refs[$refName]; if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) { $refValue = (array) $refValue; } if (!\is_array($refValue)) { throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } $data += $refValue; // array union } else { if (isset($values['value']) && '' !== $values['value']) { $value = $values['value']; } else { $value = $this->getNextEmbedBlock(); } $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags); if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsed instanceof \stdClass) { $parsed = (array) $parsed; } if (!\is_array($parsed)) { throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } if (isset($parsed[0])) { // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier // in the sequence override keys specified in later mapping nodes. foreach ($parsed as $parsedItem) { if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsedItem instanceof \stdClass) { $parsedItem = (array) $parsedItem; } if (!\is_array($parsedItem)) { throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename); } $data += $parsedItem; // array union } } else { // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the // current mapping, unless the key already exists in it. $data += $parsed; // array union } } } elseif ('<<' !== $key && isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) { $isRef = $matches['ref']; $this->refsBeingParsed[] = $isRef; $values['value'] = $matches['value']; } $subTag = null; if ($mergeNode) { // Merge keys } elseif (!isset($values['value']) || '' === $values['value'] || 0 === strpos($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) { // hash // if next line is less indented or equal, then it means that the current value is null if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { // Spec: Keys MUST be unique; first one wins. // But overwriting is allowed when a merge node is used in current block. if ($allowOverwrite || !isset($data[$key])) { if (null !== $subTag) { $data[$key] = new TaggedValue($subTag, ''); } else { $data[$key] = null; } } else { throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine); } } else { // remember the parsed line number here in case we need it to provide some contexts in error messages below $realCurrentLineNbKey = $this->getRealCurrentLineNb(); $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags); if ('<<' === $key) { $this->refs[$refMatches['ref']] = $value; if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) { $value = (array) $value; } $data += $value; } elseif ($allowOverwrite || !isset($data[$key])) { // Spec: Keys MUST be unique; first one wins. // But overwriting is allowed when a merge node is used in current block. if (null !== $subTag) { $data[$key] = new TaggedValue($subTag, $value); } else { $data[$key] = $value; } } else { throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $realCurrentLineNbKey + 1, $this->currentLine); } } } else { $value = $this->parseValue(rtrim($values['value']), $flags, $context); // Spec: Keys MUST be unique; first one wins. // But overwriting is allowed when a merge node is used in current block. if ($allowOverwrite || !isset($data[$key])) { $data[$key] = $value; } else { throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine); } } if ($isRef) { $this->refs[$isRef] = $data[$key]; array_pop($this->refsBeingParsed); } } elseif ('"' === $this->currentLine[0] || "'" === $this->currentLine[0]) { if (null !== $context) { throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } try { return Inline::parse($this->lexInlineQuotedString(), $flags, $this->refs); } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); throw $e; } } elseif ('{' === $this->currentLine[0]) { if (null !== $context) { throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } try { $parsedMapping = Inline::parse($this->lexInlineMapping(), $flags, $this->refs); while ($this->moveToNextLine()) { if (!$this->isCurrentLineEmpty()) { throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } } return $parsedMapping; } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); throw $e; } } elseif ('[' === $this->currentLine[0]) { if (null !== $context) { throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } try { $parsedSequence = Inline::parse($this->lexInlineSequence(), $flags, $this->refs); while ($this->moveToNextLine()) { if (!$this->isCurrentLineEmpty()) { throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } } return $parsedSequence; } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); throw $e; } } else { // multiple documents are not supported if ('---' === $this->currentLine) { throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine, $this->filename); } if ($deprecatedUsage = (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1])) { throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } // 1-liner optionally followed by newline(s) if (\is_string($value) && $this->lines[0] === trim($value)) { try { $value = Inline::parse($this->lines[0], $flags, $this->refs); } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); throw $e; } return $value; } // try to parse the value as a multi-line string as a last resort if (0 === $this->currentLineNb) { $previousLineWasNewline = false; $previousLineWasTerminatedWithBackslash = false; $value = ''; foreach ($this->lines as $line) { $trimmedLine = trim($line); if ('#' === ($trimmedLine[0] ?? '')) { continue; } // If the indentation is not consistent at offset 0, it is to be considered as a ParseError if (0 === $this->offset && !$deprecatedUsage && isset($line[0]) && ' ' === $line[0]) { throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } if (false !== strpos($line, ': ')) { throw new ParseException('Mapping values are not allowed in multi-line blocks.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } if ('' === $trimmedLine) { $value .= "\n"; } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) { $value .= ' '; } if ('' !== $trimmedLine && '\\' === substr($line, -1)) { $value .= ltrim(substr($line, 0, -1)); } elseif ('' !== $trimmedLine) { $value .= $trimmedLine; } if ('' === $trimmedLine) { $previousLineWasNewline = true; $previousLineWasTerminatedWithBackslash = false; } elseif ('\\' === substr($line, -1)) { $previousLineWasNewline = false; $previousLineWasTerminatedWithBackslash = true; } else { $previousLineWasNewline = false; $previousLineWasTerminatedWithBackslash = false; } } try { return Inline::parse(trim($value)); } catch (ParseException $e) { // fall-through to the ParseException thrown below } } throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } } while ($this->moveToNextLine()); if (null !== $tag) { $data = new TaggedValue($tag, $data); } if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && 'mapping' === $context && !\is_object($data)) { $object = new \stdClass(); foreach ($data as $key => $value) { $object->$key = $value; } $data = $object; } return empty($data) ? null : $data; } private function parseBlock(int $offset, string $yaml, int $flags) { $skippedLineNumbers = $this->skippedLineNumbers; foreach ($this->locallySkippedLineNumbers as $lineNumber) { if ($lineNumber < $offset) { continue; } $skippedLineNumbers[] = $lineNumber; } $parser = new self(); $parser->offset = $offset; $parser->totalNumberOfLines = $this->totalNumberOfLines; $parser->skippedLineNumbers = $skippedLineNumbers; $parser->refs = &$this->refs; $parser->refsBeingParsed = $this->refsBeingParsed; return $parser->doParse($yaml, $flags); } /** * Returns the current line number (takes the offset into account). * * @internal */ public function getRealCurrentLineNb(): int { $realCurrentLineNumber = $this->currentLineNb + $this->offset; foreach ($this->skippedLineNumbers as $skippedLineNumber) { if ($skippedLineNumber > $realCurrentLineNumber) { break; } ++$realCurrentLineNumber; } return $realCurrentLineNumber; } private function getCurrentLineIndentation(): int { if (' ' !== ($this->currentLine[0] ?? '')) { return 0; } return \strlen($this->currentLine) - \strlen(ltrim($this->currentLine, ' ')); } /** * Returns the next embed block of YAML. * * @param int|null $indentation The indent level at which the block is to be read, or null for default * @param bool $inSequence True if the enclosing data structure is a sequence * * @throws ParseException When indentation problem are detected */ private function getNextEmbedBlock(int $indentation = null, bool $inSequence = false): string { $oldLineIndentation = $this->getCurrentLineIndentation(); if (!$this->moveToNextLine()) { return ''; } if (null === $indentation) { $newIndent = null; $movements = 0; do { $EOF = false; // empty and comment-like lines do not influence the indentation depth if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) { $EOF = !$this->moveToNextLine(); if (!$EOF) { ++$movements; } } else { $newIndent = $this->getCurrentLineIndentation(); } } while (!$EOF && null === $newIndent); for ($i = 0; $i < $movements; ++$i) { $this->moveToPreviousLine(); } $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem(); if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) { throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } } else { $newIndent = $indentation; } $data = []; if ($this->getCurrentLineIndentation() >= $newIndent) { $data[] = substr($this->currentLine, $newIndent ?? 0); } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) { $data[] = $this->currentLine; } else { $this->moveToPreviousLine(); return ''; } if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) { // the previous line contained a dash but no item content, this line is a sequence item with the same indentation // and therefore no nested list or mapping $this->moveToPreviousLine(); return ''; } $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem(); $isItComment = $this->isCurrentLineComment(); while ($this->moveToNextLine()) { if ($isItComment && !$isItUnindentedCollection) { $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem(); $isItComment = $this->isCurrentLineComment(); } $indent = $this->getCurrentLineIndentation(); if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) { $this->moveToPreviousLine(); break; } if ($this->isCurrentLineBlank()) { $data[] = substr($this->currentLine, $newIndent); continue; } if ($indent >= $newIndent) { $data[] = substr($this->currentLine, $newIndent); } elseif ($this->isCurrentLineComment()) { $data[] = $this->currentLine; } elseif (0 == $indent) { $this->moveToPreviousLine(); break; } else { throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } } return implode("\n", $data); } private function hasMoreLines(): bool { return (\count($this->lines) - 1) > $this->currentLineNb; } /** * Moves the parser to the next line. */ private function moveToNextLine(): bool { if ($this->currentLineNb >= $this->numberOfParsedLines - 1) { return false; } $this->currentLine = $this->lines[++$this->currentLineNb]; return true; } /** * Moves the parser to the previous line. */ private function moveToPreviousLine(): bool { if ($this->currentLineNb < 1) { return false; } $this->currentLine = $this->lines[--$this->currentLineNb]; return true; } /** * Parses a YAML value. * * @param string $value A YAML value * @param int $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior * @param string $context The parser context (either sequence or mapping) * * @throws ParseException When reference does not exist */ private function parseValue(string $value, int $flags, string $context): mixed { if (0 === strpos($value, '*')) { if (false !== $pos = strpos($value, '#')) { $value = substr($value, 1, $pos - 2); } else { $value = substr($value, 1); } if (!\array_key_exists($value, $this->refs)) { if (false !== $pos = array_search($value, $this->refsBeingParsed, true)) { throw new ParseException(sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, $pos), [$value])), $value), $this->currentLineNb + 1, $this->currentLine, $this->filename); } throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename); } return $this->refs[$value]; } if (\in_array($value[0], ['!', '|', '>'], true) && self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) { $modifiers = $matches['modifiers'] ?? ''; $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), abs((int) $modifiers)); if ('' !== $matches['tag'] && '!' !== $matches['tag']) { if ('!!binary' === $matches['tag']) { return Inline::evaluateBinaryScalar($data); } return new TaggedValue(substr($matches['tag'], 1), $data); } return $data; } try { if ('' !== $value && '{' === $value[0]) { $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value)); return Inline::parse($this->lexInlineMapping($cursor), $flags, $this->refs); } elseif ('' !== $value && '[' === $value[0]) { $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value)); return Inline::parse($this->lexInlineSequence($cursor), $flags, $this->refs); } switch ($value[0] ?? '') { case '"': case "'": $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value)); $parsedValue = Inline::parse($this->lexInlineQuotedString($cursor), $flags, $this->refs); if (isset($this->currentLine[$cursor]) && preg_replace('/\s*(#.*)?$/A', '', substr($this->currentLine, $cursor))) { throw new ParseException(sprintf('Unexpected characters near "%s".', substr($this->currentLine, $cursor))); } return $parsedValue; default: $lines = []; while ($this->moveToNextLine()) { // unquoted strings end before the first unindented line if (0 === $this->getCurrentLineIndentation()) { $this->moveToPreviousLine(); break; } $lines[] = trim($this->currentLine); } for ($i = 0, $linesCount = \count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) { if ('' === $lines[$i]) { $value .= "\n"; $previousLineBlank = true; } elseif ($previousLineBlank) { $value .= $lines[$i]; $previousLineBlank = false; } else { $value .= ' '.$lines[$i]; $previousLineBlank = false; } } Inline::$parsedLineNumber = $this->getRealCurrentLineNb(); $parsedValue = Inline::parse($value, $flags, $this->refs); if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) { throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename); } return $parsedValue; } } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); throw $e; } } /** * Parses a block scalar. * * @param string $style The style indicator that was used to begin this block scalar (| or >) * @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -) * @param int $indentation The indentation indicator that was used to begin this block scalar */ private function parseBlockScalar(string $style, string $chomping = '', int $indentation = 0): string { $notEOF = $this->moveToNextLine(); if (!$notEOF) { return ''; } $isCurrentLineBlank = $this->isCurrentLineBlank(); $blockLines = []; // leading blank lines are consumed before determining indentation while ($notEOF && $isCurrentLineBlank) { // newline only if not EOF if ($notEOF = $this->moveToNextLine()) { $blockLines[] = ''; $isCurrentLineBlank = $this->isCurrentLineBlank(); } } // determine indentation if not specified if (0 === $indentation) { $currentLineLength = \strlen($this->currentLine); for ($i = 0; $i < $currentLineLength && ' ' === $this->currentLine[$i]; ++$i) { ++$indentation; } } if ($indentation > 0) { $pattern = sprintf('/^ {%d}(.*)$/', $indentation); while ( $notEOF && ( $isCurrentLineBlank || self::preg_match($pattern, $this->currentLine, $matches) ) ) { if ($isCurrentLineBlank && \strlen($this->currentLine) > $indentation) { $blockLines[] = substr($this->currentLine, $indentation); } elseif ($isCurrentLineBlank) { $blockLines[] = ''; } else { $blockLines[] = $matches[1]; } // newline only if not EOF if ($notEOF = $this->moveToNextLine()) { $isCurrentLineBlank = $this->isCurrentLineBlank(); } } } elseif ($notEOF) { $blockLines[] = ''; } if ($notEOF) { $blockLines[] = ''; $this->moveToPreviousLine(); } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) { $blockLines[] = ''; } // folded style if ('>' === $style) { $text = ''; $previousLineIndented = false; $previousLineBlank = false; for ($i = 0, $blockLinesCount = \count($blockLines); $i < $blockLinesCount; ++$i) { if ('' === $blockLines[$i]) { $text .= "\n"; $previousLineIndented = false; $previousLineBlank = true; } elseif (' ' === $blockLines[$i][0]) { $text .= "\n".$blockLines[$i]; $previousLineIndented = true; $previousLineBlank = false; } elseif ($previousLineIndented) { $text .= "\n".$blockLines[$i]; $previousLineIndented = false; $previousLineBlank = false; } elseif ($previousLineBlank || 0 === $i) { $text .= $blockLines[$i]; $previousLineIndented = false; $previousLineBlank = false; } else { $text .= ' '.$blockLines[$i]; $previousLineIndented = false; $previousLineBlank = false; } } } else { $text = implode("\n", $blockLines); } // deal with trailing newlines if ('' === $chomping) { $text = preg_replace('/\n+$/', "\n", $text); } elseif ('-' === $chomping) { $text = preg_replace('/\n+$/', '', $text); } return $text; } /** * Returns true if the next line is indented. */ private function isNextLineIndented(): bool { $currentIndentation = $this->getCurrentLineIndentation(); $movements = 0; do { $EOF = !$this->moveToNextLine(); if (!$EOF) { ++$movements; } } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment())); if ($EOF) { return false; } $ret = $this->getCurrentLineIndentation() > $currentIndentation; for ($i = 0; $i < $movements; ++$i) { $this->moveToPreviousLine(); } return $ret; } private function isCurrentLineEmpty(): bool { return $this->isCurrentLineBlank() || $this->isCurrentLineComment(); } private function isCurrentLineBlank(): bool { return '' === $this->currentLine || '' === trim($this->currentLine, ' '); } private function isCurrentLineComment(): bool { // checking explicitly the first char of the trim is faster than loops or strpos $ltrimmedLine = '' !== $this->currentLine && ' ' === $this->currentLine[0] ? ltrim($this->currentLine, ' ') : $this->currentLine; return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0]; } private function isCurrentLineLastLineInDocument(): bool { return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1); } private function cleanup(string $value): string { $value = str_replace(["\r\n", "\r"], "\n", $value); // strip YAML header $count = 0; $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count); $this->offset += $count; // remove leading comments $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count); if (1 === $count) { // items have been removed, update the offset $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); $value = $trimmedValue; } // remove start of the document marker (---) $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count); if (1 === $count) { // items have been removed, update the offset $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); $value = $trimmedValue; // remove end of the document marker (...) $value = preg_replace('#\.\.\.\s*$#', '', $value); } return $value; } private function isNextLineUnIndentedCollection(): bool { $currentIndentation = $this->getCurrentLineIndentation(); $movements = 0; do { $EOF = !$this->moveToNextLine(); if (!$EOF) { ++$movements; } } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment())); if ($EOF) { return false; } $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem(); for ($i = 0; $i < $movements; ++$i) { $this->moveToPreviousLine(); } return $ret; } private function isStringUnIndentedCollectionItem(): bool { return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- '); } /** * A local wrapper for "preg_match" which will throw a ParseException if there * is an internal error in the PCRE engine. * * This avoids us needing to check for "false" every time PCRE is used * in the YAML engine * * @throws ParseException on a PCRE internal error * * @see preg_last_error() * * @internal */ public static function preg_match(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0): int { if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) { switch (preg_last_error()) { case \PREG_INTERNAL_ERROR: $error = 'Internal PCRE error.'; break; case \PREG_BACKTRACK_LIMIT_ERROR: $error = 'pcre.backtrack_limit reached.'; break; case \PREG_RECURSION_LIMIT_ERROR: $error = 'pcre.recursion_limit reached.'; break; case \PREG_BAD_UTF8_ERROR: $error = 'Malformed UTF-8 data.'; break; case \PREG_BAD_UTF8_OFFSET_ERROR: $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.'; break; default: $error = 'Error.'; } throw new ParseException($error); } return $ret; } /** * Trim the tag on top of the value. * * Prevent values such as "!foo {quz: bar}" to be considered as * a mapping block. */ private function trimTag(string $value): string { if ('!' === $value[0]) { return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' '); } return $value; } private function getLineTag(string $value, int $flags, bool $nextLineCheck = true): ?string { if ('' === $value || '!' !== $value[0] || 1 !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/', $value, $matches)) { return null; } if ($nextLineCheck && !$this->isNextLineIndented()) { return null; } $tag = substr($matches['tag'], 1); // Built-in tags if ($tag && '!' === $tag[0]) { throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), $this->getRealCurrentLineNb() + 1, $value, $this->filename); } if (Yaml::PARSE_CUSTOM_TAGS & $flags) { return $tag; } throw new ParseException(sprintf('Tags support is not enabled. You must use the flag "Yaml::PARSE_CUSTOM_TAGS" to use "%s".', $matches['tag']), $this->getRealCurrentLineNb() + 1, $value, $this->filename); } private function lexInlineQuotedString(int &$cursor = 0): string { $quotation = $this->currentLine[$cursor]; $value = $quotation; ++$cursor; $previousLineWasNewline = true; $previousLineWasTerminatedWithBackslash = false; $lineNumber = 0; do { if (++$lineNumber > 1) { $cursor += strspn($this->currentLine, ' ', $cursor); } if ($this->isCurrentLineBlank()) { $value .= "\n"; } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) { $value .= ' '; } for (; \strlen($this->currentLine) > $cursor; ++$cursor) { switch ($this->currentLine[$cursor]) { case '\\': if ("'" === $quotation) { $value .= '\\'; } elseif (isset($this->currentLine[++$cursor])) { $value .= '\\'.$this->currentLine[$cursor]; } break; case $quotation: ++$cursor; if ("'" === $quotation && isset($this->currentLine[$cursor]) && "'" === $this->currentLine[$cursor]) { $value .= "''"; break; } return $value.$quotation; default: $value .= $this->currentLine[$cursor]; } } if ($this->isCurrentLineBlank()) { $previousLineWasNewline = true; $previousLineWasTerminatedWithBackslash = false; } elseif ('\\' === $this->currentLine[-1]) { $previousLineWasNewline = false; $previousLineWasTerminatedWithBackslash = true; } else { $previousLineWasNewline = false; $previousLineWasTerminatedWithBackslash = false; } if ($this->hasMoreLines()) { $cursor = 0; } } while ($this->moveToNextLine()); throw new ParseException('Malformed inline YAML string.'); } private function lexUnquotedString(int &$cursor): string { $offset = $cursor; $cursor += strcspn($this->currentLine, '[]{},: ', $cursor); if ($cursor === $offset) { throw new ParseException('Malformed unquoted YAML string.'); } return substr($this->currentLine, $offset, $cursor - $offset); } private function lexInlineMapping(int &$cursor = 0): string { return $this->lexInlineStructure($cursor, '}'); } private function lexInlineSequence(int &$cursor = 0): string { return $this->lexInlineStructure($cursor, ']'); } private function lexInlineStructure(int &$cursor, string $closingTag): string { $value = $this->currentLine[$cursor]; ++$cursor; do { $this->consumeWhitespaces($cursor); while (isset($this->currentLine[$cursor])) { switch ($this->currentLine[$cursor]) { case '"': case "'": $value .= $this->lexInlineQuotedString($cursor); break; case ':': case ',': $value .= $this->currentLine[$cursor]; ++$cursor; break; case '{': $value .= $this->lexInlineMapping($cursor); break; case '[': $value .= $this->lexInlineSequence($cursor); break; case $closingTag: $value .= $this->currentLine[$cursor]; ++$cursor; return $value; case '#': break 2; default: $value .= $this->lexUnquotedString($cursor); } if ($this->consumeWhitespaces($cursor)) { $value .= ' '; } } if ($this->hasMoreLines()) { $cursor = 0; } } while ($this->moveToNextLine()); throw new ParseException('Malformed inline YAML string.'); } private function consumeWhitespaces(int &$cursor): bool { $whitespacesConsumed = 0; do { $whitespaceOnlyTokenLength = strspn($this->currentLine, ' ', $cursor); $whitespacesConsumed += $whitespaceOnlyTokenLength; $cursor += $whitespaceOnlyTokenLength; if (isset($this->currentLine[$cursor])) { return 0 < $whitespacesConsumed; } if ($this->hasMoreLines()) { $cursor = 0; } } while ($this->moveToNextLine()); return 0 < $whitespacesConsumed; } } yaml/Resources/bin/yaml-lint 0000644 00000002342 15025017654 0012067 0 ustar 00 #!/usr/bin/env php <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if ('cli' !== \PHP_SAPI) { throw new Exception('This script must be run from the command line.'); } /** * Runs the Yaml lint command. * * @author Jan Schädlich <jan.schaedlich@sensiolabs.de> */ use Symfony\Component\Console\Application; use Symfony\Component\Yaml\Command\LintCommand; function includeIfExists(string $file): bool { return file_exists($file) && include $file; } if ( !includeIfExists(__DIR__ . '/../../../../autoload.php') && !includeIfExists(__DIR__ . '/../../vendor/autoload.php') && !includeIfExists(__DIR__ . '/../../../../../../vendor/autoload.php') ) { fwrite(STDERR, 'Install dependencies using Composer.'.PHP_EOL); exit(1); } if (!class_exists(Application::class)) { fwrite(STDERR, 'You need the "symfony/console" component in order to run the Yaml linter.'.PHP_EOL); exit(1); } (new Application())->add($command = new LintCommand()) ->getApplication() ->setDefaultCommand($command->getName(), true) ->run() ; http-foundation/AcceptHeader.php 0000644 00000006652 15025017654 0012726 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; // Help opcache.preload discover always-needed symbols class_exists(AcceptHeaderItem::class); /** * Represents an Accept-* header. * * An accept header is compound with a list of items, * sorted by descending quality. * * @author Jean-François Simon <contact@jfsimon.fr> */ class AcceptHeader { /** * @var AcceptHeaderItem[] */ private array $items = []; private bool $sorted = true; /** * @param AcceptHeaderItem[] $items */ public function __construct(array $items) { foreach ($items as $item) { $this->add($item); } } /** * Builds an AcceptHeader instance from a string. */ public static function fromString(?string $headerValue): self { $index = 0; $parts = HeaderUtils::split($headerValue ?? '', ',;='); return new self(array_map(function ($subParts) use (&$index) { $part = array_shift($subParts); $attributes = HeaderUtils::combine($subParts); $item = new AcceptHeaderItem($part[0], $attributes); $item->setIndex($index++); return $item; }, $parts)); } /** * Returns header value's string representation. */ public function __toString(): string { return implode(',', $this->items); } /** * Tests if header has given value. */ public function has(string $value): bool { return isset($this->items[$value]); } /** * Returns given value's item, if exists. */ public function get(string $value): ?AcceptHeaderItem { return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null; } /** * Adds an item. * * @return $this */ public function add(AcceptHeaderItem $item): static { $this->items[$item->getValue()] = $item; $this->sorted = false; return $this; } /** * Returns all items. * * @return AcceptHeaderItem[] */ public function all(): array { $this->sort(); return $this->items; } /** * Filters items on their value using given regex. */ public function filter(string $pattern): self { return new self(array_filter($this->items, function (AcceptHeaderItem $item) use ($pattern) { return preg_match($pattern, $item->getValue()); })); } /** * Returns first item. */ public function first(): ?AcceptHeaderItem { $this->sort(); return !empty($this->items) ? reset($this->items) : null; } /** * Sorts items by descending quality. */ private function sort(): void { if (!$this->sorted) { uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) { $qA = $a->getQuality(); $qB = $b->getQuality(); if ($qA === $qB) { return $a->getIndex() > $b->getIndex() ? 1 : -1; } return $qA > $qB ? -1 : 1; }); $this->sorted = true; } } } http-foundation/File/UploadedFile.php 0000644 00000023177 15025017654 0013633 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File; use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException; use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException; use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException; use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException; use Symfony\Component\HttpFoundation\File\Exception\NoFileException; use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException; use Symfony\Component\HttpFoundation\File\Exception\PartialFileException; use Symfony\Component\Mime\MimeTypes; /** * A file uploaded through a form. * * @author Bernhard Schussek <bschussek@gmail.com> * @author Florian Eckerstorfer <florian@eckerstorfer.org> * @author Fabien Potencier <fabien@symfony.com> */ class UploadedFile extends File { private bool $test; private string $originalName; private string $mimeType; private int $error; /** * Accepts the information of the uploaded file as provided by the PHP global $_FILES. * * The file object is only created when the uploaded file is valid (i.e. when the * isValid() method returns true). Otherwise the only methods that could be called * on an UploadedFile instance are: * * * getClientOriginalName, * * getClientMimeType, * * isValid, * * getError. * * Calling any other method on an non-valid instance will cause an unpredictable result. * * @param string $path The full temporary path to the file * @param string $originalName The original file name of the uploaded file * @param string|null $mimeType The type of the file as provided by PHP; null defaults to application/octet-stream * @param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants); null defaults to UPLOAD_ERR_OK * @param bool $test Whether the test mode is active * Local files are used in test mode hence the code should not enforce HTTP uploads * * @throws FileException If file_uploads is disabled * @throws FileNotFoundException If the file does not exist */ public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, bool $test = false) { $this->originalName = $this->getName($originalName); $this->mimeType = $mimeType ?: 'application/octet-stream'; $this->error = $error ?: \UPLOAD_ERR_OK; $this->test = $test; parent::__construct($path, \UPLOAD_ERR_OK === $this->error); } /** * Returns the original file name. * * It is extracted from the request from which the file has been uploaded. * Then it should not be considered as a safe value. */ public function getClientOriginalName(): string { return $this->originalName; } /** * Returns the original file extension. * * It is extracted from the original file name that was uploaded. * Then it should not be considered as a safe value. */ public function getClientOriginalExtension(): string { return pathinfo($this->originalName, \PATHINFO_EXTENSION); } /** * Returns the file mime type. * * The client mime type is extracted from the request from which the file * was uploaded, so it should not be considered as a safe value. * * For a trusted mime type, use getMimeType() instead (which guesses the mime * type based on the file content). * * @see getMimeType() */ public function getClientMimeType(): string { return $this->mimeType; } /** * Returns the extension based on the client mime type. * * If the mime type is unknown, returns null. * * This method uses the mime type as guessed by getClientMimeType() * to guess the file extension. As such, the extension returned * by this method cannot be trusted. * * For a trusted extension, use guessExtension() instead (which guesses * the extension based on the guessed mime type for the file). * * @see guessExtension() * @see getClientMimeType() */ public function guessClientExtension(): ?string { if (!class_exists(MimeTypes::class)) { throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".'); } return MimeTypes::getDefault()->getExtensions($this->getClientMimeType())[0] ?? null; } /** * Returns the upload error. * * If the upload was successful, the constant UPLOAD_ERR_OK is returned. * Otherwise one of the other UPLOAD_ERR_XXX constants is returned. */ public function getError(): int { return $this->error; } /** * Returns whether the file has been uploaded with HTTP and no error occurred. */ public function isValid(): bool { $isOk = \UPLOAD_ERR_OK === $this->error; return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname()); } /** * Moves the file to a new location. * * @throws FileException if, for any reason, the file could not have been moved */ public function move(string $directory, string $name = null): File { if ($this->isValid()) { if ($this->test) { return parent::move($directory, $name); } $target = $this->getTargetFile($directory, $name); set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); try { $moved = move_uploaded_file($this->getPathname(), $target); } finally { restore_error_handler(); } if (!$moved) { throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); } @chmod($target, 0666 & ~umask()); return $target; } switch ($this->error) { case \UPLOAD_ERR_INI_SIZE: throw new IniSizeFileException($this->getErrorMessage()); case \UPLOAD_ERR_FORM_SIZE: throw new FormSizeFileException($this->getErrorMessage()); case \UPLOAD_ERR_PARTIAL: throw new PartialFileException($this->getErrorMessage()); case \UPLOAD_ERR_NO_FILE: throw new NoFileException($this->getErrorMessage()); case \UPLOAD_ERR_CANT_WRITE: throw new CannotWriteFileException($this->getErrorMessage()); case \UPLOAD_ERR_NO_TMP_DIR: throw new NoTmpDirFileException($this->getErrorMessage()); case \UPLOAD_ERR_EXTENSION: throw new ExtensionFileException($this->getErrorMessage()); } throw new FileException($this->getErrorMessage()); } /** * Returns the maximum size of an uploaded file as configured in php.ini. * * @return int|float The maximum size of an uploaded file in bytes (returns float if size > PHP_INT_MAX) */ public static function getMaxFilesize(): int|float { $sizePostMax = self::parseFilesize(\ini_get('post_max_size')); $sizeUploadMax = self::parseFilesize(\ini_get('upload_max_filesize')); return min($sizePostMax ?: \PHP_INT_MAX, $sizeUploadMax ?: \PHP_INT_MAX); } private static function parseFilesize(string $size): int|float { if ('' === $size) { return 0; } $size = strtolower($size); $max = ltrim($size, '+'); if (str_starts_with($max, '0x')) { $max = \intval($max, 16); } elseif (str_starts_with($max, '0')) { $max = \intval($max, 8); } else { $max = (int) $max; } switch (substr($size, -1)) { case 't': $max *= 1024; // no break case 'g': $max *= 1024; // no break case 'm': $max *= 1024; // no break case 'k': $max *= 1024; } return $max; } /** * Returns an informative upload error message. */ public function getErrorMessage(): string { static $errors = [ \UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).', \UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.', \UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.', \UPLOAD_ERR_NO_FILE => 'No file was uploaded.', \UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.', \UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.', \UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.', ]; $errorCode = $this->error; $maxFilesize = \UPLOAD_ERR_INI_SIZE === $errorCode ? self::getMaxFilesize() / 1024 : 0; $message = $errors[$errorCode] ?? 'The file "%s" was not uploaded due to an unknown error.'; return sprintf($message, $this->getClientOriginalName(), $maxFilesize); } } http-foundation/File/Stream.php 0000644 00000000737 15025017654 0012526 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File; /** * A PHP stream of unknown size. * * @author Nicolas Grekas <p@tchwork.com> */ class Stream extends File { public function getSize(): int|false { return false; } } http-foundation/File/Exception/FileNotFoundException.php 0000644 00000001112 15025017654 0017430 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; /** * Thrown when a file was not found. * * @author Bernhard Schussek <bschussek@gmail.com> */ class FileNotFoundException extends FileException { public function __construct(string $path) { parent::__construct(sprintf('The file "%s" does not exist', $path)); } } http-foundation/File/Exception/UploadException.php 0000644 00000000715 15025017654 0016330 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; /** * Thrown when an error occurred during file upload. * * @author Bernhard Schussek <bschussek@gmail.com> */ class UploadException extends FileException { } http-foundation/File/Exception/IniSizeFileException.php 0000644 00000000742 15025017654 0017256 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; /** * Thrown when an UPLOAD_ERR_INI_SIZE error occurred with UploadedFile. * * @author Florent Mata <florentmata@gmail.com> */ class IniSizeFileException extends FileException { } http-foundation/File/Exception/NoFileException.php 0000644 00000000734 15025017654 0016261 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; /** * Thrown when an UPLOAD_ERR_NO_FILE error occurred with UploadedFile. * * @author Florent Mata <florentmata@gmail.com> */ class NoFileException extends FileException { } http-foundation/File/Exception/FileException.php 0000644 00000000722 15025017654 0015761 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; /** * Thrown when an error occurred in the component File. * * @author Bernhard Schussek <bschussek@gmail.com> */ class FileException extends \RuntimeException { } http-foundation/File/Exception/FormSizeFileException.php 0000644 00000000744 15025017654 0017444 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; /** * Thrown when an UPLOAD_ERR_FORM_SIZE error occurred with UploadedFile. * * @author Florent Mata <florentmata@gmail.com> */ class FormSizeFileException extends FileException { } http-foundation/File/Exception/PartialFileException.php 0000644 00000000741 15025017654 0017277 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; /** * Thrown when an UPLOAD_ERR_PARTIAL error occurred with UploadedFile. * * @author Florent Mata <florentmata@gmail.com> */ class PartialFileException extends FileException { } http-foundation/File/Exception/AccessDeniedException.php 0000644 00000001132 15025017654 0017410 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; /** * Thrown when the access on a file was denied. * * @author Bernhard Schussek <bschussek@gmail.com> */ class AccessDeniedException extends FileException { public function __construct(string $path) { parent::__construct(sprintf('The file %s could not be accessed', $path)); } } http-foundation/File/Exception/UnexpectedTypeException.php 0000644 00000001051 15025017654 0020044 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; class UnexpectedTypeException extends FileException { public function __construct(mixed $value, string $expectedType) { parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, get_debug_type($value))); } } http-foundation/File/Exception/NoTmpDirFileException.php 0000644 00000000745 15025017654 0017403 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; /** * Thrown when an UPLOAD_ERR_NO_TMP_DIR error occurred with UploadedFile. * * @author Florent Mata <florentmata@gmail.com> */ class NoTmpDirFileException extends FileException { } http-foundation/File/Exception/ExtensionFileException.php 0000644 00000000745 15025017654 0017663 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; /** * Thrown when an UPLOAD_ERR_EXTENSION error occurred with UploadedFile. * * @author Florent Mata <florentmata@gmail.com> */ class ExtensionFileException extends FileException { } http-foundation/File/Exception/CannotWriteFileException.php 0000644 00000000750 15025017654 0020140 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File\Exception; /** * Thrown when an UPLOAD_ERR_CANT_WRITE error occurred with UploadedFile. * * @author Florent Mata <florentmata@gmail.com> */ class CannotWriteFileException extends FileException { } http-foundation/File/File.php 0000644 00000010536 15025017654 0012150 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\File; use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; use Symfony\Component\Mime\MimeTypes; /** * A file in the file system. * * @author Bernhard Schussek <bschussek@gmail.com> */ class File extends \SplFileInfo { /** * Constructs a new file from the given path. * * @param string $path The path to the file * @param bool $checkPath Whether to check the path or not * * @throws FileNotFoundException If the given path is not a file */ public function __construct(string $path, bool $checkPath = true) { if ($checkPath && !is_file($path)) { throw new FileNotFoundException($path); } parent::__construct($path); } /** * Returns the extension based on the mime type. * * If the mime type is unknown, returns null. * * This method uses the mime type as guessed by getMimeType() * to guess the file extension. * * @see MimeTypes * @see getMimeType() */ public function guessExtension(): ?string { if (!class_exists(MimeTypes::class)) { throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".'); } return MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null; } /** * Returns the mime type of the file. * * The mime type is guessed using a MimeTypeGuesserInterface instance, * which uses finfo_file() then the "file" system binary, * depending on which of those are available. * * @see MimeTypes */ public function getMimeType(): ?string { if (!class_exists(MimeTypes::class)) { throw new \LogicException('You cannot guess the mime type as the Mime component is not installed. Try running "composer require symfony/mime".'); } return MimeTypes::getDefault()->guessMimeType($this->getPathname()); } /** * Moves the file to a new location. * * @throws FileException if the target file could not be created */ public function move(string $directory, string $name = null): self { $target = $this->getTargetFile($directory, $name); set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); try { $renamed = rename($this->getPathname(), $target); } finally { restore_error_handler(); } if (!$renamed) { throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); } @chmod($target, 0666 & ~umask()); return $target; } public function getContent(): string { $content = file_get_contents($this->getPathname()); if (false === $content) { throw new FileException(sprintf('Could not get the content of the file "%s".', $this->getPathname())); } return $content; } protected function getTargetFile(string $directory, string $name = null): self { if (!is_dir($directory)) { if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) { throw new FileException(sprintf('Unable to create the "%s" directory.', $directory)); } } elseif (!is_writable($directory)) { throw new FileException(sprintf('Unable to write in the "%s" directory.', $directory)); } $target = rtrim($directory, '/\\').\DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name)); return new self($target, false); } /** * Returns locale independent base name of the given path. */ protected function getName(string $name): string { $originalName = str_replace('\\', '/', $name); $pos = strrpos($originalName, '/'); $originalName = false === $pos ? $originalName : substr($originalName, $pos + 1); return $originalName; } } http-foundation/Session/SessionFactoryInterface.php 0000644 00000000664 15025017654 0016632 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session; /** * @author Kevin Bond <kevinbond@gmail.com> */ interface SessionFactoryInterface { public function createSession(): SessionInterface; } http-foundation/Session/SessionBagProxy.php 0000644 00000004264 15025017654 0015135 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session; /** * @author Nicolas Grekas <p@tchwork.com> * * @internal */ final class SessionBagProxy implements SessionBagInterface { private $bag; private array $data; private ?int $usageIndex; private ?\Closure $usageReporter; public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex, ?callable $usageReporter) { $this->bag = $bag; $this->data = &$data; $this->usageIndex = &$usageIndex; $this->usageReporter = $usageReporter instanceof \Closure || !\is_callable($usageReporter) ? $usageReporter : \Closure::fromCallable($usageReporter); } public function getBag(): SessionBagInterface { ++$this->usageIndex; if ($this->usageReporter && 0 <= $this->usageIndex) { ($this->usageReporter)(); } return $this->bag; } public function isEmpty(): bool { if (!isset($this->data[$this->bag->getStorageKey()])) { return true; } ++$this->usageIndex; if ($this->usageReporter && 0 <= $this->usageIndex) { ($this->usageReporter)(); } return empty($this->data[$this->bag->getStorageKey()]); } /** * {@inheritdoc} */ public function getName(): string { return $this->bag->getName(); } /** * {@inheritdoc} */ public function initialize(array &$array): void { ++$this->usageIndex; if ($this->usageReporter && 0 <= $this->usageIndex) { ($this->usageReporter)(); } $this->data[$this->bag->getStorageKey()] = &$array; $this->bag->initialize($array); } /** * {@inheritdoc} */ public function getStorageKey(): string { return $this->bag->getStorageKey(); } /** * {@inheritdoc} */ public function clear(): mixed { return $this->bag->clear(); } } http-foundation/Session/Storage/SessionStorageInterface.php 0000644 00000007172 15025017654 0020234 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Session\SessionBagInterface; /** * StorageInterface. * * @author Fabien Potencier <fabien@symfony.com> * @author Drak <drak@zikula.org> */ interface SessionStorageInterface { /** * Starts the session. * * @throws \RuntimeException if something goes wrong starting the session */ public function start(): bool; /** * Checks if the session is started. */ public function isStarted(): bool; /** * Returns the session ID. */ public function getId(): string; /** * Sets the session ID. */ public function setId(string $id); /** * Returns the session name. */ public function getName(): string; /** * Sets the session name. */ public function setName(string $name); /** * Regenerates id that represents this storage. * * This method must invoke session_regenerate_id($destroy) unless * this interface is used for a storage object designed for unit * or functional testing where a real PHP session would interfere * with testing. * * Note regenerate+destroy should not clear the session data in memory * only delete the session data from persistent storage. * * Care: When regenerating the session ID no locking is involved in PHP's * session design. See https://bugs.php.net/61470 for a discussion. * So you must make sure the regenerated session is saved BEFORE sending the * headers with the new ID. Symfony's HttpKernel offers a listener for this. * See Symfony\Component\HttpKernel\EventListener\SaveSessionListener. * Otherwise session data could get lost again for concurrent requests with the * new ID. One result could be that you get logged out after just logging in. * * @param bool $destroy Destroy session when regenerating? * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value * will leave the system settings unchanged, 0 sets the cookie * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. * * @throws \RuntimeException If an error occurs while regenerating this storage */ public function regenerate(bool $destroy = false, int $lifetime = null): bool; /** * Force the session to be saved and closed. * * This method must invoke session_write_close() unless this interface is * used for a storage object design for unit or functional testing where * a real PHP session would interfere with testing, in which case * it should actually persist the session data if required. * * @throws \RuntimeException if the session is saved without being started, or if the session * is already closed */ public function save(); /** * Clear all session data in memory. */ public function clear(); /** * Gets a SessionBagInterface by name. * * @throws \InvalidArgumentException If the bag does not exist */ public function getBag(string $name): SessionBagInterface; /** * Registers a SessionBagInterface for use. */ public function registerBag(SessionBagInterface $bag); public function getMetadataBag(): MetadataBag; } http-foundation/Session/Storage/NativeSessionStorageFactory.php 0000644 00000002702 15025017654 0021104 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; // Help opcache.preload discover always-needed symbols class_exists(NativeSessionStorage::class); /** * @author Jérémy Derussé <jeremy@derusse.com> */ class NativeSessionStorageFactory implements SessionStorageFactoryInterface { private array $options; private $handler; private $metaBag; private bool $secure; /** * @see NativeSessionStorage constructor. */ public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface $handler = null, MetadataBag $metaBag = null, bool $secure = false) { $this->options = $options; $this->handler = $handler; $this->metaBag = $metaBag; $this->secure = $secure; } public function createStorage(?Request $request): SessionStorageInterface { $storage = new NativeSessionStorage($this->options, $this->handler, $this->metaBag); if ($this->secure && $request && $request->isSecure()) { $storage->setOptions(['cookie_secure' => true]); } return $storage; } } http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php 0000644 00000004267 15025017654 0020551 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; /** * @author Drak <drak@zikula.org> */ class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { protected $handler; public function __construct(\SessionHandlerInterface $handler) { $this->handler = $handler; $this->wrapper = $handler instanceof \SessionHandler; $this->saveHandlerName = $this->wrapper || ($handler instanceof StrictSessionHandler && $handler->isWrapper()) ? \ini_get('session.save_handler') : 'user'; } public function getHandler(): \SessionHandlerInterface { return $this->handler; } // \SessionHandlerInterface public function open(string $savePath, string $sessionName): bool { return $this->handler->open($savePath, $sessionName); } public function close(): bool { return $this->handler->close(); } public function read(string $sessionId): string|false { return $this->handler->read($sessionId); } public function write(string $sessionId, string $data): bool { return $this->handler->write($sessionId, $data); } public function destroy(string $sessionId): bool { return $this->handler->destroy($sessionId); } public function gc(int $maxlifetime): int|false { return $this->handler->gc($maxlifetime); } public function validateId(string $sessionId): bool { return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId); } public function updateTimestamp(string $sessionId, string $data): bool { return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data); } } http-foundation/Session/Storage/Proxy/AbstractProxy.php 0000644 00000004170 15025017654 0017364 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; /** * @author Drak <drak@zikula.org> */ abstract class AbstractProxy { /** * Flag if handler wraps an internal PHP session handler (using \SessionHandler). * * @var bool */ protected $wrapper = false; /** * @var string */ protected $saveHandlerName; /** * Gets the session.save_handler name. */ public function getSaveHandlerName(): ?string { return $this->saveHandlerName; } /** * Is this proxy handler and instance of \SessionHandlerInterface. */ public function isSessionHandlerInterface(): bool { return $this instanceof \SessionHandlerInterface; } /** * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. */ public function isWrapper(): bool { return $this->wrapper; } /** * Has a session started? */ public function isActive(): bool { return \PHP_SESSION_ACTIVE === session_status(); } /** * Gets the session ID. */ public function getId(): string { return session_id(); } /** * Sets the session ID. * * @throws \LogicException */ public function setId(string $id) { if ($this->isActive()) { throw new \LogicException('Cannot change the ID of an active session.'); } session_id($id); } /** * Gets the session name. */ public function getName(): string { return session_name(); } /** * Sets the session name. * * @throws \LogicException */ public function setName(string $name) { if ($this->isActive()) { throw new \LogicException('Cannot change the name of an active session.'); } session_name($name); } } http-foundation/Session/Storage/MockFileSessionStorage.php 0000644 00000007747 15025017654 0020035 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage; /** * MockFileSessionStorage is used to mock sessions for * functional testing where you may need to persist session data * across separate PHP processes. * * No PHP session is actually started since a session can be initialized * and shutdown only once per PHP execution cycle and this class does * not pollute any session related globals, including session_*() functions * or session.* PHP ini directives. * * @author Drak <drak@zikula.org> */ class MockFileSessionStorage extends MockArraySessionStorage { private string $savePath; /** * @param string|null $savePath Path of directory to save session files */ public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null) { if (null === $savePath) { $savePath = sys_get_temp_dir(); } if (!is_dir($savePath) && !@mkdir($savePath, 0777, true) && !is_dir($savePath)) { throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s".', $savePath)); } $this->savePath = $savePath; parent::__construct($name, $metaBag); } /** * {@inheritdoc} */ public function start(): bool { if ($this->started) { return true; } if (!$this->id) { $this->id = $this->generateId(); } $this->read(); $this->started = true; return true; } /** * {@inheritdoc} */ public function regenerate(bool $destroy = false, int $lifetime = null): bool { if (!$this->started) { $this->start(); } if ($destroy) { $this->destroy(); } return parent::regenerate($destroy, $lifetime); } /** * {@inheritdoc} */ public function save() { if (!$this->started) { throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.'); } $data = $this->data; foreach ($this->bags as $bag) { if (empty($data[$key = $bag->getStorageKey()])) { unset($data[$key]); } } if ([$key = $this->metadataBag->getStorageKey()] === array_keys($data)) { unset($data[$key]); } try { if ($data) { $path = $this->getFilePath(); $tmp = $path.bin2hex(random_bytes(6)); file_put_contents($tmp, serialize($data)); rename($tmp, $path); } else { $this->destroy(); } } finally { $this->data = $data; } // this is needed when the session object is re-used across multiple requests // in functional tests. $this->started = false; } /** * Deletes a session from persistent storage. * Deliberately leaves session data in memory intact. */ private function destroy(): void { set_error_handler(static function () {}); try { unlink($this->getFilePath()); } finally { restore_error_handler(); } } /** * Calculate path to file. */ private function getFilePath(): string { return $this->savePath.'/'.$this->id.'.mocksess'; } /** * Reads session from storage and loads session. */ private function read(): void { set_error_handler(static function () {}); try { $data = file_get_contents($this->getFilePath()); } finally { restore_error_handler(); } $this->data = $data ? unserialize($data) : []; $this->loadSession(); } } http-foundation/Session/Storage/PhpBridgeSessionStorage.php 0000644 00000002655 15025017654 0020201 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; /** * Allows session to be started by PHP and managed by Symfony. * * @author Drak <drak@zikula.org> */ class PhpBridgeSessionStorage extends NativeSessionStorage { public function __construct(AbstractProxy|\SessionHandlerInterface $handler = null, MetadataBag $metaBag = null) { if (!\extension_loaded('session')) { throw new \LogicException('PHP extension "session" is required.'); } $this->setMetadataBag($metaBag); $this->setSaveHandler($handler); } /** * {@inheritdoc} */ public function start(): bool { if ($this->started) { return true; } $this->loadSession(); return true; } /** * {@inheritdoc} */ public function clear() { // clear out the bags and nothing else that may be set // since the purpose of this driver is to share a handler foreach ($this->bags as $bag) { $bag->clear(); } // reconnect the bags to the session $this->loadSession(); } } http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php 0000644 00000002451 15025017654 0021523 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; // Help opcache.preload discover always-needed symbols class_exists(PhpBridgeSessionStorage::class); /** * @author Jérémy Derussé <jeremy@derusse.com> */ class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface { private $handler; private $metaBag; private bool $secure; public function __construct(AbstractProxy|\SessionHandlerInterface $handler = null, MetadataBag $metaBag = null, bool $secure = false) { $this->handler = $handler; $this->metaBag = $metaBag; $this->secure = $secure; } public function createStorage(?Request $request): SessionStorageInterface { $storage = new PhpBridgeSessionStorage($this->handler, $this->metaBag); if ($this->secure && $request && $request->isSecure()) { $storage->setOptions(['cookie_secure' => true]); } return $storage; } } http-foundation/Session/Storage/NativeSessionStorage.php 0000644 00000034201 15025017654 0017553 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Session\SessionBagInterface; use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; // Help opcache.preload discover always-needed symbols class_exists(MetadataBag::class); class_exists(StrictSessionHandler::class); class_exists(SessionHandlerProxy::class); /** * This provides a base class for session attribute storage. * * @author Drak <drak@zikula.org> */ class NativeSessionStorage implements SessionStorageInterface { /** * @var SessionBagInterface[] */ protected $bags = []; /** * @var bool */ protected $started = false; /** * @var bool */ protected $closed = false; /** * @var AbstractProxy|\SessionHandlerInterface */ protected $saveHandler; /** * @var MetadataBag */ protected $metadataBag; /** * Depending on how you want the storage driver to behave you probably * want to override this constructor entirely. * * List of options for $options array with their defaults. * * @see https://php.net/session.configuration for options * but we omit 'session.' from the beginning of the keys for convenience. * * ("auto_start", is not supported as it tells PHP to start a session before * PHP starts to execute user-land code. Setting during runtime has no effect). * * cache_limiter, "" (use "0" to prevent headers from being sent entirely). * cache_expire, "0" * cookie_domain, "" * cookie_httponly, "" * cookie_lifetime, "0" * cookie_path, "/" * cookie_secure, "" * cookie_samesite, null * gc_divisor, "100" * gc_maxlifetime, "1440" * gc_probability, "1" * lazy_write, "1" * name, "PHPSESSID" * referer_check, "" * serialize_handler, "php" * use_strict_mode, "1" * use_cookies, "1" * use_only_cookies, "1" * use_trans_sid, "0" * sid_length, "32" * sid_bits_per_character, "5" * trans_sid_hosts, $_SERVER['HTTP_HOST'] * trans_sid_tags, "a=href,area=href,frame=src,form=" */ public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface $handler = null, MetadataBag $metaBag = null) { if (!\extension_loaded('session')) { throw new \LogicException('PHP extension "session" is required.'); } $options += [ 'cache_limiter' => '', 'cache_expire' => 0, 'use_cookies' => 1, 'lazy_write' => 1, 'use_strict_mode' => 1, ]; session_register_shutdown(); $this->setMetadataBag($metaBag); $this->setOptions($options); $this->setSaveHandler($handler); } /** * Gets the save handler instance. */ public function getSaveHandler(): AbstractProxy|\SessionHandlerInterface { return $this->saveHandler; } /** * {@inheritdoc} */ public function start(): bool { if ($this->started) { return true; } if (\PHP_SESSION_ACTIVE === session_status()) { throw new \RuntimeException('Failed to start the session: already started by PHP.'); } if (filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN) && headers_sent($file, $line)) { throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line)); } $sessionId = $_COOKIE[session_name()] ?? null; /* * Explanation of the session ID regular expression: `/^[a-zA-Z0-9,-]{22,250}$/`. * * ---------- Part 1 * * The part `[a-zA-Z0-9,-]` is related to the PHP ini directive `session.sid_bits_per_character` defined as 6. * See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-bits-per-character. * Allowed values are integers such as: * - 4 for range `a-f0-9` * - 5 for range `a-v0-9` * - 6 for range `a-zA-Z0-9,-` * * ---------- Part 2 * * The part `{22,250}` is related to the PHP ini directive `session.sid_length`. * See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length. * Allowed values are integers between 22 and 256, but we use 250 for the max. * * Where does the 250 come from? * - The length of Windows and Linux filenames is limited to 255 bytes. Then the max must not exceed 255. * - The session filename prefix is `sess_`, a 5 bytes string. Then the max must not exceed 255 - 5 = 250. * * ---------- Conclusion * * The parts 1 and 2 prevent the warning below: * `PHP Warning: SessionHandler::read(): Session ID is too long or contains illegal characters. Only the A-Z, a-z, 0-9, "-", and "," characters are allowed.` * * The part 2 prevents the warning below: * `PHP Warning: SessionHandler::read(): open(filepath, O_RDWR) failed: No such file or directory (2).` */ if ($sessionId && $this->saveHandler instanceof AbstractProxy && 'files' === $this->saveHandler->getSaveHandlerName() && !preg_match('/^[a-zA-Z0-9,-]{22,250}$/', $sessionId)) { // the session ID in the header is invalid, create a new one session_id(session_create_id()); } // ok to try and start the session if (!session_start()) { throw new \RuntimeException('Failed to start the session.'); } $this->loadSession(); return true; } /** * {@inheritdoc} */ public function getId(): string { return $this->saveHandler->getId(); } /** * {@inheritdoc} */ public function setId(string $id) { $this->saveHandler->setId($id); } /** * {@inheritdoc} */ public function getName(): string { return $this->saveHandler->getName(); } /** * {@inheritdoc} */ public function setName(string $name) { $this->saveHandler->setName($name); } /** * {@inheritdoc} */ public function regenerate(bool $destroy = false, int $lifetime = null): bool { // Cannot regenerate the session ID for non-active sessions. if (\PHP_SESSION_ACTIVE !== session_status()) { return false; } if (headers_sent()) { return false; } if (null !== $lifetime && $lifetime != \ini_get('session.cookie_lifetime')) { $this->save(); ini_set('session.cookie_lifetime', $lifetime); $this->start(); } if ($destroy) { $this->metadataBag->stampNew(); } return session_regenerate_id($destroy); } /** * {@inheritdoc} */ public function save() { // Store a copy so we can restore the bags in case the session was not left empty $session = $_SESSION; foreach ($this->bags as $bag) { if (empty($_SESSION[$key = $bag->getStorageKey()])) { unset($_SESSION[$key]); } } if ($_SESSION && [$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) { unset($_SESSION[$key]); } // Register error handler to add information about the current save handler $previousHandler = set_error_handler(function ($type, $msg, $file, $line) use (&$previousHandler) { if (\E_WARNING === $type && str_starts_with($msg, 'session_write_close():')) { $handler = $this->saveHandler instanceof SessionHandlerProxy ? $this->saveHandler->getHandler() : $this->saveHandler; $msg = sprintf('session_write_close(): Failed to write session data with "%s" handler', \get_class($handler)); } return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false; }); try { session_write_close(); } finally { restore_error_handler(); // Restore only if not empty if ($_SESSION) { $_SESSION = $session; } } $this->closed = true; $this->started = false; } /** * {@inheritdoc} */ public function clear() { // clear out the bags foreach ($this->bags as $bag) { $bag->clear(); } // clear out the session $_SESSION = []; // reconnect the bags to the session $this->loadSession(); } /** * {@inheritdoc} */ public function registerBag(SessionBagInterface $bag) { if ($this->started) { throw new \LogicException('Cannot register a bag when the session is already started.'); } $this->bags[$bag->getName()] = $bag; } /** * {@inheritdoc} */ public function getBag(string $name): SessionBagInterface { if (!isset($this->bags[$name])) { throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name)); } if (!$this->started && $this->saveHandler->isActive()) { $this->loadSession(); } elseif (!$this->started) { $this->start(); } return $this->bags[$name]; } public function setMetadataBag(MetadataBag $metaBag = null) { if (null === $metaBag) { $metaBag = new MetadataBag(); } $this->metadataBag = $metaBag; } /** * Gets the MetadataBag. */ public function getMetadataBag(): MetadataBag { return $this->metadataBag; } /** * {@inheritdoc} */ public function isStarted(): bool { return $this->started; } /** * Sets session.* ini variables. * * For convenience we omit 'session.' from the beginning of the keys. * Explicitly ignores other ini keys. * * @param array $options Session ini directives [key => value] * * @see https://php.net/session.configuration */ public function setOptions(array $options) { if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { return; } $validOptions = array_flip([ 'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly', 'cookie_lifetime', 'cookie_path', 'cookie_secure', 'cookie_samesite', 'gc_divisor', 'gc_maxlifetime', 'gc_probability', 'lazy_write', 'name', 'referer_check', 'serialize_handler', 'use_strict_mode', 'use_cookies', 'use_only_cookies', 'use_trans_sid', 'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags', ]); foreach ($options as $key => $value) { if (isset($validOptions[$key])) { if ('cookie_secure' === $key && 'auto' === $value) { continue; } ini_set('session.'.$key, $value); } } } /** * Registers session save handler as a PHP session handler. * * To use internal PHP session save handlers, override this method using ini_set with * session.save_handler and session.save_path e.g. * * ini_set('session.save_handler', 'files'); * ini_set('session.save_path', '/tmp'); * * or pass in a \SessionHandler instance which configures session.save_handler in the * constructor, for a template see NativeFileSessionHandler. * * @see https://php.net/session-set-save-handler * @see https://php.net/sessionhandlerinterface * @see https://php.net/sessionhandler * * @throws \InvalidArgumentException */ public function setSaveHandler(AbstractProxy|\SessionHandlerInterface $saveHandler = null) { if (!$saveHandler instanceof AbstractProxy && !$saveHandler instanceof \SessionHandlerInterface && null !== $saveHandler) { throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.'); } // Wrap $saveHandler in proxy and prevent double wrapping of proxy if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) { $saveHandler = new SessionHandlerProxy($saveHandler); } elseif (!$saveHandler instanceof AbstractProxy) { $saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler())); } $this->saveHandler = $saveHandler; if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { return; } if ($this->saveHandler instanceof SessionHandlerProxy) { session_set_save_handler($this->saveHandler, false); } } /** * Load the session with attributes. * * After starting the session, PHP retrieves the session from whatever handlers * are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()). * PHP takes the return value from the read() handler, unserializes it * and populates $_SESSION with the result automatically. */ protected function loadSession(array &$session = null) { if (null === $session) { $session = &$_SESSION; } $bags = array_merge($this->bags, [$this->metadataBag]); foreach ($bags as $bag) { $key = $bag->getStorageKey(); $session[$key] = isset($session[$key]) && \is_array($session[$key]) ? $session[$key] : []; $bag->initialize($session[$key]); } $this->started = true; $this->closed = false; } } http-foundation/Session/Storage/SessionStorageFactoryInterface.php 0000644 00000001132 15025017654 0021552 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Request; /** * @author Jérémy Derussé <jeremy@derusse.com> */ interface SessionStorageFactoryInterface { /** * Creates a new instance of SessionStorageInterface. */ public function createStorage(?Request $request): SessionStorageInterface; } http-foundation/Session/Storage/MockFileSessionStorageFactory.php 0000644 00000002163 15025017654 0021350 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Request; // Help opcache.preload discover always-needed symbols class_exists(MockFileSessionStorage::class); /** * @author Jérémy Derussé <jeremy@derusse.com> */ class MockFileSessionStorageFactory implements SessionStorageFactoryInterface { private ?string $savePath; private string $name; private $metaBag; /** * @see MockFileSessionStorage constructor. */ public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null) { $this->savePath = $savePath; $this->name = $name; $this->metaBag = $metaBag; } public function createStorage(?Request $request): SessionStorageInterface { return new MockFileSessionStorage($this->savePath, $this->name, $this->metaBag); } } http-foundation/Session/Storage/MetadataBag.php 0000644 00000007000 15025017654 0015563 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Session\SessionBagInterface; /** * Metadata container. * * Adds metadata to the session. * * @author Drak <drak@zikula.org> */ class MetadataBag implements SessionBagInterface { public const CREATED = 'c'; public const UPDATED = 'u'; public const LIFETIME = 'l'; private string $name = '__metadata'; private string $storageKey; /** * @var array */ protected $meta = [self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0]; /** * Unix timestamp. */ private int $lastUsed; private int $updateThreshold; /** * @param string $storageKey The key used to store bag in the session * @param int $updateThreshold The time to wait between two UPDATED updates */ public function __construct(string $storageKey = '_sf2_meta', int $updateThreshold = 0) { $this->storageKey = $storageKey; $this->updateThreshold = $updateThreshold; } /** * {@inheritdoc} */ public function initialize(array &$array) { $this->meta = &$array; if (isset($array[self::CREATED])) { $this->lastUsed = $this->meta[self::UPDATED]; $timeStamp = time(); if ($timeStamp - $array[self::UPDATED] >= $this->updateThreshold) { $this->meta[self::UPDATED] = $timeStamp; } } else { $this->stampCreated(); } } /** * Gets the lifetime that the session cookie was set with. */ public function getLifetime(): int { return $this->meta[self::LIFETIME]; } /** * Stamps a new session's metadata. * * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value * will leave the system settings unchanged, 0 sets the cookie * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. */ public function stampNew(int $lifetime = null) { $this->stampCreated($lifetime); } /** * {@inheritdoc} */ public function getStorageKey(): string { return $this->storageKey; } /** * Gets the created timestamp metadata. * * @return int Unix timestamp */ public function getCreated(): int { return $this->meta[self::CREATED]; } /** * Gets the last used metadata. * * @return int Unix timestamp */ public function getLastUsed(): int { return $this->lastUsed; } /** * {@inheritdoc} */ public function clear(): mixed { // nothing to do return null; } /** * {@inheritdoc} */ public function getName(): string { return $this->name; } /** * Sets name. */ public function setName(string $name) { $this->name = $name; } private function stampCreated(int $lifetime = null): void { $timeStamp = time(); $this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp; $this->meta[self::LIFETIME] = $lifetime ?? (int) \ini_get('session.cookie_lifetime'); } } http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php 0000644 00000011667 15025017654 0021213 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; use MongoDB\BSON\Binary; use MongoDB\BSON\UTCDateTime; use MongoDB\Client; use MongoDB\Collection; /** * Session handler using the mongodb/mongodb package and MongoDB driver extension. * * @author Markus Bachmann <markus.bachmann@bachi.biz> * * @see https://packagist.org/packages/mongodb/mongodb * @see https://php.net/mongodb */ class MongoDbSessionHandler extends AbstractSessionHandler { private $mongo; private $collection; private array $options; /** * Constructor. * * List of available options: * * database: The name of the database [required] * * collection: The name of the collection [required] * * id_field: The field name for storing the session id [default: _id] * * data_field: The field name for storing the session data [default: data] * * time_field: The field name for storing the timestamp [default: time] * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. * * It is strongly recommended to put an index on the `expiry_field` for * garbage-collection. Alternatively it's possible to automatically expire * the sessions in the database as described below: * * A TTL collections can be used on MongoDB 2.2+ to cleanup expired sessions * automatically. Such an index can for example look like this: * * db.<session-collection>.createIndex( * { "<expiry-field>": 1 }, * { "expireAfterSeconds": 0 } * ) * * More details on: https://docs.mongodb.org/manual/tutorial/expire-data/ * * If you use such an index, you can drop `gc_probability` to 0 since * no garbage-collection is required. * * @throws \InvalidArgumentException When "database" or "collection" not provided */ public function __construct(Client $mongo, array $options) { if (!isset($options['database']) || !isset($options['collection'])) { throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler.'); } $this->mongo = $mongo; $this->options = array_merge([ 'id_field' => '_id', 'data_field' => 'data', 'time_field' => 'time', 'expiry_field' => 'expires_at', ], $options); } public function close(): bool { return true; } /** * {@inheritdoc} */ protected function doDestroy(string $sessionId): bool { $this->getCollection()->deleteOne([ $this->options['id_field'] => $sessionId, ]); return true; } public function gc(int $maxlifetime): int|false { return $this->getCollection()->deleteMany([ $this->options['expiry_field'] => ['$lt' => new UTCDateTime()], ])->getDeletedCount(); } /** * {@inheritdoc} */ protected function doWrite(string $sessionId, string $data): bool { $expiry = new UTCDateTime((time() + (int) \ini_get('session.gc_maxlifetime')) * 1000); $fields = [ $this->options['time_field'] => new UTCDateTime(), $this->options['expiry_field'] => $expiry, $this->options['data_field'] => new Binary($data, Binary::TYPE_OLD_BINARY), ]; $this->getCollection()->updateOne( [$this->options['id_field'] => $sessionId], ['$set' => $fields], ['upsert' => true] ); return true; } public function updateTimestamp(string $sessionId, string $data): bool { $expiry = new UTCDateTime((time() + (int) \ini_get('session.gc_maxlifetime')) * 1000); $this->getCollection()->updateOne( [$this->options['id_field'] => $sessionId], ['$set' => [ $this->options['time_field'] => new UTCDateTime(), $this->options['expiry_field'] => $expiry, ]] ); return true; } /** * {@inheritdoc} */ protected function doRead(string $sessionId): string { $dbData = $this->getCollection()->findOne([ $this->options['id_field'] => $sessionId, $this->options['expiry_field'] => ['$gte' => new UTCDateTime()], ]); if (null === $dbData) { return ''; } return $dbData[$this->options['data_field']]->getData(); } private function getCollection(): Collection { return $this->collection ??= $this->mongo->selectCollection($this->options['database'], $this->options['collection']); } protected function getMongo(): Client { return $this->mongo; } } http-foundation/Session/Storage/Handler/NullSessionHandler.php 0000644 00000002341 15025017654 0020565 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** * Can be used in unit testing or in a situations where persisted sessions are not desired. * * @author Drak <drak@zikula.org> */ class NullSessionHandler extends AbstractSessionHandler { public function close(): bool { return true; } public function validateId(string $sessionId): bool { return true; } /** * {@inheritdoc} */ protected function doRead(string $sessionId): string { return ''; } public function updateTimestamp(string $sessionId, string $data): bool { return true; } /** * {@inheritdoc} */ protected function doWrite(string $sessionId, string $data): bool { return true; } /** * {@inheritdoc} */ protected function doDestroy(string $sessionId): bool { return true; } public function gc(int $maxlifetime): int|false { return 0; } } http-foundation/Session/Storage/Handler/SessionHandlerFactory.php 0000644 00000007314 15025017654 0021267 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; use Doctrine\DBAL\DriverManager; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Traits\RedisClusterProxy; use Symfony\Component\Cache\Traits\RedisProxy; /** * @author Nicolas Grekas <p@tchwork.com> */ class SessionHandlerFactory { public static function createHandler(object|string $connection): AbstractSessionHandler { if ($options = \is_string($connection) ? parse_url($connection) : false) { parse_str($options['query'] ?? '', $options); } switch (true) { case $connection instanceof \Redis: case $connection instanceof \RedisArray: case $connection instanceof \RedisCluster: case $connection instanceof \Predis\ClientInterface: case $connection instanceof RedisProxy: case $connection instanceof RedisClusterProxy: return new RedisSessionHandler($connection); case $connection instanceof \Memcached: return new MemcachedSessionHandler($connection); case $connection instanceof \PDO: return new PdoSessionHandler($connection); case !\is_string($connection): throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', get_debug_type($connection))); case str_starts_with($connection, 'file://'): $savePath = substr($connection, 7); return new StrictSessionHandler(new NativeFileSessionHandler('' === $savePath ? null : $savePath)); case str_starts_with($connection, 'redis:'): case str_starts_with($connection, 'rediss:'): case str_starts_with($connection, 'memcached:'): if (!class_exists(AbstractAdapter::class)) { throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection)); } $handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class; $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]); return new $handlerClass($connection, array_intersect_key($options ?: [], ['prefix' => 1, 'ttl' => 1])); case str_starts_with($connection, 'pdo_oci://'): if (!class_exists(DriverManager::class)) { throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require doctrine/dbal".', $connection)); } $connection = DriverManager::getConnection(['url' => $connection])->getWrappedConnection(); // no break; case str_starts_with($connection, 'mssql://'): case str_starts_with($connection, 'mysql://'): case str_starts_with($connection, 'mysql2://'): case str_starts_with($connection, 'pgsql://'): case str_starts_with($connection, 'postgres://'): case str_starts_with($connection, 'postgresql://'): case str_starts_with($connection, 'sqlsrv://'): case str_starts_with($connection, 'sqlite://'): case str_starts_with($connection, 'sqlite3://'): return new PdoSessionHandler($connection, $options ?: []); } throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection)); } } http-foundation/Session/Storage/Handler/IdentityMarshaller.php 0000644 00000001714 15025017654 0020620 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; use Symfony\Component\Cache\Marshaller\MarshallerInterface; /** * @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com> */ class IdentityMarshaller implements MarshallerInterface { /** * {@inheritdoc} */ public function marshall(array $values, ?array &$failed): array { foreach ($values as $key => $value) { if (!\is_string($value)) { throw new \LogicException(sprintf('%s accepts only string as data.', __METHOD__)); } } return $values; } /** * {@inheritdoc} */ public function unmarshall(string $value): string { return $value; } } http-foundation/Session/Storage/Handler/AbstractSessionHandler.php 0000644 00000007541 15025017654 0021425 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; use Symfony\Component\HttpFoundation\Session\SessionUtils; /** * This abstract session handler provides a generic implementation * of the PHP 7.0 SessionUpdateTimestampHandlerInterface, * enabling strict and lazy session handling. * * @author Nicolas Grekas <p@tchwork.com> */ abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { private string $sessionName; private string $prefetchId; private string $prefetchData; private ?string $newSessionId = null; private string $igbinaryEmptyData; public function open(string $savePath, string $sessionName): bool { $this->sessionName = $sessionName; if (!headers_sent() && !\ini_get('session.cache_limiter') && '0' !== \ini_get('session.cache_limiter')) { header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) \ini_get('session.cache_expire'))); } return true; } abstract protected function doRead(string $sessionId): string; abstract protected function doWrite(string $sessionId, string $data): bool; abstract protected function doDestroy(string $sessionId): bool; public function validateId(string $sessionId): bool { $this->prefetchData = $this->read($sessionId); $this->prefetchId = $sessionId; return '' !== $this->prefetchData; } public function read(string $sessionId): string { if (isset($this->prefetchId)) { $prefetchId = $this->prefetchId; $prefetchData = $this->prefetchData; unset($this->prefetchId, $this->prefetchData); if ($prefetchId === $sessionId || '' === $prefetchData) { $this->newSessionId = '' === $prefetchData ? $sessionId : null; return $prefetchData; } } $data = $this->doRead($sessionId); $this->newSessionId = '' === $data ? $sessionId : null; return $data; } public function write(string $sessionId, string $data): bool { // see https://github.com/igbinary/igbinary/issues/146 $this->igbinaryEmptyData ??= \function_exists('igbinary_serialize') ? igbinary_serialize([]) : ''; if ('' === $data || $this->igbinaryEmptyData === $data) { return $this->destroy($sessionId); } $this->newSessionId = null; return $this->doWrite($sessionId, $data); } public function destroy(string $sessionId): bool { if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN)) { if (!isset($this->sessionName)) { throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class)); } $cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId); /* * We send an invalidation Set-Cookie header (zero lifetime) * when either the session was started or a cookie with * the session name was sent by the client (in which case * we know it's invalid as a valid session cookie would've * started the session). */ if (null === $cookie || isset($_COOKIE[$this->sessionName])) { $params = session_get_cookie_params(); unset($params['lifetime']); setcookie($this->sessionName, '', $params); } } return $this->newSessionId === $sessionId || $this->doDestroy($sessionId); } } http-foundation/Session/Storage/Handler/StrictSessionHandler.php 0000644 00000004743 15025017654 0021133 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** * Adds basic `SessionUpdateTimestampHandlerInterface` behaviors to another `SessionHandlerInterface`. * * @author Nicolas Grekas <p@tchwork.com> */ class StrictSessionHandler extends AbstractSessionHandler { private \SessionHandlerInterface $handler; private bool $doDestroy; public function __construct(\SessionHandlerInterface $handler) { if ($handler instanceof \SessionUpdateTimestampHandlerInterface) { throw new \LogicException(sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_debug_type($handler), self::class)); } $this->handler = $handler; } /** * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. * * @internal */ public function isWrapper(): bool { return $this->handler instanceof \SessionHandler; } public function open(string $savePath, string $sessionName): bool { parent::open($savePath, $sessionName); return $this->handler->open($savePath, $sessionName); } /** * {@inheritdoc} */ protected function doRead(string $sessionId): string { return $this->handler->read($sessionId); } public function updateTimestamp(string $sessionId, string $data): bool { return $this->write($sessionId, $data); } /** * {@inheritdoc} */ protected function doWrite(string $sessionId, string $data): bool { return $this->handler->write($sessionId, $data); } public function destroy(string $sessionId): bool { $this->doDestroy = true; $destroyed = parent::destroy($sessionId); return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed; } /** * {@inheritdoc} */ protected function doDestroy(string $sessionId): bool { $this->doDestroy = false; return $this->handler->destroy($sessionId); } public function close(): bool { return $this->handler->close(); } public function gc(int $maxlifetime): int|false { return $this->handler->gc($maxlifetime); } } http-foundation/Session/Storage/Handler/PdoSessionHandler.php 0000644 00000106056 15025017654 0020405 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** * Session handler using a PDO connection to read and write data. * * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements * different locking strategies to handle concurrent access to the same session. * Locking is necessary to prevent loss of data due to race conditions and to keep * the session data consistent between read() and write(). With locking, requests * for the same session will wait until the other one finished writing. For this * reason it's best practice to close a session as early as possible to improve * concurrency. PHPs internal files session handler also implements locking. * * Attention: Since SQLite does not support row level locks but locks the whole database, * it means only one session can be accessed at a time. Even different sessions would wait * for another to finish. So saving session in SQLite should only be considered for * development or prototypes. * * Session data is a binary string that can contain non-printable characters like the null byte. * For this reason it must be saved in a binary column in the database like BLOB in MySQL. * Saving it in a character column could corrupt the data. You can use createTable() * to initialize a correctly defined table. * * @see https://php.net/sessionhandlerinterface * * @author Fabien Potencier <fabien@symfony.com> * @author Michael Williams <michael.williams@funsational.com> * @author Tobias Schultze <http://tobion.de> */ class PdoSessionHandler extends AbstractSessionHandler { /** * No locking is done. This means sessions are prone to loss of data due to * race conditions of concurrent requests to the same session. The last session * write will win in this case. It might be useful when you implement your own * logic to deal with this like an optimistic approach. */ public const LOCK_NONE = 0; /** * Creates an application-level lock on a session. The disadvantage is that the * lock is not enforced by the database and thus other, unaware parts of the * application could still concurrently modify the session. The advantage is it * does not require a transaction. * This mode is not available for SQLite and not yet implemented for oci and sqlsrv. */ public const LOCK_ADVISORY = 1; /** * Issues a real row lock. Since it uses a transaction between opening and * closing a session, you have to be careful when you use same database connection * that you also use for your application logic. This mode is the default because * it's the only reliable solution across DBMSs. */ public const LOCK_TRANSACTIONAL = 2; private $pdo; /** * DSN string or null for session.save_path or false when lazy connection disabled. */ private string|false|null $dsn = false; private string $driver; private string $table = 'sessions'; private string $idCol = 'sess_id'; private string $dataCol = 'sess_data'; private string $lifetimeCol = 'sess_lifetime'; private string $timeCol = 'sess_time'; /** * Username when lazy-connect. */ private string $username = ''; /** * Password when lazy-connect. */ private string $password = ''; /** * Connection options when lazy-connect. */ private array $connectionOptions = []; /** * The strategy for locking, see constants. */ private int $lockMode = self::LOCK_TRANSACTIONAL; /** * It's an array to support multiple reads before closing which is manual, non-standard usage. * * @var \PDOStatement[] An array of statements to release advisory locks */ private array $unlockStatements = []; /** * True when the current session exists but expired according to session.gc_maxlifetime. */ private bool $sessionExpired = false; /** * Whether a transaction is active. */ private bool $inTransaction = false; /** * Whether gc() has been called. */ private bool $gcCalled = false; /** * You can either pass an existing database connection as PDO instance or * pass a DSN string that will be used to lazy-connect to the database * when the session is actually used. Furthermore it's possible to pass null * which will then use the session.save_path ini setting as PDO DSN parameter. * * List of available options: * * db_table: The name of the table [default: sessions] * * db_id_col: The column where to store the session id [default: sess_id] * * db_data_col: The column where to store the session data [default: sess_data] * * db_lifetime_col: The column where to store the lifetime [default: sess_lifetime] * * db_time_col: The column where to store the timestamp [default: sess_time] * * db_username: The username when lazy-connect [default: ''] * * db_password: The password when lazy-connect [default: ''] * * db_connection_options: An array of driver-specific connection options [default: []] * * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL] * * @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null * * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION */ public function __construct(\PDO|string $pdoOrDsn = null, array $options = []) { if ($pdoOrDsn instanceof \PDO) { if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__)); } $this->pdo = $pdoOrDsn; $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); } elseif (\is_string($pdoOrDsn) && str_contains($pdoOrDsn, '://')) { $this->dsn = $this->buildDsnFromUrl($pdoOrDsn); } else { $this->dsn = $pdoOrDsn; } $this->table = $options['db_table'] ?? $this->table; $this->idCol = $options['db_id_col'] ?? $this->idCol; $this->dataCol = $options['db_data_col'] ?? $this->dataCol; $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol; $this->timeCol = $options['db_time_col'] ?? $this->timeCol; $this->username = $options['db_username'] ?? $this->username; $this->password = $options['db_password'] ?? $this->password; $this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions; $this->lockMode = $options['lock_mode'] ?? $this->lockMode; } /** * Creates the table to store sessions which can be called once for setup. * * Session ID is saved in a column of maximum length 128 because that is enough even * for a 512 bit configured session.hash_function like Whirlpool. Session data is * saved in a BLOB. One could also use a shorter inlined varbinary column * if one was sure the data fits into it. * * @throws \PDOException When the table already exists * @throws \DomainException When an unsupported PDO driver is used */ public function createTable() { // connect if we are not yet $this->getConnection(); switch ($this->driver) { case 'mysql': // We use varbinary for the ID column because it prevents unwanted conversions: // - character set conversions between server and client // - trailing space removal // - case-insensitivity // - language processing like é == e $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB"; break; case 'sqlite': $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; break; case 'pgsql': $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; break; case 'oci': $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; break; case 'sqlsrv': $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; break; default: throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)); } try { $this->pdo->exec($sql); $this->pdo->exec("CREATE INDEX EXPIRY ON $this->table ($this->lifetimeCol)"); } catch (\PDOException $e) { $this->rollback(); throw $e; } } /** * Returns true when the current session exists but expired according to session.gc_maxlifetime. * * Can be used to distinguish between a new session and one that expired due to inactivity. */ public function isSessionExpired(): bool { return $this->sessionExpired; } public function open(string $savePath, string $sessionName): bool { $this->sessionExpired = false; if (!isset($this->pdo)) { $this->connect($this->dsn ?: $savePath); } return parent::open($savePath, $sessionName); } public function read(string $sessionId): string { try { return parent::read($sessionId); } catch (\PDOException $e) { $this->rollback(); throw $e; } } public function gc(int $maxlifetime): int|false { // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process. // This way, pruning expired sessions does not block them from being started while the current session is used. $this->gcCalled = true; return 0; } /** * {@inheritdoc} */ protected function doDestroy(string $sessionId): bool { // delete the record associated with this id $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; try { $stmt = $this->pdo->prepare($sql); $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $stmt->execute(); } catch (\PDOException $e) { $this->rollback(); throw $e; } return true; } /** * {@inheritdoc} */ protected function doWrite(string $sessionId, string $data): bool { $maxlifetime = (int) \ini_get('session.gc_maxlifetime'); try { // We use a single MERGE SQL query when supported by the database. $mergeStmt = $this->getMergeStatement($sessionId, $data, $maxlifetime); if (null !== $mergeStmt) { $mergeStmt->execute(); return true; } $updateStmt = $this->getUpdateStatement($sessionId, $data, $maxlifetime); $updateStmt->execute(); // When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in // duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior). // We can just catch such an error and re-execute the update. This is similar to a serializable // transaction with retry logic on serialization failures but without the overhead and without possible // false positives due to longer gap locking. if (!$updateStmt->rowCount()) { try { $insertStmt = $this->getInsertStatement($sessionId, $data, $maxlifetime); $insertStmt->execute(); } catch (\PDOException $e) { // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys if (str_starts_with($e->getCode(), '23')) { $updateStmt->execute(); } else { throw $e; } } } } catch (\PDOException $e) { $this->rollback(); throw $e; } return true; } public function updateTimestamp(string $sessionId, string $data): bool { $expiry = time() + (int) \ini_get('session.gc_maxlifetime'); try { $updateStmt = $this->pdo->prepare( "UPDATE $this->table SET $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id" ); $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $updateStmt->bindParam(':expiry', $expiry, \PDO::PARAM_INT); $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); $updateStmt->execute(); } catch (\PDOException $e) { $this->rollback(); throw $e; } return true; } public function close(): bool { $this->commit(); while ($unlockStmt = array_shift($this->unlockStatements)) { $unlockStmt->execute(); } if ($this->gcCalled) { $this->gcCalled = false; // delete the session records that have expired $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':time', time(), \PDO::PARAM_INT); $stmt->execute(); } if (false !== $this->dsn) { unset($this->pdo, $this->driver); // only close lazy-connection } return true; } /** * Lazy-connects to the database. */ private function connect(string $dsn): void { $this->pdo = new \PDO($dsn, $this->username, $this->password, $this->connectionOptions); $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); } /** * Builds a PDO DSN from a URL-like connection string. * * @todo implement missing support for oci DSN (which look totally different from other PDO ones) */ private function buildDsnFromUrl(string $dsnOrUrl): string { // (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid $url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl); $params = parse_url($url); if (false === $params) { return $dsnOrUrl; // If the URL is not valid, let's assume it might be a DSN already. } $params = array_map('rawurldecode', $params); // Override the default username and password. Values passed through options will still win over these in the constructor. if (isset($params['user'])) { $this->username = $params['user']; } if (isset($params['pass'])) { $this->password = $params['pass']; } if (!isset($params['scheme'])) { throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler.'); } $driverAliasMap = [ 'mssql' => 'sqlsrv', 'mysql2' => 'mysql', // Amazon RDS, for some weird reason 'postgres' => 'pgsql', 'postgresql' => 'pgsql', 'sqlite3' => 'sqlite', ]; $driver = $driverAliasMap[$params['scheme']] ?? $params['scheme']; // Doctrine DBAL supports passing its internal pdo_* driver names directly too (allowing both dashes and underscores). This allows supporting the same here. if (str_starts_with($driver, 'pdo_') || str_starts_with($driver, 'pdo-')) { $driver = substr($driver, 4); } $dsn = null; switch ($driver) { case 'mysql': $dsn = 'mysql:'; if ('' !== ($params['query'] ?? '')) { $queryParams = []; parse_str($params['query'], $queryParams); if ('' !== ($queryParams['charset'] ?? '')) { $dsn .= 'charset='.$queryParams['charset'].';'; } if ('' !== ($queryParams['unix_socket'] ?? '')) { $dsn .= 'unix_socket='.$queryParams['unix_socket'].';'; if (isset($params['path'])) { $dbName = substr($params['path'], 1); // Remove the leading slash $dsn .= 'dbname='.$dbName.';'; } return $dsn; } } // If "unix_socket" is not in the query, we continue with the same process as pgsql // no break case 'pgsql': $dsn ?? $dsn = 'pgsql:'; if (isset($params['host']) && '' !== $params['host']) { $dsn .= 'host='.$params['host'].';'; } if (isset($params['port']) && '' !== $params['port']) { $dsn .= 'port='.$params['port'].';'; } if (isset($params['path'])) { $dbName = substr($params['path'], 1); // Remove the leading slash $dsn .= 'dbname='.$dbName.';'; } return $dsn; case 'sqlite': return 'sqlite:'.substr($params['path'], 1); case 'sqlsrv': $dsn = 'sqlsrv:server='; if (isset($params['host'])) { $dsn .= $params['host']; } if (isset($params['port']) && '' !== $params['port']) { $dsn .= ','.$params['port']; } if (isset($params['path'])) { $dbName = substr($params['path'], 1); // Remove the leading slash $dsn .= ';Database='.$dbName; } return $dsn; default: throw new \InvalidArgumentException(sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme'])); } } /** * Helper method to begin a transaction. * * Since SQLite does not support row level locks, we have to acquire a reserved lock * on the database immediately. Because of https://bugs.php.net/42766 we have to create * such a transaction manually which also means we cannot use PDO::commit or * PDO::rollback or PDO::inTransaction for SQLite. * * Also MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions * due to https://percona.com/blog/2013/12/12/one-more-innodb-gap-lock-to-avoid/ . * So we change it to READ COMMITTED. */ private function beginTransaction(): void { if (!$this->inTransaction) { if ('sqlite' === $this->driver) { $this->pdo->exec('BEGIN IMMEDIATE TRANSACTION'); } else { if ('mysql' === $this->driver) { $this->pdo->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); } $this->pdo->beginTransaction(); } $this->inTransaction = true; } } /** * Helper method to commit a transaction. */ private function commit(): void { if ($this->inTransaction) { try { // commit read-write transaction which also releases the lock if ('sqlite' === $this->driver) { $this->pdo->exec('COMMIT'); } else { $this->pdo->commit(); } $this->inTransaction = false; } catch (\PDOException $e) { $this->rollback(); throw $e; } } } /** * Helper method to rollback a transaction. */ private function rollback(): void { // We only need to rollback if we are in a transaction. Otherwise the resulting // error would hide the real problem why rollback was called. We might not be // in a transaction when not using the transactional locking behavior or when // two callbacks (e.g. destroy and write) are invoked that both fail. if ($this->inTransaction) { if ('sqlite' === $this->driver) { $this->pdo->exec('ROLLBACK'); } else { $this->pdo->rollBack(); } $this->inTransaction = false; } } /** * Reads the session data in respect to the different locking strategies. * * We need to make sure we do not return session data that is already considered garbage according * to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. */ protected function doRead(string $sessionId): string { if (self::LOCK_ADVISORY === $this->lockMode) { $this->unlockStatements[] = $this->doAdvisoryLock($sessionId); } $selectSql = $this->getSelectSql(); $selectStmt = $this->pdo->prepare($selectSql); $selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $insertStmt = null; while (true) { $selectStmt->execute(); $sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM); if ($sessionRows) { $expiry = (int) $sessionRows[0][1]; if ($expiry < time()) { $this->sessionExpired = true; return ''; } return \is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; } if (null !== $insertStmt) { $this->rollback(); throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.'); } if (!filter_var(\ini_get('session.use_strict_mode'), \FILTER_VALIDATE_BOOLEAN) && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { // In strict mode, session fixation is not possible: new sessions always start with a unique // random id, so that concurrency is not possible and this code path can be skipped. // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block // until other connections to the session are committed. try { $insertStmt = $this->getInsertStatement($sessionId, '', 0); $insertStmt->execute(); } catch (\PDOException $e) { // Catch duplicate key error because other connection created the session already. // It would only not be the case when the other connection destroyed the session. if (str_starts_with($e->getCode(), '23')) { // Retrieve finished session data written by concurrent connection by restarting the loop. // We have to start a new transaction as a failed query will mark the current transaction as // aborted in PostgreSQL and disallow further queries within it. $this->rollback(); $this->beginTransaction(); continue; } throw $e; } } return ''; } } /** * Executes an application-level lock on the database. * * @return \PDOStatement The statement that needs to be executed later to release the lock * * @throws \DomainException When an unsupported PDO driver is used * * @todo implement missing advisory locks * - for oci using DBMS_LOCK.REQUEST * - for sqlsrv using sp_getapplock with LockOwner = Session */ private function doAdvisoryLock(string $sessionId): \PDOStatement { switch ($this->driver) { case 'mysql': // MySQL 5.7.5 and later enforces a maximum length on lock names of 64 characters. Previously, no limit was enforced. $lockId = substr($sessionId, 0, 64); // should we handle the return value? 0 on timeout, null on error // we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout $stmt = $this->pdo->prepare('SELECT GET_LOCK(:key, 50)'); $stmt->bindValue(':key', $lockId, \PDO::PARAM_STR); $stmt->execute(); $releaseStmt = $this->pdo->prepare('DO RELEASE_LOCK(:key)'); $releaseStmt->bindValue(':key', $lockId, \PDO::PARAM_STR); return $releaseStmt; case 'pgsql': // Obtaining an exclusive session level advisory lock requires an integer key. // When session.sid_bits_per_character > 4, the session id can contain non-hex-characters. // So we cannot just use hexdec(). if (4 === \PHP_INT_SIZE) { $sessionInt1 = $this->convertStringToInt($sessionId); $sessionInt2 = $this->convertStringToInt(substr($sessionId, 4, 4)); $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key1, :key2)'); $stmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); $stmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT); $stmt->execute(); $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key1, :key2)'); $releaseStmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); $releaseStmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT); } else { $sessionBigInt = $this->convertStringToInt($sessionId); $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key)'); $stmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT); $stmt->execute(); $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key)'); $releaseStmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT); } return $releaseStmt; case 'sqlite': throw new \DomainException('SQLite does not support advisory locks.'); default: throw new \DomainException(sprintf('Advisory locks are currently not implemented for PDO driver "%s".', $this->driver)); } } /** * Encodes the first 4 (when PHP_INT_SIZE == 4) or 8 characters of the string as an integer. * * Keep in mind, PHP integers are signed. */ private function convertStringToInt(string $string): int { if (4 === \PHP_INT_SIZE) { return (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); } $int1 = (\ord($string[7]) << 24) + (\ord($string[6]) << 16) + (\ord($string[5]) << 8) + \ord($string[4]); $int2 = (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); return $int2 + ($int1 << 32); } /** * Return a locking or nonlocking SQL query to read session information. * * @throws \DomainException When an unsupported PDO driver is used */ private function getSelectSql(): string { if (self::LOCK_TRANSACTIONAL === $this->lockMode) { $this->beginTransaction(); switch ($this->driver) { case 'mysql': case 'oci': case 'pgsql': return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE"; case 'sqlsrv': return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id"; case 'sqlite': // we already locked when starting transaction break; default: throw new \DomainException(sprintf('Transactional locks are currently not implemented for PDO driver "%s".', $this->driver)); } } return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WHERE $this->idCol = :id"; } /** * Returns an insert statement supported by the database for writing session data. */ private function getInsertStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement { switch ($this->driver) { case 'oci': $data = fopen('php://memory', 'r+'); fwrite($data, $sessionData); rewind($data); $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, EMPTY_BLOB(), :expiry, :time) RETURNING $this->dataCol into :data"; break; default: $data = $sessionData; $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)"; break; } $stmt = $this->pdo->prepare($sql); $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); $stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); $stmt->bindValue(':time', time(), \PDO::PARAM_INT); return $stmt; } /** * Returns an update statement supported by the database for writing session data. */ private function getUpdateStatement(string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement { switch ($this->driver) { case 'oci': $data = fopen('php://memory', 'r+'); fwrite($data, $sessionData); rewind($data); $sql = "UPDATE $this->table SET $this->dataCol = EMPTY_BLOB(), $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :data"; break; default: $data = $sessionData; $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id"; break; } $stmt = $this->pdo->prepare($sql); $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); $stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); $stmt->bindValue(':time', time(), \PDO::PARAM_INT); return $stmt; } /** * Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data. */ private function getMergeStatement(string $sessionId, string $data, int $maxlifetime): ?\PDOStatement { switch (true) { case 'mysql' === $this->driver: $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ". "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; break; case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='): // MERGE is only available since SQL Server 2008 and must be terminated by semicolon // It also requires HOLDLOCK according to https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/ $mergeSql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; break; case 'sqlite' === $this->driver: $mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)"; break; case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='): $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ". "ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; break; default: // MERGE is not supported with LOBs: https://oracle.com/technetwork/articles/fuecks-lobs-095315.html return null; } $mergeStmt = $this->pdo->prepare($mergeSql); if ('sqlsrv' === $this->driver) { $mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR); $mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR); $mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB); $mergeStmt->bindValue(4, time() + $maxlifetime, \PDO::PARAM_INT); $mergeStmt->bindValue(5, time(), \PDO::PARAM_INT); $mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB); $mergeStmt->bindValue(7, time() + $maxlifetime, \PDO::PARAM_INT); $mergeStmt->bindValue(8, time(), \PDO::PARAM_INT); } else { $mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB); $mergeStmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); $mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT); } return $mergeStmt; } /** * Return a PDO instance. */ protected function getConnection(): \PDO { if (!isset($this->pdo)) { $this->connect($this->dsn ?: \ini_get('session.save_path')); } return $this->pdo; } } http-foundation/Session/Storage/Handler/RedisSessionHandler.php 0000644 00000005726 15025017654 0020733 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; use Predis\Response\ErrorInterface; use Symfony\Component\Cache\Traits\RedisClusterProxy; use Symfony\Component\Cache\Traits\RedisProxy; /** * Redis based session storage handler based on the Redis class * provided by the PHP redis extension. * * @author Dalibor Karlović <dalibor@flexolabs.io> */ class RedisSessionHandler extends AbstractSessionHandler { private $redis; /** * Key prefix for shared environments. */ private string $prefix; /** * Time to live in seconds. */ private ?int $ttl; /** * List of available options: * * prefix: The prefix to use for the keys in order to avoid collision on the Redis server * * ttl: The time to live in seconds. * * @throws \InvalidArgumentException When unsupported client or options are passed */ public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis, array $options = []) { if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) { throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff))); } $this->redis = $redis; $this->prefix = $options['prefix'] ?? 'sf_s'; $this->ttl = $options['ttl'] ?? null; } /** * {@inheritdoc} */ protected function doRead(string $sessionId): string { return $this->redis->get($this->prefix.$sessionId) ?: ''; } /** * {@inheritdoc} */ protected function doWrite(string $sessionId, string $data): bool { $result = $this->redis->setEx($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime')), $data); return $result && !$result instanceof ErrorInterface; } /** * {@inheritdoc} */ protected function doDestroy(string $sessionId): bool { static $unlink = true; if ($unlink) { try { $unlink = false !== $this->redis->unlink($this->prefix.$sessionId); } catch (\Throwable $e) { $unlink = false; } } if (!$unlink) { $this->redis->del($this->prefix.$sessionId); } return true; } /** * {@inheritdoc} */ #[\ReturnTypeWillChange] public function close(): bool { return true; } public function gc(int $maxlifetime): int|false { return 0; } public function updateTimestamp(string $sessionId, string $data): bool { return $this->redis->expire($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime'))); } } http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php 0000644 00000003706 15025017654 0022122 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; use Symfony\Component\Cache\Marshaller\MarshallerInterface; /** * @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com> */ class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { private $handler; private $marshaller; public function __construct(AbstractSessionHandler $handler, MarshallerInterface $marshaller) { $this->handler = $handler; $this->marshaller = $marshaller; } public function open(string $savePath, string $name): bool { return $this->handler->open($savePath, $name); } public function close(): bool { return $this->handler->close(); } public function destroy(string $sessionId): bool { return $this->handler->destroy($sessionId); } public function gc(int $maxlifetime): int|false { return $this->handler->gc($maxlifetime); } public function read(string $sessionId): string { return $this->marshaller->unmarshall($this->handler->read($sessionId)); } public function write(string $sessionId, string $data): bool { $failed = []; $marshalledData = $this->marshaller->marshall(['data' => $data], $failed); if (isset($failed['data'])) { return false; } return $this->handler->write($sessionId, $marshalledData['data']); } public function validateId(string $sessionId): bool { return $this->handler->validateId($sessionId); } public function updateTimestamp(string $sessionId, string $data): bool { return $this->handler->updateTimestamp($sessionId, $data); } } http-foundation/Session/Storage/Handler/MigratingSessionHandler.php 0000644 00000006352 15025017654 0021602 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** * Migrating session handler for migrating from one handler to another. It reads * from the current handler and writes both the current and new ones. * * It ignores errors from the new handler. * * @author Ross Motley <ross.motley@amara.com> * @author Oliver Radwell <oliver.radwell@amara.com> */ class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { /** * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface */ private \SessionHandlerInterface $currentHandler; /** * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface */ private \SessionHandlerInterface $writeOnlyHandler; public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler) { if (!$currentHandler instanceof \SessionUpdateTimestampHandlerInterface) { $currentHandler = new StrictSessionHandler($currentHandler); } if (!$writeOnlyHandler instanceof \SessionUpdateTimestampHandlerInterface) { $writeOnlyHandler = new StrictSessionHandler($writeOnlyHandler); } $this->currentHandler = $currentHandler; $this->writeOnlyHandler = $writeOnlyHandler; } public function close(): bool { $result = $this->currentHandler->close(); $this->writeOnlyHandler->close(); return $result; } public function destroy(string $sessionId): bool { $result = $this->currentHandler->destroy($sessionId); $this->writeOnlyHandler->destroy($sessionId); return $result; } public function gc(int $maxlifetime): int|false { $result = $this->currentHandler->gc($maxlifetime); $this->writeOnlyHandler->gc($maxlifetime); return $result; } public function open(string $savePath, string $sessionName): bool { $result = $this->currentHandler->open($savePath, $sessionName); $this->writeOnlyHandler->open($savePath, $sessionName); return $result; } public function read(string $sessionId): string { // No reading from new handler until switch-over return $this->currentHandler->read($sessionId); } public function write(string $sessionId, string $sessionData): bool { $result = $this->currentHandler->write($sessionId, $sessionData); $this->writeOnlyHandler->write($sessionId, $sessionData); return $result; } public function validateId(string $sessionId): bool { // No reading from new handler until switch-over return $this->currentHandler->validateId($sessionId); } public function updateTimestamp(string $sessionId, string $sessionData): bool { $result = $this->currentHandler->updateTimestamp($sessionId, $sessionData); $this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData); return $result; } } http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php 0000644 00000006244 15025017654 0021527 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** * Memcached based session storage handler based on the Memcached class * provided by the PHP memcached extension. * * @see https://php.net/memcached * * @author Drak <drak@zikula.org> */ class MemcachedSessionHandler extends AbstractSessionHandler { private $memcached; /** * Time to live in seconds. */ private ?int $ttl; /** * Key prefix for shared environments. */ private string $prefix; /** * Constructor. * * List of available options: * * prefix: The prefix to use for the memcached keys in order to avoid collision * * ttl: The time to live in seconds. * * @throws \InvalidArgumentException When unsupported options are passed */ public function __construct(\Memcached $memcached, array $options = []) { $this->memcached = $memcached; if ($diff = array_diff(array_keys($options), ['prefix', 'expiretime', 'ttl'])) { throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff))); } $this->ttl = $options['expiretime'] ?? $options['ttl'] ?? null; $this->prefix = $options['prefix'] ?? 'sf2s'; } public function close(): bool { return $this->memcached->quit(); } /** * {@inheritdoc} */ protected function doRead(string $sessionId): string { return $this->memcached->get($this->prefix.$sessionId) ?: ''; } public function updateTimestamp(string $sessionId, string $data): bool { $this->memcached->touch($this->prefix.$sessionId, $this->getCompatibleTtl()); return true; } /** * {@inheritdoc} */ protected function doWrite(string $sessionId, string $data): bool { return $this->memcached->set($this->prefix.$sessionId, $data, $this->getCompatibleTtl()); } private function getCompatibleTtl(): int { $ttl = (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime')); // If the relative TTL that is used exceeds 30 days, memcached will treat the value as Unix time. // We have to convert it to an absolute Unix time at this point, to make sure the TTL is correct. if ($ttl > 60 * 60 * 24 * 30) { $ttl += time(); } return $ttl; } /** * {@inheritdoc} */ protected function doDestroy(string $sessionId): bool { $result = $this->memcached->delete($this->prefix.$sessionId); return $result || \Memcached::RES_NOTFOUND == $this->memcached->getResultCode(); } public function gc(int $maxlifetime): int|false { // not required here because memcached will auto expire the records anyhow. return 0; } /** * Return a Memcached instance. */ protected function getMemcached(): \Memcached { return $this->memcached; } } http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php 0000644 00000003444 15025017654 0021706 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** * Native session handler using PHP's built in file storage. * * @author Drak <drak@zikula.org> */ class NativeFileSessionHandler extends \SessionHandler { /** * @param string $savePath Path of directory to save session files * Default null will leave setting as defined by PHP. * '/path', 'N;/path', or 'N;octal-mode;/path * * @see https://php.net/session.configuration#ini.session.save-path for further details. * * @throws \InvalidArgumentException On invalid $savePath * @throws \RuntimeException When failing to create the save directory */ public function __construct(string $savePath = null) { if (null === $savePath) { $savePath = \ini_get('session.save_path'); } $baseDir = $savePath; if ($count = substr_count($savePath, ';')) { if ($count > 2) { throw new \InvalidArgumentException(sprintf('Invalid argument $savePath \'%s\'.', $savePath)); } // characters after last ';' are the path $baseDir = ltrim(strrchr($savePath, ';'), ';'); } if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0777, true) && !is_dir($baseDir)) { throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s".', $baseDir)); } ini_set('session.save_path', $savePath); ini_set('session.save_handler', 'files'); } } http-foundation/Session/Storage/MockArraySessionStorage.php 0000644 00000011766 15025017654 0020230 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Session\SessionBagInterface; /** * MockArraySessionStorage mocks the session for unit tests. * * No PHP session is actually started since a session can be initialized * and shutdown only once per PHP execution cycle. * * When doing functional testing, you should use MockFileSessionStorage instead. * * @author Fabien Potencier <fabien@symfony.com> * @author Bulat Shakirzyanov <mallluhuct@gmail.com> * @author Drak <drak@zikula.org> */ class MockArraySessionStorage implements SessionStorageInterface { /** * @var string */ protected $id = ''; /** * @var string */ protected $name; /** * @var bool */ protected $started = false; /** * @var bool */ protected $closed = false; /** * @var array */ protected $data = []; /** * @var MetadataBag */ protected $metadataBag; /** * @var array|SessionBagInterface[] */ protected $bags = []; public function __construct(string $name = 'MOCKSESSID', MetadataBag $metaBag = null) { $this->name = $name; $this->setMetadataBag($metaBag); } public function setSessionData(array $array) { $this->data = $array; } /** * {@inheritdoc} */ public function start(): bool { if ($this->started) { return true; } if (empty($this->id)) { $this->id = $this->generateId(); } $this->loadSession(); return true; } /** * {@inheritdoc} */ public function regenerate(bool $destroy = false, int $lifetime = null): bool { if (!$this->started) { $this->start(); } $this->metadataBag->stampNew($lifetime); $this->id = $this->generateId(); return true; } /** * {@inheritdoc} */ public function getId(): string { return $this->id; } /** * {@inheritdoc} */ public function setId(string $id) { if ($this->started) { throw new \LogicException('Cannot set session ID after the session has started.'); } $this->id = $id; } /** * {@inheritdoc} */ public function getName(): string { return $this->name; } /** * {@inheritdoc} */ public function setName(string $name) { $this->name = $name; } /** * {@inheritdoc} */ public function save() { if (!$this->started || $this->closed) { throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.'); } // nothing to do since we don't persist the session data $this->closed = false; $this->started = false; } /** * {@inheritdoc} */ public function clear() { // clear out the bags foreach ($this->bags as $bag) { $bag->clear(); } // clear out the session $this->data = []; // reconnect the bags to the session $this->loadSession(); } /** * {@inheritdoc} */ public function registerBag(SessionBagInterface $bag) { $this->bags[$bag->getName()] = $bag; } /** * {@inheritdoc} */ public function getBag(string $name): SessionBagInterface { if (!isset($this->bags[$name])) { throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name)); } if (!$this->started) { $this->start(); } return $this->bags[$name]; } /** * {@inheritdoc} */ public function isStarted(): bool { return $this->started; } public function setMetadataBag(MetadataBag $bag = null) { if (null === $bag) { $bag = new MetadataBag(); } $this->metadataBag = $bag; } /** * Gets the MetadataBag. */ public function getMetadataBag(): MetadataBag { return $this->metadataBag; } /** * Generates a session ID. * * This doesn't need to be particularly cryptographically secure since this is just * a mock. */ protected function generateId(): string { return hash('sha256', uniqid('ss_mock_', true)); } protected function loadSession() { $bags = array_merge($this->bags, [$this->metadataBag]); foreach ($bags as $bag) { $key = $bag->getStorageKey(); $this->data[$key] = $this->data[$key] ?? []; $bag->initialize($this->data[$key]); } $this->started = true; $this->closed = false; } } http-foundation/Session/SessionBagInterface.php 0000644 00000001456 15025017654 0015714 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session; /** * Session Bag store. * * @author Drak <drak@zikula.org> */ interface SessionBagInterface { /** * Gets this bag's name. */ public function getName(): string; /** * Initializes the Bag. */ public function initialize(array &$array); /** * Gets the storage key for this bag. */ public function getStorageKey(): string; /** * Clears out data from bag. * * @return mixed Whatever data was contained */ public function clear(): mixed; } http-foundation/Session/Flash/FlashBagInterface.php 0000644 00000003227 15025017654 0016361 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Flash; use Symfony\Component\HttpFoundation\Session\SessionBagInterface; /** * FlashBagInterface. * * @author Drak <drak@zikula.org> */ interface FlashBagInterface extends SessionBagInterface { /** * Adds a flash message for the given type. */ public function add(string $type, mixed $message); /** * Registers one or more messages for a given type. */ public function set(string $type, string|array $messages); /** * Gets flash messages for a given type. * * @param string $type Message category type * @param array $default Default value if $type does not exist */ public function peek(string $type, array $default = []): array; /** * Gets all flash messages. */ public function peekAll(): array; /** * Gets and clears flash from the stack. * * @param array $default Default value if $type does not exist */ public function get(string $type, array $default = []): array; /** * Gets and clears flashes from the stack. */ public function all(): array; /** * Sets all flash messages. */ public function setAll(array $messages); /** * Has flash messages for a given type? */ public function has(string $type): bool; /** * Returns a list of all defined types. */ public function keys(): array; } http-foundation/Session/Flash/AutoExpireFlashBag.php 0000644 00000006754 15025017654 0016556 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Flash; /** * AutoExpireFlashBag flash message container. * * @author Drak <drak@zikula.org> */ class AutoExpireFlashBag implements FlashBagInterface { private string $name = 'flashes'; private array $flashes = ['display' => [], 'new' => []]; private string $storageKey; /** * @param string $storageKey The key used to store flashes in the session */ public function __construct(string $storageKey = '_symfony_flashes') { $this->storageKey = $storageKey; } /** * {@inheritdoc} */ public function getName(): string { return $this->name; } public function setName(string $name) { $this->name = $name; } /** * {@inheritdoc} */ public function initialize(array &$flashes) { $this->flashes = &$flashes; // The logic: messages from the last request will be stored in new, so we move them to previous // This request we will show what is in 'display'. What is placed into 'new' this time round will // be moved to display next time round. $this->flashes['display'] = \array_key_exists('new', $this->flashes) ? $this->flashes['new'] : []; $this->flashes['new'] = []; } /** * {@inheritdoc} */ public function add(string $type, mixed $message) { $this->flashes['new'][$type][] = $message; } /** * {@inheritdoc} */ public function peek(string $type, array $default = []): array { return $this->has($type) ? $this->flashes['display'][$type] : $default; } /** * {@inheritdoc} */ public function peekAll(): array { return \array_key_exists('display', $this->flashes) ? $this->flashes['display'] : []; } /** * {@inheritdoc} */ public function get(string $type, array $default = []): array { $return = $default; if (!$this->has($type)) { return $return; } if (isset($this->flashes['display'][$type])) { $return = $this->flashes['display'][$type]; unset($this->flashes['display'][$type]); } return $return; } /** * {@inheritdoc} */ public function all(): array { $return = $this->flashes['display']; $this->flashes['display'] = []; return $return; } /** * {@inheritdoc} */ public function setAll(array $messages) { $this->flashes['new'] = $messages; } /** * {@inheritdoc} */ public function set(string $type, string|array $messages) { $this->flashes['new'][$type] = (array) $messages; } /** * {@inheritdoc} */ public function has(string $type): bool { return \array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type]; } /** * {@inheritdoc} */ public function keys(): array { return array_keys($this->flashes['display']); } /** * {@inheritdoc} */ public function getStorageKey(): string { return $this->storageKey; } /** * {@inheritdoc} */ public function clear(): mixed { return $this->all(); } } http-foundation/Session/Flash/FlashBag.php 0000644 00000005433 15025017654 0014541 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Flash; /** * FlashBag flash message container. * * @author Drak <drak@zikula.org> */ class FlashBag implements FlashBagInterface { private string $name = 'flashes'; private array $flashes = []; private string $storageKey; /** * @param string $storageKey The key used to store flashes in the session */ public function __construct(string $storageKey = '_symfony_flashes') { $this->storageKey = $storageKey; } /** * {@inheritdoc} */ public function getName(): string { return $this->name; } public function setName(string $name) { $this->name = $name; } /** * {@inheritdoc} */ public function initialize(array &$flashes) { $this->flashes = &$flashes; } /** * {@inheritdoc} */ public function add(string $type, mixed $message) { $this->flashes[$type][] = $message; } /** * {@inheritdoc} */ public function peek(string $type, array $default = []): array { return $this->has($type) ? $this->flashes[$type] : $default; } /** * {@inheritdoc} */ public function peekAll(): array { return $this->flashes; } /** * {@inheritdoc} */ public function get(string $type, array $default = []): array { if (!$this->has($type)) { return $default; } $return = $this->flashes[$type]; unset($this->flashes[$type]); return $return; } /** * {@inheritdoc} */ public function all(): array { $return = $this->peekAll(); $this->flashes = []; return $return; } /** * {@inheritdoc} */ public function set(string $type, string|array $messages) { $this->flashes[$type] = (array) $messages; } /** * {@inheritdoc} */ public function setAll(array $messages) { $this->flashes = $messages; } /** * {@inheritdoc} */ public function has(string $type): bool { return \array_key_exists($type, $this->flashes) && $this->flashes[$type]; } /** * {@inheritdoc} */ public function keys(): array { return array_keys($this->flashes); } /** * {@inheritdoc} */ public function getStorageKey(): string { return $this->storageKey; } /** * {@inheritdoc} */ public function clear(): mixed { return $this->all(); } } http-foundation/Session/Attribute/AttributeBag.php 0000644 00000005715 15025017654 0016360 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Attribute; /** * This class relates to session attribute storage. * * @implements \IteratorAggregate<string, mixed> */ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable { private string $name = 'attributes'; private string $storageKey; protected $attributes = []; /** * @param string $storageKey The key used to store attributes in the session */ public function __construct(string $storageKey = '_sf2_attributes') { $this->storageKey = $storageKey; } /** * {@inheritdoc} */ public function getName(): string { return $this->name; } public function setName(string $name) { $this->name = $name; } /** * {@inheritdoc} */ public function initialize(array &$attributes) { $this->attributes = &$attributes; } /** * {@inheritdoc} */ public function getStorageKey(): string { return $this->storageKey; } /** * {@inheritdoc} */ public function has(string $name): bool { return \array_key_exists($name, $this->attributes); } /** * {@inheritdoc} */ public function get(string $name, mixed $default = null): mixed { return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; } /** * {@inheritdoc} */ public function set(string $name, mixed $value) { $this->attributes[$name] = $value; } /** * {@inheritdoc} */ public function all(): array { return $this->attributes; } /** * {@inheritdoc} */ public function replace(array $attributes) { $this->attributes = []; foreach ($attributes as $key => $value) { $this->set($key, $value); } } /** * {@inheritdoc} */ public function remove(string $name): mixed { $retval = null; if (\array_key_exists($name, $this->attributes)) { $retval = $this->attributes[$name]; unset($this->attributes[$name]); } return $retval; } /** * {@inheritdoc} */ public function clear(): mixed { $return = $this->attributes; $this->attributes = []; return $return; } /** * Returns an iterator for attributes. * * @return \ArrayIterator<string, mixed> */ public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->attributes); } /** * Returns the number of attributes. */ public function count(): int { return \count($this->attributes); } } http-foundation/Session/Attribute/AttributeBagInterface.php 0000644 00000002202 15025017654 0020165 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session\Attribute; use Symfony\Component\HttpFoundation\Session\SessionBagInterface; /** * Attributes store. * * @author Drak <drak@zikula.org> */ interface AttributeBagInterface extends SessionBagInterface { /** * Checks if an attribute is defined. */ public function has(string $name): bool; /** * Returns an attribute. */ public function get(string $name, mixed $default = null): mixed; /** * Sets an attribute. */ public function set(string $name, mixed $value); /** * Returns attributes. * * @return array<string, mixed> */ public function all(): array; public function replace(array $attributes); /** * Removes an attribute. * * @return mixed The removed value or null when it does not exist */ public function remove(string $name): mixed; } http-foundation/Session/SessionFactory.php 0000644 00000002471 15025017654 0015007 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface; // Help opcache.preload discover always-needed symbols class_exists(Session::class); /** * @author Jérémy Derussé <jeremy@derusse.com> */ class SessionFactory implements SessionFactoryInterface { private $requestStack; private $storageFactory; private ?\Closure $usageReporter; public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, callable $usageReporter = null) { $this->requestStack = $requestStack; $this->storageFactory = $storageFactory; $this->usageReporter = $usageReporter instanceof \Closure || !\is_callable($usageReporter) ? $usageReporter : \Closure::fromCallable($usageReporter); } public function createSession(): SessionInterface { return new Session($this->storageFactory->createStorage($this->requestStack->getMainRequest()), null, null, $this->usageReporter); } } http-foundation/Session/SessionInterface.php 0000644 00000007003 15025017654 0015274 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session; use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; /** * Interface for the session. * * @author Drak <drak@zikula.org> */ interface SessionInterface { /** * Starts the session storage. * * @throws \RuntimeException if session fails to start */ public function start(): bool; /** * Returns the session ID. */ public function getId(): string; /** * Sets the session ID. */ public function setId(string $id); /** * Returns the session name. */ public function getName(): string; /** * Sets the session name. */ public function setName(string $name); /** * Invalidates the current session. * * Clears all session attributes and flashes and regenerates the * session and deletes the old session from persistence. * * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value * will leave the system settings unchanged, 0 sets the cookie * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. */ public function invalidate(int $lifetime = null): bool; /** * Migrates the current session to a new session id while maintaining all * session attributes. * * @param bool $destroy Whether to delete the old session or leave it to garbage collection * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value * will leave the system settings unchanged, 0 sets the cookie * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. */ public function migrate(bool $destroy = false, int $lifetime = null): bool; /** * Force the session to be saved and closed. * * This method is generally not required for real sessions as * the session will be automatically saved at the end of * code execution. */ public function save(); /** * Checks if an attribute is defined. */ public function has(string $name): bool; /** * Returns an attribute. */ public function get(string $name, mixed $default = null): mixed; /** * Sets an attribute. */ public function set(string $name, mixed $value); /** * Returns attributes. */ public function all(): array; /** * Sets attributes. */ public function replace(array $attributes); /** * Removes an attribute. * * @return mixed The removed value or null when it does not exist */ public function remove(string $name): mixed; /** * Clears all attributes. */ public function clear(); /** * Checks if the session was started. */ public function isStarted(): bool; /** * Registers a SessionBagInterface with the session. */ public function registerBag(SessionBagInterface $bag); /** * Gets a bag instance by name. */ public function getBag(string $name): SessionBagInterface; /** * Gets session meta. */ public function getMetadataBag(): MetadataBag; } http-foundation/Session/Session.php 0000644 00000014577 15025017654 0013471 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; // Help opcache.preload discover always-needed symbols class_exists(AttributeBag::class); class_exists(FlashBag::class); class_exists(SessionBagProxy::class); /** * @author Fabien Potencier <fabien@symfony.com> * @author Drak <drak@zikula.org> * * @implements \IteratorAggregate<string, mixed> */ class Session implements SessionInterface, \IteratorAggregate, \Countable { protected $storage; private string $flashName; private string $attributeName; private array $data = []; private int $usageIndex = 0; private ?\Closure $usageReporter; public function __construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null, callable $usageReporter = null) { $this->storage = $storage ?? new NativeSessionStorage(); $this->usageReporter = $usageReporter instanceof \Closure || !\is_callable($usageReporter) ? $usageReporter : \Closure::fromCallable($usageReporter); $attributes = $attributes ?? new AttributeBag(); $this->attributeName = $attributes->getName(); $this->registerBag($attributes); $flashes = $flashes ?? new FlashBag(); $this->flashName = $flashes->getName(); $this->registerBag($flashes); } /** * {@inheritdoc} */ public function start(): bool { return $this->storage->start(); } /** * {@inheritdoc} */ public function has(string $name): bool { return $this->getAttributeBag()->has($name); } /** * {@inheritdoc} */ public function get(string $name, mixed $default = null): mixed { return $this->getAttributeBag()->get($name, $default); } /** * {@inheritdoc} */ public function set(string $name, mixed $value) { $this->getAttributeBag()->set($name, $value); } /** * {@inheritdoc} */ public function all(): array { return $this->getAttributeBag()->all(); } /** * {@inheritdoc} */ public function replace(array $attributes) { $this->getAttributeBag()->replace($attributes); } /** * {@inheritdoc} */ public function remove(string $name): mixed { return $this->getAttributeBag()->remove($name); } /** * {@inheritdoc} */ public function clear() { $this->getAttributeBag()->clear(); } /** * {@inheritdoc} */ public function isStarted(): bool { return $this->storage->isStarted(); } /** * Returns an iterator for attributes. * * @return \ArrayIterator<string, mixed> */ public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->getAttributeBag()->all()); } /** * Returns the number of attributes. */ public function count(): int { return \count($this->getAttributeBag()->all()); } public function &getUsageIndex(): int { return $this->usageIndex; } /** * @internal */ public function isEmpty(): bool { if ($this->isStarted()) { ++$this->usageIndex; if ($this->usageReporter && 0 <= $this->usageIndex) { ($this->usageReporter)(); } } foreach ($this->data as &$data) { if (!empty($data)) { return false; } } return true; } /** * {@inheritdoc} */ public function invalidate(int $lifetime = null): bool { $this->storage->clear(); return $this->migrate(true, $lifetime); } /** * {@inheritdoc} */ public function migrate(bool $destroy = false, int $lifetime = null): bool { return $this->storage->regenerate($destroy, $lifetime); } /** * {@inheritdoc} */ public function save() { $this->storage->save(); } /** * {@inheritdoc} */ public function getId(): string { return $this->storage->getId(); } /** * {@inheritdoc} */ public function setId(string $id) { if ($this->storage->getId() !== $id) { $this->storage->setId($id); } } /** * {@inheritdoc} */ public function getName(): string { return $this->storage->getName(); } /** * {@inheritdoc} */ public function setName(string $name) { $this->storage->setName($name); } /** * {@inheritdoc} */ public function getMetadataBag(): MetadataBag { ++$this->usageIndex; if ($this->usageReporter && 0 <= $this->usageIndex) { ($this->usageReporter)(); } return $this->storage->getMetadataBag(); } /** * {@inheritdoc} */ public function registerBag(SessionBagInterface $bag) { $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter)); } /** * {@inheritdoc} */ public function getBag(string $name): SessionBagInterface { $bag = $this->storage->getBag($name); return method_exists($bag, 'getBag') ? $bag->getBag() : $bag; } /** * Gets the flashbag interface. */ public function getFlashBag(): FlashBagInterface { return $this->getBag($this->flashName); } /** * Gets the attributebag interface. * * Note that this method was added to help with IDE autocompletion. */ private function getAttributeBag(): AttributeBagInterface { return $this->getBag($this->attributeName); } } http-foundation/Session/SessionUtils.php 0000644 00000003135 15025017654 0014476 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Session; /** * Session utility functions. * * @author Nicolas Grekas <p@tchwork.com> * @author Rémon van de Kamp <rpkamp@gmail.com> * * @internal */ final class SessionUtils { /** * Finds the session header amongst the headers that are to be sent, removes it, and returns * it so the caller can process it further. */ public static function popSessionCookie(string $sessionName, string $sessionId): ?string { $sessionCookie = null; $sessionCookiePrefix = sprintf(' %s=', urlencode($sessionName)); $sessionCookieWithId = sprintf('%s%s;', $sessionCookiePrefix, urlencode($sessionId)); $otherCookies = []; foreach (headers_list() as $h) { if (0 !== stripos($h, 'Set-Cookie:')) { continue; } if (11 === strpos($h, $sessionCookiePrefix, 11)) { $sessionCookie = $h; if (11 !== strpos($h, $sessionCookieWithId, 11)) { $otherCookies[] = $h; } } else { $otherCookies[] = $h; } } if (null === $sessionCookie) { return null; } header_remove('Set-Cookie'); foreach ($otherCookies as $h) { header($h, false); } return $sessionCookie; } } http-foundation/Response.php 0000644 00000111043 15025017654 0012203 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; // Help opcache.preload discover always-needed symbols class_exists(ResponseHeaderBag::class); /** * Response represents an HTTP response. * * @author Fabien Potencier <fabien@symfony.com> */ class Response { public const HTTP_CONTINUE = 100; public const HTTP_SWITCHING_PROTOCOLS = 101; public const HTTP_PROCESSING = 102; // RFC2518 public const HTTP_EARLY_HINTS = 103; // RFC8297 public const HTTP_OK = 200; public const HTTP_CREATED = 201; public const HTTP_ACCEPTED = 202; public const HTTP_NON_AUTHORITATIVE_INFORMATION = 203; public const HTTP_NO_CONTENT = 204; public const HTTP_RESET_CONTENT = 205; public const HTTP_PARTIAL_CONTENT = 206; public const HTTP_MULTI_STATUS = 207; // RFC4918 public const HTTP_ALREADY_REPORTED = 208; // RFC5842 public const HTTP_IM_USED = 226; // RFC3229 public const HTTP_MULTIPLE_CHOICES = 300; public const HTTP_MOVED_PERMANENTLY = 301; public const HTTP_FOUND = 302; public const HTTP_SEE_OTHER = 303; public const HTTP_NOT_MODIFIED = 304; public const HTTP_USE_PROXY = 305; public const HTTP_RESERVED = 306; public const HTTP_TEMPORARY_REDIRECT = 307; public const HTTP_PERMANENTLY_REDIRECT = 308; // RFC7238 public const HTTP_BAD_REQUEST = 400; public const HTTP_UNAUTHORIZED = 401; public const HTTP_PAYMENT_REQUIRED = 402; public const HTTP_FORBIDDEN = 403; public const HTTP_NOT_FOUND = 404; public const HTTP_METHOD_NOT_ALLOWED = 405; public const HTTP_NOT_ACCEPTABLE = 406; public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407; public const HTTP_REQUEST_TIMEOUT = 408; public const HTTP_CONFLICT = 409; public const HTTP_GONE = 410; public const HTTP_LENGTH_REQUIRED = 411; public const HTTP_PRECONDITION_FAILED = 412; public const HTTP_REQUEST_ENTITY_TOO_LARGE = 413; public const HTTP_REQUEST_URI_TOO_LONG = 414; public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415; public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; public const HTTP_EXPECTATION_FAILED = 417; public const HTTP_I_AM_A_TEAPOT = 418; // RFC2324 public const HTTP_MISDIRECTED_REQUEST = 421; // RFC7540 public const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918 public const HTTP_LOCKED = 423; // RFC4918 public const HTTP_FAILED_DEPENDENCY = 424; // RFC4918 public const HTTP_TOO_EARLY = 425; // RFC-ietf-httpbis-replay-04 public const HTTP_UPGRADE_REQUIRED = 426; // RFC2817 public const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585 public const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585 public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585 public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451; // RFC7725 public const HTTP_INTERNAL_SERVER_ERROR = 500; public const HTTP_NOT_IMPLEMENTED = 501; public const HTTP_BAD_GATEWAY = 502; public const HTTP_SERVICE_UNAVAILABLE = 503; public const HTTP_GATEWAY_TIMEOUT = 504; public const HTTP_VERSION_NOT_SUPPORTED = 505; public const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295 public const HTTP_INSUFFICIENT_STORAGE = 507; // RFC4918 public const HTTP_LOOP_DETECTED = 508; // RFC5842 public const HTTP_NOT_EXTENDED = 510; // RFC2774 public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585 /** * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control */ private const HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES = [ 'must_revalidate' => false, 'no_cache' => false, 'no_store' => false, 'no_transform' => false, 'public' => false, 'private' => false, 'proxy_revalidate' => false, 'max_age' => true, 's_maxage' => true, 'immutable' => false, 'last_modified' => true, 'etag' => true, ]; /** * @var ResponseHeaderBag */ public $headers; /** * @var string */ protected $content; /** * @var string */ protected $version; /** * @var int */ protected $statusCode; /** * @var string */ protected $statusText; /** * @var string */ protected $charset; /** * Status codes translation table. * * The list of codes is complete according to the * {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry} * (last updated 2021-10-01). * * Unless otherwise noted, the status code is defined in RFC2616. * * @var array */ public static $statusTexts = [ 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', // RFC2518 103 => 'Early Hints', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', // RFC4918 208 => 'Already Reported', // RFC5842 226 => 'IM Used', // RFC3229 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', // RFC7238 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Content Too Large', // RFC-ietf-httpbis-semantics 414 => 'URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Range Not Satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', // RFC2324 421 => 'Misdirected Request', // RFC7540 422 => 'Unprocessable Content', // RFC-ietf-httpbis-semantics 423 => 'Locked', // RFC4918 424 => 'Failed Dependency', // RFC4918 425 => 'Too Early', // RFC-ietf-httpbis-replay-04 426 => 'Upgrade Required', // RFC2817 428 => 'Precondition Required', // RFC6585 429 => 'Too Many Requests', // RFC6585 431 => 'Request Header Fields Too Large', // RFC6585 451 => 'Unavailable For Legal Reasons', // RFC7725 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', 506 => 'Variant Also Negotiates', // RFC2295 507 => 'Insufficient Storage', // RFC4918 508 => 'Loop Detected', // RFC5842 510 => 'Not Extended', // RFC2774 511 => 'Network Authentication Required', // RFC6585 ]; /** * @throws \InvalidArgumentException When the HTTP status code is not valid */ public function __construct(?string $content = '', int $status = 200, array $headers = []) { $this->headers = new ResponseHeaderBag($headers); $this->setContent($content); $this->setStatusCode($status); $this->setProtocolVersion('1.0'); } /** * Returns the Response as an HTTP string. * * The string representation of the Response is the same as the * one that will be sent to the client only if the prepare() method * has been called before. * * @see prepare() */ public function __toString(): string { return sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n". $this->headers."\r\n". $this->getContent(); } /** * Clones the current Response instance. */ public function __clone() { $this->headers = clone $this->headers; } /** * Prepares the Response before it is sent to the client. * * This method tweaks the Response to ensure that it is * compliant with RFC 2616. Most of the changes are based on * the Request that is "associated" with this Response. * * @return $this */ public function prepare(Request $request): static { $headers = $this->headers; if ($this->isInformational() || $this->isEmpty()) { $this->setContent(null); $headers->remove('Content-Type'); $headers->remove('Content-Length'); // prevent PHP from sending the Content-Type header based on default_mimetype ini_set('default_mimetype', ''); } else { // Content-type based on the Request if (!$headers->has('Content-Type')) { $format = $request->getRequestFormat(null); if (null !== $format && $mimeType = $request->getMimeType($format)) { $headers->set('Content-Type', $mimeType); } } // Fix Content-Type $charset = $this->charset ?: 'UTF-8'; if (!$headers->has('Content-Type')) { $headers->set('Content-Type', 'text/html; charset='.$charset); } elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) { // add the charset $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset); } // Fix Content-Length if ($headers->has('Transfer-Encoding')) { $headers->remove('Content-Length'); } if ($request->isMethod('HEAD')) { // cf. RFC2616 14.13 $length = $headers->get('Content-Length'); $this->setContent(null); if ($length) { $headers->set('Content-Length', $length); } } } // Fix protocol if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) { $this->setProtocolVersion('1.1'); } // Check if we need to send extra expire info headers if ('1.0' == $this->getProtocolVersion() && str_contains($headers->get('Cache-Control', ''), 'no-cache')) { $headers->set('pragma', 'no-cache'); $headers->set('expires', -1); } $this->ensureIEOverSSLCompatibility($request); if ($request->isSecure()) { foreach ($headers->getCookies() as $cookie) { $cookie->setSecureDefault(true); } } return $this; } /** * Sends HTTP headers. * * @return $this */ public function sendHeaders(): static { // headers have already been sent by the developer if (headers_sent()) { return $this; } // headers foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { $replace = 0 === strcasecmp($name, 'Content-Type'); foreach ($values as $value) { header($name.': '.$value, $replace, $this->statusCode); } } // cookies foreach ($this->headers->getCookies() as $cookie) { header('Set-Cookie: '.$cookie, false, $this->statusCode); } // status header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); return $this; } /** * Sends content for the current web response. * * @return $this */ public function sendContent(): static { echo $this->content; return $this; } /** * Sends HTTP headers and content. * * @return $this */ public function send(): static { $this->sendHeaders(); $this->sendContent(); if (\function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } elseif (\function_exists('litespeed_finish_request')) { litespeed_finish_request(); } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { static::closeOutputBuffers(0, true); flush(); } return $this; } /** * Sets the response content. * * @return $this */ public function setContent(?string $content): static { $this->content = $content ?? ''; return $this; } /** * Gets the current response content. */ public function getContent(): string|false { return $this->content; } /** * Sets the HTTP protocol version (1.0 or 1.1). * * @return $this * * @final */ public function setProtocolVersion(string $version): static { $this->version = $version; return $this; } /** * Gets the HTTP protocol version. * * @final */ public function getProtocolVersion(): string { return $this->version; } /** * Sets the response status code. * * If the status text is null it will be automatically populated for the known * status codes and left empty otherwise. * * @return $this * * @throws \InvalidArgumentException When the HTTP status code is not valid * * @final */ public function setStatusCode(int $code, string $text = null): static { $this->statusCode = $code; if ($this->isInvalid()) { throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code)); } if (null === $text) { $this->statusText = self::$statusTexts[$code] ?? 'unknown status'; return $this; } if (false === $text) { $this->statusText = ''; return $this; } $this->statusText = $text; return $this; } /** * Retrieves the status code for the current web response. * * @final */ public function getStatusCode(): int { return $this->statusCode; } /** * Sets the response charset. * * @return $this * * @final */ public function setCharset(string $charset): static { $this->charset = $charset; return $this; } /** * Retrieves the response charset. * * @final */ public function getCharset(): ?string { return $this->charset; } /** * Returns true if the response may safely be kept in a shared (surrogate) cache. * * Responses marked "private" with an explicit Cache-Control directive are * considered uncacheable. * * Responses with neither a freshness lifetime (Expires, max-age) nor cache * validator (Last-Modified, ETag) are considered uncacheable because there is * no way to tell when or how to remove them from the cache. * * Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation, * for example "status codes that are defined as cacheable by default [...] * can be reused by a cache with heuristic expiration unless otherwise indicated" * (https://tools.ietf.org/html/rfc7231#section-6.1) * * @final */ public function isCacheable(): bool { if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410])) { return false; } if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) { return false; } return $this->isValidateable() || $this->isFresh(); } /** * Returns true if the response is "fresh". * * Fresh responses may be served from cache without any interaction with the * origin. A response is considered fresh when it includes a Cache-Control/max-age * indicator or Expires header and the calculated age is less than the freshness lifetime. * * @final */ public function isFresh(): bool { return $this->getTtl() > 0; } /** * Returns true if the response includes headers that can be used to validate * the response with the origin server using a conditional GET request. * * @final */ public function isValidateable(): bool { return $this->headers->has('Last-Modified') || $this->headers->has('ETag'); } /** * Marks the response as "private". * * It makes the response ineligible for serving other clients. * * @return $this * * @final */ public function setPrivate(): static { $this->headers->removeCacheControlDirective('public'); $this->headers->addCacheControlDirective('private'); return $this; } /** * Marks the response as "public". * * It makes the response eligible for serving other clients. * * @return $this * * @final */ public function setPublic(): static { $this->headers->addCacheControlDirective('public'); $this->headers->removeCacheControlDirective('private'); return $this; } /** * Marks the response as "immutable". * * @return $this * * @final */ public function setImmutable(bool $immutable = true): static { if ($immutable) { $this->headers->addCacheControlDirective('immutable'); } else { $this->headers->removeCacheControlDirective('immutable'); } return $this; } /** * Returns true if the response is marked as "immutable". * * @final */ public function isImmutable(): bool { return $this->headers->hasCacheControlDirective('immutable'); } /** * Returns true if the response must be revalidated by shared caches once it has become stale. * * This method indicates that the response must not be served stale by a * cache in any circumstance without first revalidating with the origin. * When present, the TTL of the response should not be overridden to be * greater than the value provided by the origin. * * @final */ public function mustRevalidate(): bool { return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->hasCacheControlDirective('proxy-revalidate'); } /** * Returns the Date header as a DateTime instance. * * @throws \RuntimeException When the header is not parseable * * @final */ public function getDate(): ?\DateTimeInterface { return $this->headers->getDate('Date'); } /** * Sets the Date header. * * @return $this * * @final */ public function setDate(\DateTimeInterface $date): static { if ($date instanceof \DateTime) { $date = \DateTimeImmutable::createFromMutable($date); } $date = $date->setTimezone(new \DateTimeZone('UTC')); $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT'); return $this; } /** * Returns the age of the response in seconds. * * @final */ public function getAge(): int { if (null !== $age = $this->headers->get('Age')) { return (int) $age; } return max(time() - (int) $this->getDate()->format('U'), 0); } /** * Marks the response stale by setting the Age header to be equal to the maximum age of the response. * * @return $this */ public function expire(): static { if ($this->isFresh()) { $this->headers->set('Age', $this->getMaxAge()); $this->headers->remove('Expires'); } return $this; } /** * Returns the value of the Expires header as a DateTime instance. * * @final */ public function getExpires(): ?\DateTimeInterface { try { return $this->headers->getDate('Expires'); } catch (\RuntimeException $e) { // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past return \DateTime::createFromFormat('U', time() - 172800); } } /** * Sets the Expires HTTP header with a DateTime instance. * * Passing null as value will remove the header. * * @return $this * * @final */ public function setExpires(\DateTimeInterface $date = null): static { if (null === $date) { $this->headers->remove('Expires'); return $this; } if ($date instanceof \DateTime) { $date = \DateTimeImmutable::createFromMutable($date); } $date = $date->setTimezone(new \DateTimeZone('UTC')); $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT'); return $this; } /** * Returns the number of seconds after the time specified in the response's Date * header when the response should no longer be considered fresh. * * First, it checks for a s-maxage directive, then a max-age directive, and then it falls * back on an expires header. It returns null when no maximum age can be established. * * @final */ public function getMaxAge(): ?int { if ($this->headers->hasCacheControlDirective('s-maxage')) { return (int) $this->headers->getCacheControlDirective('s-maxage'); } if ($this->headers->hasCacheControlDirective('max-age')) { return (int) $this->headers->getCacheControlDirective('max-age'); } if (null !== $this->getExpires()) { return (int) $this->getExpires()->format('U') - (int) $this->getDate()->format('U'); } return null; } /** * Sets the number of seconds after which the response should no longer be considered fresh. * * This methods sets the Cache-Control max-age directive. * * @return $this * * @final */ public function setMaxAge(int $value): static { $this->headers->addCacheControlDirective('max-age', $value); return $this; } /** * Sets the number of seconds after which the response should no longer be considered fresh by shared caches. * * This methods sets the Cache-Control s-maxage directive. * * @return $this * * @final */ public function setSharedMaxAge(int $value): static { $this->setPublic(); $this->headers->addCacheControlDirective('s-maxage', $value); return $this; } /** * Returns the response's time-to-live in seconds. * * It returns null when no freshness information is present in the response. * * When the responses TTL is <= 0, the response may not be served from cache without first * revalidating with the origin. * * @final */ public function getTtl(): ?int { $maxAge = $this->getMaxAge(); return null !== $maxAge ? $maxAge - $this->getAge() : null; } /** * Sets the response's time-to-live for shared caches in seconds. * * This method adjusts the Cache-Control/s-maxage directive. * * @return $this * * @final */ public function setTtl(int $seconds): static { $this->setSharedMaxAge($this->getAge() + $seconds); return $this; } /** * Sets the response's time-to-live for private/client caches in seconds. * * This method adjusts the Cache-Control/max-age directive. * * @return $this * * @final */ public function setClientTtl(int $seconds): static { $this->setMaxAge($this->getAge() + $seconds); return $this; } /** * Returns the Last-Modified HTTP header as a DateTime instance. * * @throws \RuntimeException When the HTTP header is not parseable * * @final */ public function getLastModified(): ?\DateTimeInterface { return $this->headers->getDate('Last-Modified'); } /** * Sets the Last-Modified HTTP header with a DateTime instance. * * Passing null as value will remove the header. * * @return $this * * @final */ public function setLastModified(\DateTimeInterface $date = null): static { if (null === $date) { $this->headers->remove('Last-Modified'); return $this; } if ($date instanceof \DateTime) { $date = \DateTimeImmutable::createFromMutable($date); } $date = $date->setTimezone(new \DateTimeZone('UTC')); $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT'); return $this; } /** * Returns the literal value of the ETag HTTP header. * * @final */ public function getEtag(): ?string { return $this->headers->get('ETag'); } /** * Sets the ETag value. * * @param string|null $etag The ETag unique identifier or null to remove the header * @param bool $weak Whether you want a weak ETag or not * * @return $this * * @final */ public function setEtag(string $etag = null, bool $weak = false): static { if (null === $etag) { $this->headers->remove('Etag'); } else { if (!str_starts_with($etag, '"')) { $etag = '"'.$etag.'"'; } $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag); } return $this; } /** * Sets the response's cache headers (validation and/or expiration). * * Available options are: must_revalidate, no_cache, no_store, no_transform, public, private, proxy_revalidate, max_age, s_maxage, immutable, last_modified and etag. * * @return $this * * @throws \InvalidArgumentException * * @final */ public function setCache(array $options): static { if ($diff = array_diff(array_keys($options), array_keys(self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES))) { throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', $diff))); } if (isset($options['etag'])) { $this->setEtag($options['etag']); } if (isset($options['last_modified'])) { $this->setLastModified($options['last_modified']); } if (isset($options['max_age'])) { $this->setMaxAge($options['max_age']); } if (isset($options['s_maxage'])) { $this->setSharedMaxAge($options['s_maxage']); } foreach (self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES as $directive => $hasValue) { if (!$hasValue && isset($options[$directive])) { if ($options[$directive]) { $this->headers->addCacheControlDirective(str_replace('_', '-', $directive)); } else { $this->headers->removeCacheControlDirective(str_replace('_', '-', $directive)); } } } if (isset($options['public'])) { if ($options['public']) { $this->setPublic(); } else { $this->setPrivate(); } } if (isset($options['private'])) { if ($options['private']) { $this->setPrivate(); } else { $this->setPublic(); } } return $this; } /** * Modifies the response so that it conforms to the rules defined for a 304 status code. * * This sets the status, removes the body, and discards any headers * that MUST NOT be included in 304 responses. * * @return $this * * @see https://tools.ietf.org/html/rfc2616#section-10.3.5 * * @final */ public function setNotModified(): static { $this->setStatusCode(304); $this->setContent(null); // remove headers that MUST NOT be included with 304 Not Modified responses foreach (['Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified'] as $header) { $this->headers->remove($header); } return $this; } /** * Returns true if the response includes a Vary header. * * @final */ public function hasVary(): bool { return null !== $this->headers->get('Vary'); } /** * Returns an array of header names given in the Vary header. * * @final */ public function getVary(): array { if (!$vary = $this->headers->all('Vary')) { return []; } $ret = []; foreach ($vary as $item) { $ret[] = preg_split('/[\s,]+/', $item); } return array_merge([], ...$ret); } /** * Sets the Vary header. * * @param bool $replace Whether to replace the actual value or not (true by default) * * @return $this * * @final */ public function setVary(string|array $headers, bool $replace = true): static { $this->headers->set('Vary', $headers, $replace); return $this; } /** * Determines if the Response validators (ETag, Last-Modified) match * a conditional value specified in the Request. * * If the Response is not modified, it sets the status code to 304 and * removes the actual content by calling the setNotModified() method. * * @final */ public function isNotModified(Request $request): bool { if (!$request->isMethodCacheable()) { return false; } $notModified = false; $lastModified = $this->headers->get('Last-Modified'); $modifiedSince = $request->headers->get('If-Modified-Since'); if (($ifNoneMatchEtags = $request->getETags()) && (null !== $etag = $this->getEtag())) { if (0 == strncmp($etag, 'W/', 2)) { $etag = substr($etag, 2); } // Use weak comparison as per https://tools.ietf.org/html/rfc7232#section-3.2. foreach ($ifNoneMatchEtags as $ifNoneMatchEtag) { if (0 == strncmp($ifNoneMatchEtag, 'W/', 2)) { $ifNoneMatchEtag = substr($ifNoneMatchEtag, 2); } if ($ifNoneMatchEtag === $etag || '*' === $ifNoneMatchEtag) { $notModified = true; break; } } } // Only do If-Modified-Since date comparison when If-None-Match is not present as per https://tools.ietf.org/html/rfc7232#section-3.3. elseif ($modifiedSince && $lastModified) { $notModified = strtotime($modifiedSince) >= strtotime($lastModified); } if ($notModified) { $this->setNotModified(); } return $notModified; } /** * Is response invalid? * * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html * * @final */ public function isInvalid(): bool { return $this->statusCode < 100 || $this->statusCode >= 600; } /** * Is response informative? * * @final */ public function isInformational(): bool { return $this->statusCode >= 100 && $this->statusCode < 200; } /** * Is response successful? * * @final */ public function isSuccessful(): bool { return $this->statusCode >= 200 && $this->statusCode < 300; } /** * Is the response a redirect? * * @final */ public function isRedirection(): bool { return $this->statusCode >= 300 && $this->statusCode < 400; } /** * Is there a client error? * * @final */ public function isClientError(): bool { return $this->statusCode >= 400 && $this->statusCode < 500; } /** * Was there a server side error? * * @final */ public function isServerError(): bool { return $this->statusCode >= 500 && $this->statusCode < 600; } /** * Is the response OK? * * @final */ public function isOk(): bool { return 200 === $this->statusCode; } /** * Is the response forbidden? * * @final */ public function isForbidden(): bool { return 403 === $this->statusCode; } /** * Is the response a not found error? * * @final */ public function isNotFound(): bool { return 404 === $this->statusCode; } /** * Is the response a redirect of some form? * * @final */ public function isRedirect(string $location = null): bool { return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location')); } /** * Is the response empty? * * @final */ public function isEmpty(): bool { return \in_array($this->statusCode, [204, 304]); } /** * Cleans or flushes output buffers up to target level. * * Resulting level can be greater than target level if a non-removable buffer has been encountered. * * @final */ public static function closeOutputBuffers(int $targetLevel, bool $flush): void { $status = ob_get_status(true); $level = \count($status); $flags = \PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? \PHP_OUTPUT_HANDLER_FLUSHABLE : \PHP_OUTPUT_HANDLER_CLEANABLE); while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { if ($flush) { ob_end_flush(); } else { ob_end_clean(); } } } /** * Marks a response as safe according to RFC8674. * * @see https://tools.ietf.org/html/rfc8674 */ public function setContentSafe(bool $safe = true): void { if ($safe) { $this->headers->set('Preference-Applied', 'safe'); } elseif ('safe' === $this->headers->get('Preference-Applied')) { $this->headers->remove('Preference-Applied'); } $this->setVary('Prefer', false); } /** * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9. * * @see http://support.microsoft.com/kb/323308 * * @final */ protected function ensureIEOverSSLCompatibility(Request $request): void { if (false !== stripos($this->headers->get('Content-Disposition') ?? '', 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT') ?? '', $match) && true === $request->isSecure()) { if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) { $this->headers->remove('Cache-Control'); } } } } http-foundation/JsonResponse.php 0000644 00000014724 15025017654 0013045 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * Response represents an HTTP response in JSON format. * * Note that this class does not force the returned JSON content to be an * object. It is however recommended that you do return an object as it * protects yourself against XSSI and JSON-JavaScript Hijacking. * * @see https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/AJAX_Security_Cheat_Sheet.md#always-return-json-with-an-object-on-the-outside * * @author Igor Wiedler <igor@wiedler.ch> */ class JsonResponse extends Response { protected $data; protected $callback; // Encode <, >, ', &, and " characters in the JSON, making it also safe to be embedded into HTML. // 15 === JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT public const DEFAULT_ENCODING_OPTIONS = 15; protected $encodingOptions = self::DEFAULT_ENCODING_OPTIONS; /** * @param bool $json If the data is already a JSON string */ public function __construct(mixed $data = null, int $status = 200, array $headers = [], bool $json = false) { parent::__construct('', $status, $headers); if ($json && !\is_string($data) && !is_numeric($data) && !\is_callable([$data, '__toString'])) { throw new \TypeError(sprintf('"%s": If $json is set to true, argument $data must be a string or object implementing __toString(), "%s" given.', __METHOD__, get_debug_type($data))); } if (null === $data) { $data = new \ArrayObject(); } $json ? $this->setJson($data) : $this->setData($data); } /** * Factory method for chainability. * * Example: * * return JsonResponse::fromJsonString('{"key": "value"}') * ->setSharedMaxAge(300); * * @param string $data The JSON response string * @param int $status The response status code * @param array $headers An array of response headers */ public static function fromJsonString(string $data, int $status = 200, array $headers = []): static { return new static($data, $status, $headers, true); } /** * Sets the JSONP callback. * * @param string|null $callback The JSONP callback or null to use none * * @return $this * * @throws \InvalidArgumentException When the callback name is not valid */ public function setCallback(string $callback = null): static { if (null !== $callback) { // partially taken from https://geekality.net/2011/08/03/valid-javascript-identifier/ // partially taken from https://github.com/willdurand/JsonpCallbackValidator // JsonpCallbackValidator is released under the MIT License. See https://github.com/willdurand/JsonpCallbackValidator/blob/v1.1.0/LICENSE for details. // (c) William Durand <william.durand1@gmail.com> $pattern = '/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*(?:\[(?:"(?:\\\.|[^"\\\])*"|\'(?:\\\.|[^\'\\\])*\'|\d+)\])*?$/u'; $reserved = [ 'break', 'do', 'instanceof', 'typeof', 'case', 'else', 'new', 'var', 'catch', 'finally', 'return', 'void', 'continue', 'for', 'switch', 'while', 'debugger', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in', 'try', 'class', 'enum', 'extends', 'super', 'const', 'export', 'import', 'implements', 'let', 'private', 'public', 'yield', 'interface', 'package', 'protected', 'static', 'null', 'true', 'false', ]; $parts = explode('.', $callback); foreach ($parts as $part) { if (!preg_match($pattern, $part) || \in_array($part, $reserved, true)) { throw new \InvalidArgumentException('The callback name is not valid.'); } } } $this->callback = $callback; return $this->update(); } /** * Sets a raw string containing a JSON document to be sent. * * @return $this */ public function setJson(string $json): static { $this->data = $json; return $this->update(); } /** * Sets the data to be sent as JSON. * * @return $this * * @throws \InvalidArgumentException */ public function setData(mixed $data = []): static { try { $data = json_encode($data, $this->encodingOptions); } catch (\Exception $e) { if ('Exception' === \get_class($e) && str_starts_with($e->getMessage(), 'Failed calling ')) { throw $e->getPrevious() ?: $e; } throw $e; } if (\JSON_THROW_ON_ERROR & $this->encodingOptions) { return $this->setJson($data); } if (\JSON_ERROR_NONE !== json_last_error()) { throw new \InvalidArgumentException(json_last_error_msg()); } return $this->setJson($data); } /** * Returns options used while encoding data to JSON. */ public function getEncodingOptions(): int { return $this->encodingOptions; } /** * Sets options used while encoding data to JSON. * * @return $this */ public function setEncodingOptions(int $encodingOptions): static { $this->encodingOptions = $encodingOptions; return $this->setData(json_decode($this->data)); } /** * Updates the content and headers according to the JSON data and callback. * * @return $this */ protected function update(): static { if (null !== $this->callback) { // Not using application/javascript for compatibility reasons with older browsers. $this->headers->set('Content-Type', 'text/javascript'); return $this->setContent(sprintf('/**/%s(%s);', $this->callback, $this->data)); } // Only set the header when there is none or when it equals 'text/javascript' (from a previous update with callback) // in order to not overwrite a custom definition. if (!$this->headers->has('Content-Type') || 'text/javascript' === $this->headers->get('Content-Type')) { $this->headers->set('Content-Type', 'application/json'); } return $this->setContent($this->data); } } http-foundation/RateLimiter/RequestRateLimiterInterface.php 0000644 00000001336 15025017654 0020244 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\RateLimiter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\RateLimiter\RateLimit; /** * A special type of limiter that deals with requests. * * This allows to limit on different types of information * from the requests. * * @author Wouter de Jong <wouter@wouterj.nl> */ interface RequestRateLimiterInterface { public function consume(Request $request): RateLimit; public function reset(Request $request): void; } http-foundation/RateLimiter/AbstractRequestRateLimiter.php 0000644 00000004200 15025017654 0020100 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\RateLimiter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\RateLimiter\LimiterInterface; use Symfony\Component\RateLimiter\Policy\NoLimiter; use Symfony\Component\RateLimiter\RateLimit; /** * An implementation of RequestRateLimiterInterface that * fits most use-cases. * * @author Wouter de Jong <wouter@wouterj.nl> */ abstract class AbstractRequestRateLimiter implements RequestRateLimiterInterface { public function consume(Request $request): RateLimit { $limiters = $this->getLimiters($request); if (0 === \count($limiters)) { $limiters = [new NoLimiter()]; } $minimalRateLimit = null; foreach ($limiters as $limiter) { $rateLimit = $limiter->consume(1); $minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit; } return $minimalRateLimit; } public function reset(Request $request): void { foreach ($this->getLimiters($request) as $limiter) { $limiter->reset(); } } /** * @return LimiterInterface[] a set of limiters using keys extracted from the request */ abstract protected function getLimiters(Request $request): array; private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit { if ($first->isAccepted() !== $second->isAccepted()) { return $first->isAccepted() ? $second : $first; } $firstRemainingTokens = $first->getRemainingTokens(); $secondRemainingTokens = $second->getRemainingTokens(); if ($firstRemainingTokens === $secondRemainingTokens) { return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first; } return $firstRemainingTokens > $secondRemainingTokens ? $second : $first; } } http-foundation/ResponseHeaderBag.php 0000644 00000017443 15025017654 0013737 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * ResponseHeaderBag is a container for Response HTTP headers. * * @author Fabien Potencier <fabien@symfony.com> */ class ResponseHeaderBag extends HeaderBag { public const COOKIES_FLAT = 'flat'; public const COOKIES_ARRAY = 'array'; public const DISPOSITION_ATTACHMENT = 'attachment'; public const DISPOSITION_INLINE = 'inline'; protected $computedCacheControl = []; protected $cookies = []; protected $headerNames = []; public function __construct(array $headers = []) { parent::__construct($headers); if (!isset($this->headers['cache-control'])) { $this->set('Cache-Control', ''); } /* RFC2616 - 14.18 says all Responses need to have a Date */ if (!isset($this->headers['date'])) { $this->initDate(); } } /** * Returns the headers, with original capitalizations. */ public function allPreserveCase(): array { $headers = []; foreach ($this->all() as $name => $value) { $headers[$this->headerNames[$name] ?? $name] = $value; } return $headers; } public function allPreserveCaseWithoutCookies() { $headers = $this->allPreserveCase(); if (isset($this->headerNames['set-cookie'])) { unset($headers[$this->headerNames['set-cookie']]); } return $headers; } /** * {@inheritdoc} */ public function replace(array $headers = []) { $this->headerNames = []; parent::replace($headers); if (!isset($this->headers['cache-control'])) { $this->set('Cache-Control', ''); } if (!isset($this->headers['date'])) { $this->initDate(); } } /** * {@inheritdoc} */ public function all(string $key = null): array { $headers = parent::all(); if (null !== $key) { $key = strtr($key, self::UPPER, self::LOWER); return 'set-cookie' !== $key ? $headers[$key] ?? [] : array_map('strval', $this->getCookies()); } foreach ($this->getCookies() as $cookie) { $headers['set-cookie'][] = (string) $cookie; } return $headers; } /** * {@inheritdoc} */ public function set(string $key, string|array|null $values, bool $replace = true) { $uniqueKey = strtr($key, self::UPPER, self::LOWER); if ('set-cookie' === $uniqueKey) { if ($replace) { $this->cookies = []; } foreach ((array) $values as $cookie) { $this->setCookie(Cookie::fromString($cookie)); } $this->headerNames[$uniqueKey] = $key; return; } $this->headerNames[$uniqueKey] = $key; parent::set($key, $values, $replace); // ensure the cache-control header has sensible defaults if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true) && '' !== $computed = $this->computeCacheControlValue()) { $this->headers['cache-control'] = [$computed]; $this->headerNames['cache-control'] = 'Cache-Control'; $this->computedCacheControl = $this->parseCacheControl($computed); } } /** * {@inheritdoc} */ public function remove(string $key) { $uniqueKey = strtr($key, self::UPPER, self::LOWER); unset($this->headerNames[$uniqueKey]); if ('set-cookie' === $uniqueKey) { $this->cookies = []; return; } parent::remove($key); if ('cache-control' === $uniqueKey) { $this->computedCacheControl = []; } if ('date' === $uniqueKey) { $this->initDate(); } } /** * {@inheritdoc} */ public function hasCacheControlDirective(string $key): bool { return \array_key_exists($key, $this->computedCacheControl); } /** * {@inheritdoc} */ public function getCacheControlDirective(string $key): bool|string|null { return $this->computedCacheControl[$key] ?? null; } public function setCookie(Cookie $cookie) { $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; $this->headerNames['set-cookie'] = 'Set-Cookie'; } /** * Removes a cookie from the array, but does not unset it in the browser. */ public function removeCookie(string $name, ?string $path = '/', string $domain = null) { if (null === $path) { $path = '/'; } unset($this->cookies[$domain][$path][$name]); if (empty($this->cookies[$domain][$path])) { unset($this->cookies[$domain][$path]); if (empty($this->cookies[$domain])) { unset($this->cookies[$domain]); } } if (empty($this->cookies)) { unset($this->headerNames['set-cookie']); } } /** * Returns an array with all cookies. * * @return Cookie[] * * @throws \InvalidArgumentException When the $format is invalid */ public function getCookies(string $format = self::COOKIES_FLAT): array { if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY])) { throw new \InvalidArgumentException(sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY]))); } if (self::COOKIES_ARRAY === $format) { return $this->cookies; } $flattenedCookies = []; foreach ($this->cookies as $path) { foreach ($path as $cookies) { foreach ($cookies as $cookie) { $flattenedCookies[] = $cookie; } } } return $flattenedCookies; } /** * Clears a cookie in the browser. */ public function clearCookie(string $name, ?string $path = '/', string $domain = null, bool $secure = false, bool $httpOnly = true, string $sameSite = null) { $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite)); } /** * @see HeaderUtils::makeDisposition() */ public function makeDisposition(string $disposition, string $filename, string $filenameFallback = '') { return HeaderUtils::makeDisposition($disposition, $filename, $filenameFallback); } /** * Returns the calculated value of the cache-control header. * * This considers several other headers and calculates or modifies the * cache-control header to a sensible, conservative value. */ protected function computeCacheControlValue(): string { if (!$this->cacheControl) { if ($this->has('Last-Modified') || $this->has('Expires')) { return 'private, must-revalidate'; // allows for heuristic expiration (RFC 7234 Section 4.2.2) in the case of "Last-Modified" } // conservative by default return 'no-cache, private'; } $header = $this->getCacheControlHeader(); if (isset($this->cacheControl['public']) || isset($this->cacheControl['private'])) { return $header; } // public if s-maxage is defined, private otherwise if (!isset($this->cacheControl['s-maxage'])) { return $header.', private'; } return $header; } private function initDate(): void { $this->set('Date', gmdate('D, d M Y H:i:s').' GMT'); } } http-foundation/AcceptHeaderItem.php 0000644 00000006301 15025017654 0013534 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * Represents an Accept-* header item. * * @author Jean-François Simon <contact@jfsimon.fr> */ class AcceptHeaderItem { private string $value; private float $quality = 1.0; private int $index = 0; private array $attributes = []; public function __construct(string $value, array $attributes = []) { $this->value = $value; foreach ($attributes as $name => $value) { $this->setAttribute($name, $value); } } /** * Builds an AcceptHeaderInstance instance from a string. */ public static function fromString(?string $itemValue): self { $parts = HeaderUtils::split($itemValue ?? '', ';='); $part = array_shift($parts); $attributes = HeaderUtils::combine($parts); return new self($part[0], $attributes); } /** * Returns header value's string representation. */ public function __toString(): string { $string = $this->value.($this->quality < 1 ? ';q='.$this->quality : ''); if (\count($this->attributes) > 0) { $string .= '; '.HeaderUtils::toString($this->attributes, ';'); } return $string; } /** * Set the item value. * * @return $this */ public function setValue(string $value): static { $this->value = $value; return $this; } /** * Returns the item value. */ public function getValue(): string { return $this->value; } /** * Set the item quality. * * @return $this */ public function setQuality(float $quality): static { $this->quality = $quality; return $this; } /** * Returns the item quality. */ public function getQuality(): float { return $this->quality; } /** * Set the item index. * * @return $this */ public function setIndex(int $index): static { $this->index = $index; return $this; } /** * Returns the item index. */ public function getIndex(): int { return $this->index; } /** * Tests if an attribute exists. */ public function hasAttribute(string $name): bool { return isset($this->attributes[$name]); } /** * Returns an attribute by its name. */ public function getAttribute(string $name, mixed $default = null): mixed { return $this->attributes[$name] ?? $default; } /** * Returns all attributes. */ public function getAttributes(): array { return $this->attributes; } /** * Set an attribute. * * @return $this */ public function setAttribute(string $name, string $value): static { if ('q' === $name) { $this->quality = (float) $value; } else { $this->attributes[$name] = $value; } return $this; } } http-foundation/Request.php 0000644 00000200155 15025017654 0012040 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException; use Symfony\Component\HttpFoundation\Exception\JsonException; use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpFoundation\Session\SessionInterface; // Help opcache.preload discover always-needed symbols class_exists(AcceptHeader::class); class_exists(FileBag::class); class_exists(HeaderBag::class); class_exists(HeaderUtils::class); class_exists(InputBag::class); class_exists(ParameterBag::class); class_exists(ServerBag::class); /** * Request represents an HTTP request. * * The methods dealing with URL accept / return a raw path (% encoded): * * getBasePath * * getBaseUrl * * getPathInfo * * getRequestUri * * getUri * * getUriForPath * * @author Fabien Potencier <fabien@symfony.com> */ class Request { public const HEADER_FORWARDED = 0b000001; // When using RFC 7239 public const HEADER_X_FORWARDED_FOR = 0b000010; public const HEADER_X_FORWARDED_HOST = 0b000100; public const HEADER_X_FORWARDED_PROTO = 0b001000; public const HEADER_X_FORWARDED_PORT = 0b010000; public const HEADER_X_FORWARDED_PREFIX = 0b100000; public const HEADER_X_FORWARDED_AWS_ELB = 0b0011010; // AWS ELB doesn't send X-Forwarded-Host public const HEADER_X_FORWARDED_TRAEFIK = 0b0111110; // All "X-Forwarded-*" headers sent by Traefik reverse proxy public const METHOD_HEAD = 'HEAD'; public const METHOD_GET = 'GET'; public const METHOD_POST = 'POST'; public const METHOD_PUT = 'PUT'; public const METHOD_PATCH = 'PATCH'; public const METHOD_DELETE = 'DELETE'; public const METHOD_PURGE = 'PURGE'; public const METHOD_OPTIONS = 'OPTIONS'; public const METHOD_TRACE = 'TRACE'; public const METHOD_CONNECT = 'CONNECT'; /** * @var string[] */ protected static $trustedProxies = []; /** * @var string[] */ protected static $trustedHostPatterns = []; /** * @var string[] */ protected static $trustedHosts = []; protected static $httpMethodParameterOverride = false; /** * Custom parameters. * * @var ParameterBag */ public $attributes; /** * Request body parameters ($_POST). * * @var InputBag */ public $request; /** * Query string parameters ($_GET). * * @var InputBag */ public $query; /** * Server and execution environment parameters ($_SERVER). * * @var ServerBag */ public $server; /** * Uploaded files ($_FILES). * * @var FileBag */ public $files; /** * Cookies ($_COOKIE). * * @var InputBag */ public $cookies; /** * Headers (taken from the $_SERVER). * * @var HeaderBag */ public $headers; /** * @var string|resource|false|null */ protected $content; /** * @var array */ protected $languages; /** * @var array */ protected $charsets; /** * @var array */ protected $encodings; /** * @var array */ protected $acceptableContentTypes; /** * @var string */ protected $pathInfo; /** * @var string */ protected $requestUri; /** * @var string */ protected $baseUrl; /** * @var string */ protected $basePath; /** * @var string */ protected $method; /** * @var string */ protected $format; /** * @var SessionInterface|callable(): SessionInterface */ protected $session; /** * @var string */ protected $locale; /** * @var string */ protected $defaultLocale = 'en'; /** * @var array */ protected static $formats; protected static $requestFactory; private ?string $preferredFormat = null; private bool $isHostValid = true; private bool $isForwardedValid = true; private bool $isSafeContentPreferred; private static int $trustedHeaderSet = -1; private const FORWARDED_PARAMS = [ self::HEADER_X_FORWARDED_FOR => 'for', self::HEADER_X_FORWARDED_HOST => 'host', self::HEADER_X_FORWARDED_PROTO => 'proto', self::HEADER_X_FORWARDED_PORT => 'host', ]; /** * Names for headers that can be trusted when * using trusted proxies. * * The FORWARDED header is the standard as of rfc7239. * * The other headers are non-standard, but widely used * by popular reverse proxies (like Apache mod_proxy or Amazon EC2). */ private const TRUSTED_HEADERS = [ self::HEADER_FORWARDED => 'FORWARDED', self::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR', self::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST', self::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO', self::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT', self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX', ]; /** * @param array $query The GET parameters * @param array $request The POST parameters * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) * @param array $cookies The COOKIE parameters * @param array $files The FILES parameters * @param array $server The SERVER parameters * @param string|resource|null $content The raw body data */ public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) { $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content); } /** * Sets the parameters for this request. * * This method also re-initializes all properties. * * @param array $query The GET parameters * @param array $request The POST parameters * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) * @param array $cookies The COOKIE parameters * @param array $files The FILES parameters * @param array $server The SERVER parameters * @param string|resource|null $content The raw body data */ public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) { $this->request = new InputBag($request); $this->query = new InputBag($query); $this->attributes = new ParameterBag($attributes); $this->cookies = new InputBag($cookies); $this->files = new FileBag($files); $this->server = new ServerBag($server); $this->headers = new HeaderBag($this->server->getHeaders()); $this->content = $content; $this->languages = null; $this->charsets = null; $this->encodings = null; $this->acceptableContentTypes = null; $this->pathInfo = null; $this->requestUri = null; $this->baseUrl = null; $this->basePath = null; $this->method = null; $this->format = null; } /** * Creates a new request with values from PHP's super globals. */ public static function createFromGlobals(): static { $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER); if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded') && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH']) ) { parse_str($request->getContent(), $data); $request->request = new InputBag($data); } return $request; } /** * Creates a Request based on a given URI and configuration. * * The information contained in the URI always take precedence * over the other information (server and parameters). * * @param string $uri The URI * @param string $method The HTTP method * @param array $parameters The query (GET) or request (POST) parameters * @param array $cookies The request cookies ($_COOKIE) * @param array $files The request files ($_FILES) * @param array $server The server parameters ($_SERVER) * @param string|resource|null $content The raw body data */ public static function create(string $uri, string $method = 'GET', array $parameters = [], array $cookies = [], array $files = [], array $server = [], $content = null): static { $server = array_replace([ 'SERVER_NAME' => 'localhost', 'SERVER_PORT' => 80, 'HTTP_HOST' => 'localhost', 'HTTP_USER_AGENT' => 'Symfony', 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'HTTP_ACCEPT_LANGUAGE' => 'en-us,en;q=0.5', 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', 'REMOTE_ADDR' => '127.0.0.1', 'SCRIPT_NAME' => '', 'SCRIPT_FILENAME' => '', 'SERVER_PROTOCOL' => 'HTTP/1.1', 'REQUEST_TIME' => time(), 'REQUEST_TIME_FLOAT' => microtime(true), ], $server); $server['PATH_INFO'] = ''; $server['REQUEST_METHOD'] = strtoupper($method); $components = parse_url($uri); if (isset($components['host'])) { $server['SERVER_NAME'] = $components['host']; $server['HTTP_HOST'] = $components['host']; } if (isset($components['scheme'])) { if ('https' === $components['scheme']) { $server['HTTPS'] = 'on'; $server['SERVER_PORT'] = 443; } else { unset($server['HTTPS']); $server['SERVER_PORT'] = 80; } } if (isset($components['port'])) { $server['SERVER_PORT'] = $components['port']; $server['HTTP_HOST'] .= ':'.$components['port']; } if (isset($components['user'])) { $server['PHP_AUTH_USER'] = $components['user']; } if (isset($components['pass'])) { $server['PHP_AUTH_PW'] = $components['pass']; } if (!isset($components['path'])) { $components['path'] = '/'; } switch (strtoupper($method)) { case 'POST': case 'PUT': case 'DELETE': if (!isset($server['CONTENT_TYPE'])) { $server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; } // no break case 'PATCH': $request = $parameters; $query = []; break; default: $request = []; $query = $parameters; break; } $queryString = ''; if (isset($components['query'])) { parse_str(html_entity_decode($components['query']), $qs); if ($query) { $query = array_replace($qs, $query); $queryString = http_build_query($query, '', '&'); } else { $query = $qs; $queryString = $components['query']; } } elseif ($query) { $queryString = http_build_query($query, '', '&'); } $server['REQUEST_URI'] = $components['path'].('' !== $queryString ? '?'.$queryString : ''); $server['QUERY_STRING'] = $queryString; return self::createRequestFromFactory($query, $request, [], $cookies, $files, $server, $content); } /** * Sets a callable able to create a Request instance. * * This is mainly useful when you need to override the Request class * to keep BC with an existing system. It should not be used for any * other purpose. */ public static function setFactory(?callable $callable) { self::$requestFactory = $callable; } /** * Clones a request and overrides some of its parameters. * * @param array $query The GET parameters * @param array $request The POST parameters * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) * @param array $cookies The COOKIE parameters * @param array $files The FILES parameters * @param array $server The SERVER parameters */ public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null): static { $dup = clone $this; if (null !== $query) { $dup->query = new InputBag($query); } if (null !== $request) { $dup->request = new InputBag($request); } if (null !== $attributes) { $dup->attributes = new ParameterBag($attributes); } if (null !== $cookies) { $dup->cookies = new InputBag($cookies); } if (null !== $files) { $dup->files = new FileBag($files); } if (null !== $server) { $dup->server = new ServerBag($server); $dup->headers = new HeaderBag($dup->server->getHeaders()); } $dup->languages = null; $dup->charsets = null; $dup->encodings = null; $dup->acceptableContentTypes = null; $dup->pathInfo = null; $dup->requestUri = null; $dup->baseUrl = null; $dup->basePath = null; $dup->method = null; $dup->format = null; if (!$dup->get('_format') && $this->get('_format')) { $dup->attributes->set('_format', $this->get('_format')); } if (!$dup->getRequestFormat(null)) { $dup->setRequestFormat($this->getRequestFormat(null)); } return $dup; } /** * Clones the current request. * * Note that the session is not cloned as duplicated requests * are most of the time sub-requests of the main one. */ public function __clone() { $this->query = clone $this->query; $this->request = clone $this->request; $this->attributes = clone $this->attributes; $this->cookies = clone $this->cookies; $this->files = clone $this->files; $this->server = clone $this->server; $this->headers = clone $this->headers; } public function __toString(): string { $content = $this->getContent(); $cookieHeader = ''; $cookies = []; foreach ($this->cookies as $k => $v) { $cookies[] = \is_array($v) ? http_build_query([$k => $v], '', '; ', \PHP_QUERY_RFC3986) : "$k=$v"; } if ($cookies) { $cookieHeader = 'Cookie: '.implode('; ', $cookies)."\r\n"; } return sprintf('%s %s %s', $this->getMethod(), $this->getRequestUri(), $this->server->get('SERVER_PROTOCOL'))."\r\n". $this->headers. $cookieHeader."\r\n". $content; } /** * Overrides the PHP global variables according to this request instance. * * It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE. * $_FILES is never overridden, see rfc1867 */ public function overrideGlobals() { $this->server->set('QUERY_STRING', static::normalizeQueryString(http_build_query($this->query->all(), '', '&'))); $_GET = $this->query->all(); $_POST = $this->request->all(); $_SERVER = $this->server->all(); $_COOKIE = $this->cookies->all(); foreach ($this->headers->all() as $key => $value) { $key = strtoupper(str_replace('-', '_', $key)); if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) { $_SERVER[$key] = implode(', ', $value); } else { $_SERVER['HTTP_'.$key] = implode(', ', $value); } } $request = ['g' => $_GET, 'p' => $_POST, 'c' => $_COOKIE]; $requestOrder = \ini_get('request_order') ?: \ini_get('variables_order'); $requestOrder = preg_replace('#[^cgp]#', '', strtolower($requestOrder)) ?: 'gp'; $_REQUEST = [[]]; foreach (str_split($requestOrder) as $order) { $_REQUEST[] = $request[$order]; } $_REQUEST = array_merge(...$_REQUEST); } /** * Sets a list of trusted proxies. * * You should only list the reverse proxies that you manage directly. * * @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR'] * @param int $trustedHeaderSet A bit field of Request::HEADER_*, to set which headers to trust from your proxies */ public static function setTrustedProxies(array $proxies, int $trustedHeaderSet) { self::$trustedProxies = array_reduce($proxies, function ($proxies, $proxy) { if ('REMOTE_ADDR' !== $proxy) { $proxies[] = $proxy; } elseif (isset($_SERVER['REMOTE_ADDR'])) { $proxies[] = $_SERVER['REMOTE_ADDR']; } return $proxies; }, []); self::$trustedHeaderSet = $trustedHeaderSet; } /** * Gets the list of trusted proxies. */ public static function getTrustedProxies(): array { return self::$trustedProxies; } /** * Gets the set of trusted headers from trusted proxies. * * @return int A bit field of Request::HEADER_* that defines which headers are trusted from your proxies */ public static function getTrustedHeaderSet(): int { return self::$trustedHeaderSet; } /** * Sets a list of trusted host patterns. * * You should only list the hosts you manage using regexs. * * @param array $hostPatterns A list of trusted host patterns */ public static function setTrustedHosts(array $hostPatterns) { self::$trustedHostPatterns = array_map(function ($hostPattern) { return sprintf('{%s}i', $hostPattern); }, $hostPatterns); // we need to reset trusted hosts on trusted host patterns change self::$trustedHosts = []; } /** * Gets the list of trusted host patterns. */ public static function getTrustedHosts(): array { return self::$trustedHostPatterns; } /** * Normalizes a query string. * * It builds a normalized query string, where keys/value pairs are alphabetized, * have consistent escaping and unneeded delimiters are removed. */ public static function normalizeQueryString(?string $qs): string { if ('' === ($qs ?? '')) { return ''; } $qs = HeaderUtils::parseQuery($qs); ksort($qs); return http_build_query($qs, '', '&', \PHP_QUERY_RFC3986); } /** * Enables support for the _method request parameter to determine the intended HTTP method. * * Be warned that enabling this feature might lead to CSRF issues in your code. * Check that you are using CSRF tokens when required. * If the HTTP method parameter override is enabled, an html-form with method "POST" can be altered * and used to send a "PUT" or "DELETE" request via the _method request parameter. * If these methods are not protected against CSRF, this presents a possible vulnerability. * * The HTTP method can only be overridden when the real HTTP method is POST. */ public static function enableHttpMethodParameterOverride() { self::$httpMethodParameterOverride = true; } /** * Checks whether support for the _method request parameter is enabled. */ public static function getHttpMethodParameterOverride(): bool { return self::$httpMethodParameterOverride; } /** * Gets a "parameter" value from any bag. * * This method is mainly useful for libraries that want to provide some flexibility. If you don't need the * flexibility in controllers, it is better to explicitly get request parameters from the appropriate * public property instead (attributes, query, request). * * Order of precedence: PATH (routing placeholders or custom attributes), GET, POST * * @internal use explicit input sources instead */ public function get(string $key, mixed $default = null): mixed { if ($this !== $result = $this->attributes->get($key, $this)) { return $result; } if ($this->query->has($key)) { return $this->query->all()[$key]; } if ($this->request->has($key)) { return $this->request->all()[$key]; } return $default; } /** * Gets the Session. */ public function getSession(): SessionInterface { $session = $this->session; if (!$session instanceof SessionInterface && null !== $session) { $this->setSession($session = $session()); } if (null === $session) { throw new SessionNotFoundException('Session has not been set.'); } return $session; } /** * Whether the request contains a Session which was started in one of the * previous requests. */ public function hasPreviousSession(): bool { // the check for $this->session avoids malicious users trying to fake a session cookie with proper name return $this->hasSession() && $this->cookies->has($this->getSession()->getName()); } /** * Whether the request contains a Session object. * * This method does not give any information about the state of the session object, * like whether the session is started or not. It is just a way to check if this Request * is associated with a Session instance. * * @param bool $skipIfUninitialized When true, ignores factories injected by `setSessionFactory` */ public function hasSession(bool $skipIfUninitialized = false): bool { return null !== $this->session && (!$skipIfUninitialized || $this->session instanceof SessionInterface); } public function setSession(SessionInterface $session) { $this->session = $session; } /** * @internal * * @param callable(): SessionInterface $factory */ public function setSessionFactory(callable $factory) { $this->session = $factory; } /** * Returns the client IP addresses. * * In the returned array the most trusted IP address is first, and the * least trusted one last. The "real" client IP address is the last one, * but this is also the least trusted one. Trusted proxies are stripped. * * Use this method carefully; you should use getClientIp() instead. * * @see getClientIp() */ public function getClientIps(): array { $ip = $this->server->get('REMOTE_ADDR'); if (!$this->isFromTrustedProxy()) { return [$ip]; } return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip]; } /** * Returns the client IP address. * * This method can read the client IP address from the "X-Forwarded-For" header * when trusted proxies were set via "setTrustedProxies()". The "X-Forwarded-For" * header value is a comma+space separated list of IP addresses, the left-most * being the original client, and each successive proxy that passed the request * adding the IP address where it received the request from. * * If your reverse proxy uses a different header name than "X-Forwarded-For", * ("Client-Ip" for instance), configure it via the $trustedHeaderSet * argument of the Request::setTrustedProxies() method instead. * * @see getClientIps() * @see https://wikipedia.org/wiki/X-Forwarded-For */ public function getClientIp(): ?string { $ipAddresses = $this->getClientIps(); return $ipAddresses[0]; } /** * Returns current script name. */ public function getScriptName(): string { return $this->server->get('SCRIPT_NAME', $this->server->get('ORIG_SCRIPT_NAME', '')); } /** * Returns the path being requested relative to the executed script. * * The path info always starts with a /. * * Suppose this request is instantiated from /mysite on localhost: * * * http://localhost/mysite returns an empty string * * http://localhost/mysite/about returns '/about' * * http://localhost/mysite/enco%20ded returns '/enco%20ded' * * http://localhost/mysite/about?var=1 returns '/about' * * @return string The raw path (i.e. not urldecoded) */ public function getPathInfo(): string { if (null === $this->pathInfo) { $this->pathInfo = $this->preparePathInfo(); } return $this->pathInfo; } /** * Returns the root path from which this request is executed. * * Suppose that an index.php file instantiates this request object: * * * http://localhost/index.php returns an empty string * * http://localhost/index.php/page returns an empty string * * http://localhost/web/index.php returns '/web' * * http://localhost/we%20b/index.php returns '/we%20b' * * @return string The raw path (i.e. not urldecoded) */ public function getBasePath(): string { if (null === $this->basePath) { $this->basePath = $this->prepareBasePath(); } return $this->basePath; } /** * Returns the root URL from which this request is executed. * * The base URL never ends with a /. * * This is similar to getBasePath(), except that it also includes the * script filename (e.g. index.php) if one exists. * * @return string The raw URL (i.e. not urldecoded) */ public function getBaseUrl(): string { $trustedPrefix = ''; // the proxy prefix must be prepended to any prefix being needed at the webserver level if ($this->isFromTrustedProxy() && $trustedPrefixValues = $this->getTrustedValues(self::HEADER_X_FORWARDED_PREFIX)) { $trustedPrefix = rtrim($trustedPrefixValues[0], '/'); } return $trustedPrefix.$this->getBaseUrlReal(); } /** * Returns the real base URL received by the webserver from which this request is executed. * The URL does not include trusted reverse proxy prefix. * * @return string The raw URL (i.e. not urldecoded) */ private function getBaseUrlReal(): string { if (null === $this->baseUrl) { $this->baseUrl = $this->prepareBaseUrl(); } return $this->baseUrl; } /** * Gets the request's scheme. */ public function getScheme(): string { return $this->isSecure() ? 'https' : 'http'; } /** * Returns the port on which the request is made. * * This method can read the client port from the "X-Forwarded-Port" header * when trusted proxies were set via "setTrustedProxies()". * * The "X-Forwarded-Port" header must contain the client port. * * @return int|string|null Can be a string if fetched from the server bag */ public function getPort(): int|string|null { if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_PORT)) { $host = $host[0]; } elseif ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) { $host = $host[0]; } elseif (!$host = $this->headers->get('HOST')) { return $this->server->get('SERVER_PORT'); } if ('[' === $host[0]) { $pos = strpos($host, ':', strrpos($host, ']')); } else { $pos = strrpos($host, ':'); } if (false !== $pos && $port = substr($host, $pos + 1)) { return (int) $port; } return 'https' === $this->getScheme() ? 443 : 80; } /** * Returns the user. */ public function getUser(): ?string { return $this->headers->get('PHP_AUTH_USER'); } /** * Returns the password. */ public function getPassword(): ?string { return $this->headers->get('PHP_AUTH_PW'); } /** * Gets the user info. * * @return string|null A user name if any and, optionally, scheme-specific information about how to gain authorization to access the server */ public function getUserInfo(): ?string { $userinfo = $this->getUser(); $pass = $this->getPassword(); if ('' != $pass) { $userinfo .= ":$pass"; } return $userinfo; } /** * Returns the HTTP host being requested. * * The port name will be appended to the host if it's non-standard. */ public function getHttpHost(): string { $scheme = $this->getScheme(); $port = $this->getPort(); if (('http' == $scheme && 80 == $port) || ('https' == $scheme && 443 == $port)) { return $this->getHost(); } return $this->getHost().':'.$port; } /** * Returns the requested URI (path and query string). * * @return string The raw URI (i.e. not URI decoded) */ public function getRequestUri(): string { if (null === $this->requestUri) { $this->requestUri = $this->prepareRequestUri(); } return $this->requestUri; } /** * Gets the scheme and HTTP host. * * If the URL was called with basic authentication, the user * and the password are not added to the generated string. */ public function getSchemeAndHttpHost(): string { return $this->getScheme().'://'.$this->getHttpHost(); } /** * Generates a normalized URI (URL) for the Request. * * @see getQueryString() */ public function getUri(): string { if (null !== $qs = $this->getQueryString()) { $qs = '?'.$qs; } return $this->getSchemeAndHttpHost().$this->getBaseUrl().$this->getPathInfo().$qs; } /** * Generates a normalized URI for the given path. * * @param string $path A path to use instead of the current one */ public function getUriForPath(string $path): string { return $this->getSchemeAndHttpHost().$this->getBaseUrl().$path; } /** * Returns the path as relative reference from the current Request path. * * Only the URIs path component (no schema, host etc.) is relevant and must be given. * Both paths must be absolute and not contain relative parts. * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives. * Furthermore, they can be used to reduce the link size in documents. * * Example target paths, given a base path of "/a/b/c/d": * - "/a/b/c/d" -> "" * - "/a/b/c/" -> "./" * - "/a/b/" -> "../" * - "/a/b/c/other" -> "other" * - "/a/x/y" -> "../../x/y" */ public function getRelativeUriForPath(string $path): string { // be sure that we are dealing with an absolute path if (!isset($path[0]) || '/' !== $path[0]) { return $path; } if ($path === $basePath = $this->getPathInfo()) { return ''; } $sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath); $targetDirs = explode('/', substr($path, 1)); array_pop($sourceDirs); $targetFile = array_pop($targetDirs); foreach ($sourceDirs as $i => $dir) { if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) { unset($sourceDirs[$i], $targetDirs[$i]); } else { break; } } $targetDirs[] = $targetFile; $path = str_repeat('../', \count($sourceDirs)).implode('/', $targetDirs); // A reference to the same base directory or an empty subdirectory must be prefixed with "./". // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used // as the first segment of a relative-path reference, as it would be mistaken for a scheme name // (see https://tools.ietf.org/html/rfc3986#section-4.2). return !isset($path[0]) || '/' === $path[0] || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos) ? "./$path" : $path; } /** * Generates the normalized query string for the Request. * * It builds a normalized query string, where keys/value pairs are alphabetized * and have consistent escaping. */ public function getQueryString(): ?string { $qs = static::normalizeQueryString($this->server->get('QUERY_STRING')); return '' === $qs ? null : $qs; } /** * Checks whether the request is secure or not. * * This method can read the client protocol from the "X-Forwarded-Proto" header * when trusted proxies were set via "setTrustedProxies()". * * The "X-Forwarded-Proto" header must contain the protocol: "https" or "http". */ public function isSecure(): bool { if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO)) { return \in_array(strtolower($proto[0]), ['https', 'on', 'ssl', '1'], true); } $https = $this->server->get('HTTPS'); return !empty($https) && 'off' !== strtolower($https); } /** * Returns the host name. * * This method can read the client host name from the "X-Forwarded-Host" header * when trusted proxies were set via "setTrustedProxies()". * * The "X-Forwarded-Host" header must contain the client host name. * * @throws SuspiciousOperationException when the host name is invalid or not trusted */ public function getHost(): string { if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) { $host = $host[0]; } elseif (!$host = $this->headers->get('HOST')) { if (!$host = $this->server->get('SERVER_NAME')) { $host = $this->server->get('SERVER_ADDR', ''); } } // trim and remove port number from host // host is lowercase as per RFC 952/2181 $host = strtolower(preg_replace('/:\d+$/', '', trim($host))); // as the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user) // check that it does not contain forbidden characters (see RFC 952 and RFC 2181) // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names if ($host && '' !== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host)) { if (!$this->isHostValid) { return ''; } $this->isHostValid = false; throw new SuspiciousOperationException(sprintf('Invalid Host "%s".', $host)); } if (\count(self::$trustedHostPatterns) > 0) { // to avoid host header injection attacks, you should provide a list of trusted host patterns if (\in_array($host, self::$trustedHosts)) { return $host; } foreach (self::$trustedHostPatterns as $pattern) { if (preg_match($pattern, $host)) { self::$trustedHosts[] = $host; return $host; } } if (!$this->isHostValid) { return ''; } $this->isHostValid = false; throw new SuspiciousOperationException(sprintf('Untrusted Host "%s".', $host)); } return $host; } /** * Sets the request method. */ public function setMethod(string $method) { $this->method = null; $this->server->set('REQUEST_METHOD', $method); } /** * Gets the request "intended" method. * * If the X-HTTP-Method-Override header is set, and if the method is a POST, * then it is used to determine the "real" intended HTTP method. * * The _method request parameter can also be used to determine the HTTP method, * but only if enableHttpMethodParameterOverride() has been called. * * The method is always an uppercased string. * * @see getRealMethod() */ public function getMethod(): string { if (null !== $this->method) { return $this->method; } $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET')); if ('POST' !== $this->method) { return $this->method; } $method = $this->headers->get('X-HTTP-METHOD-OVERRIDE'); if (!$method && self::$httpMethodParameterOverride) { $method = $this->request->get('_method', $this->query->get('_method', 'POST')); } if (!\is_string($method)) { return $this->method; } $method = strtoupper($method); if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE'], true)) { return $this->method = $method; } if (!preg_match('/^[A-Z]++$/D', $method)) { throw new SuspiciousOperationException(sprintf('Invalid method override "%s".', $method)); } return $this->method = $method; } /** * Gets the "real" request method. * * @see getMethod() */ public function getRealMethod(): string { return strtoupper($this->server->get('REQUEST_METHOD', 'GET')); } /** * Gets the mime type associated with the format. */ public function getMimeType(string $format): ?string { if (null === static::$formats) { static::initializeFormats(); } return isset(static::$formats[$format]) ? static::$formats[$format][0] : null; } /** * Gets the mime types associated with the format. */ public static function getMimeTypes(string $format): array { if (null === static::$formats) { static::initializeFormats(); } return static::$formats[$format] ?? []; } /** * Gets the format associated with the mime type. */ public function getFormat(?string $mimeType): ?string { $canonicalMimeType = null; if ($mimeType && false !== $pos = strpos($mimeType, ';')) { $canonicalMimeType = trim(substr($mimeType, 0, $pos)); } if (null === static::$formats) { static::initializeFormats(); } foreach (static::$formats as $format => $mimeTypes) { if (\in_array($mimeType, (array) $mimeTypes)) { return $format; } if (null !== $canonicalMimeType && \in_array($canonicalMimeType, (array) $mimeTypes)) { return $format; } } return null; } /** * Associates a format with mime types. * * @param string|array $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type) */ public function setFormat(?string $format, string|array $mimeTypes) { if (null === static::$formats) { static::initializeFormats(); } static::$formats[$format] = \is_array($mimeTypes) ? $mimeTypes : [$mimeTypes]; } /** * Gets the request format. * * Here is the process to determine the format: * * * format defined by the user (with setRequestFormat()) * * _format request attribute * * $default * * @see getPreferredFormat */ public function getRequestFormat(?string $default = 'html'): ?string { if (null === $this->format) { $this->format = $this->attributes->get('_format'); } return $this->format ?? $default; } /** * Sets the request format. */ public function setRequestFormat(?string $format) { $this->format = $format; } /** * Gets the format associated with the request. */ public function getContentType(): ?string { return $this->getFormat($this->headers->get('CONTENT_TYPE', '')); } /** * Sets the default locale. */ public function setDefaultLocale(string $locale) { $this->defaultLocale = $locale; if (null === $this->locale) { $this->setPhpDefaultLocale($locale); } } /** * Get the default locale. */ public function getDefaultLocale(): string { return $this->defaultLocale; } /** * Sets the locale. */ public function setLocale(string $locale) { $this->setPhpDefaultLocale($this->locale = $locale); } /** * Get the locale. */ public function getLocale(): string { return null === $this->locale ? $this->defaultLocale : $this->locale; } /** * Checks if the request method is of specified type. * * @param string $method Uppercase request method (GET, POST etc) */ public function isMethod(string $method): bool { return $this->getMethod() === strtoupper($method); } /** * Checks whether or not the method is safe. * * @see https://tools.ietf.org/html/rfc7231#section-4.2.1 */ public function isMethodSafe(): bool { return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']); } /** * Checks whether or not the method is idempotent. */ public function isMethodIdempotent(): bool { return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE']); } /** * Checks whether the method is cacheable or not. * * @see https://tools.ietf.org/html/rfc7231#section-4.2.3 */ public function isMethodCacheable(): bool { return \in_array($this->getMethod(), ['GET', 'HEAD']); } /** * Returns the protocol version. * * If the application is behind a proxy, the protocol version used in the * requests between the client and the proxy and between the proxy and the * server might be different. This returns the former (from the "Via" header) * if the proxy is trusted (see "setTrustedProxies()"), otherwise it returns * the latter (from the "SERVER_PROTOCOL" server parameter). */ public function getProtocolVersion(): ?string { if ($this->isFromTrustedProxy()) { preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via') ?? '', $matches); if ($matches) { return 'HTTP/'.$matches[2]; } } return $this->server->get('SERVER_PROTOCOL'); } /** * Returns the request body content. * * @param bool $asResource If true, a resource will be returned * * @return string|resource */ public function getContent(bool $asResource = false) { $currentContentIsResource = \is_resource($this->content); if (true === $asResource) { if ($currentContentIsResource) { rewind($this->content); return $this->content; } // Content passed in parameter (test) if (\is_string($this->content)) { $resource = fopen('php://temp', 'r+'); fwrite($resource, $this->content); rewind($resource); return $resource; } $this->content = false; return fopen('php://input', 'r'); } if ($currentContentIsResource) { rewind($this->content); return stream_get_contents($this->content); } if (null === $this->content || false === $this->content) { $this->content = file_get_contents('php://input'); } return $this->content; } /** * Gets the request body decoded as array, typically from a JSON payload. * * @throws JsonException When the body cannot be decoded to an array */ public function toArray(): array { if ('' === $content = $this->getContent()) { throw new JsonException('Request body is empty.'); } try { $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR); } catch (\JsonException $e) { throw new JsonException('Could not decode request body.', $e->getCode(), $e); } if (!\is_array($content)) { throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content))); } return $content; } /** * Gets the Etags. */ public function getETags(): array { return preg_split('/\s*,\s*/', $this->headers->get('If-None-Match', ''), -1, \PREG_SPLIT_NO_EMPTY); } public function isNoCache(): bool { return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma'); } /** * Gets the preferred format for the response by inspecting, in the following order: * * the request format set using setRequestFormat; * * the values of the Accept HTTP header. * * Note that if you use this method, you should send the "Vary: Accept" header * in the response to prevent any issues with intermediary HTTP caches. */ public function getPreferredFormat(?string $default = 'html'): ?string { if (null !== $this->preferredFormat || null !== $this->preferredFormat = $this->getRequestFormat(null)) { return $this->preferredFormat; } foreach ($this->getAcceptableContentTypes() as $mimeType) { if ($this->preferredFormat = $this->getFormat($mimeType)) { return $this->preferredFormat; } } return $default; } /** * Returns the preferred language. * * @param string[] $locales An array of ordered available locales */ public function getPreferredLanguage(array $locales = null): ?string { $preferredLanguages = $this->getLanguages(); if (empty($locales)) { return $preferredLanguages[0] ?? null; } if (!$preferredLanguages) { return $locales[0]; } $extendedPreferredLanguages = []; foreach ($preferredLanguages as $language) { $extendedPreferredLanguages[] = $language; if (false !== $position = strpos($language, '_')) { $superLanguage = substr($language, 0, $position); if (!\in_array($superLanguage, $preferredLanguages)) { $extendedPreferredLanguages[] = $superLanguage; } } } $preferredLanguages = array_values(array_intersect($extendedPreferredLanguages, $locales)); return $preferredLanguages[0] ?? $locales[0]; } /** * Gets a list of languages acceptable by the client browser ordered in the user browser preferences. */ public function getLanguages(): array { if (null !== $this->languages) { return $this->languages; } $languages = AcceptHeader::fromString($this->headers->get('Accept-Language'))->all(); $this->languages = []; foreach ($languages as $acceptHeaderItem) { $lang = $acceptHeaderItem->getValue(); if (str_contains($lang, '-')) { $codes = explode('-', $lang); if ('i' === $codes[0]) { // Language not listed in ISO 639 that are not variants // of any listed language, which can be registered with the // i-prefix, such as i-cherokee if (\count($codes) > 1) { $lang = $codes[1]; } } else { for ($i = 0, $max = \count($codes); $i < $max; ++$i) { if (0 === $i) { $lang = strtolower($codes[0]); } else { $lang .= '_'.strtoupper($codes[$i]); } } } } $this->languages[] = $lang; } return $this->languages; } /** * Gets a list of charsets acceptable by the client browser in preferable order. */ public function getCharsets(): array { if (null !== $this->charsets) { return $this->charsets; } return $this->charsets = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all())); } /** * Gets a list of encodings acceptable by the client browser in preferable order. */ public function getEncodings(): array { if (null !== $this->encodings) { return $this->encodings; } return $this->encodings = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all())); } /** * Gets a list of content types acceptable by the client browser in preferable order. */ public function getAcceptableContentTypes(): array { if (null !== $this->acceptableContentTypes) { return $this->acceptableContentTypes; } return $this->acceptableContentTypes = array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all())); } /** * Returns true if the request is an XMLHttpRequest. * * It works if your JavaScript library sets an X-Requested-With HTTP header. * It is known to work with common JavaScript frameworks: * * @see https://wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript */ public function isXmlHttpRequest(): bool { return 'XMLHttpRequest' == $this->headers->get('X-Requested-With'); } /** * Checks whether the client browser prefers safe content or not according to RFC8674. * * @see https://tools.ietf.org/html/rfc8674 */ public function preferSafeContent(): bool { if (isset($this->isSafeContentPreferred)) { return $this->isSafeContentPreferred; } if (!$this->isSecure()) { // see https://tools.ietf.org/html/rfc8674#section-3 return $this->isSafeContentPreferred = false; } return $this->isSafeContentPreferred = AcceptHeader::fromString($this->headers->get('Prefer'))->has('safe'); } /* * The following methods are derived from code of the Zend Framework (1.10dev - 2010-01-24) * * Code subject to the new BSD license (https://framework.zend.com/license). * * Copyright (c) 2005-2010 Zend Technologies USA Inc. (https://www.zend.com/) */ protected function prepareRequestUri() { $requestUri = ''; if ('1' == $this->server->get('IIS_WasUrlRewritten') && '' != $this->server->get('UNENCODED_URL')) { // IIS7 with URL Rewrite: make sure we get the unencoded URL (double slash problem) $requestUri = $this->server->get('UNENCODED_URL'); $this->server->remove('UNENCODED_URL'); $this->server->remove('IIS_WasUrlRewritten'); } elseif ($this->server->has('REQUEST_URI')) { $requestUri = $this->server->get('REQUEST_URI'); if ('' !== $requestUri && '/' === $requestUri[0]) { // To only use path and query remove the fragment. if (false !== $pos = strpos($requestUri, '#')) { $requestUri = substr($requestUri, 0, $pos); } } else { // HTTP proxy reqs setup request URI with scheme and host [and port] + the URL path, // only use URL path. $uriComponents = parse_url($requestUri); if (isset($uriComponents['path'])) { $requestUri = $uriComponents['path']; } if (isset($uriComponents['query'])) { $requestUri .= '?'.$uriComponents['query']; } } } elseif ($this->server->has('ORIG_PATH_INFO')) { // IIS 5.0, PHP as CGI $requestUri = $this->server->get('ORIG_PATH_INFO'); if ('' != $this->server->get('QUERY_STRING')) { $requestUri .= '?'.$this->server->get('QUERY_STRING'); } $this->server->remove('ORIG_PATH_INFO'); } // normalize the request URI to ease creating sub-requests from this request $this->server->set('REQUEST_URI', $requestUri); return $requestUri; } /** * Prepares the base URL. */ protected function prepareBaseUrl(): string { $filename = basename($this->server->get('SCRIPT_FILENAME', '')); if (basename($this->server->get('SCRIPT_NAME', '')) === $filename) { $baseUrl = $this->server->get('SCRIPT_NAME'); } elseif (basename($this->server->get('PHP_SELF', '')) === $filename) { $baseUrl = $this->server->get('PHP_SELF'); } elseif (basename($this->server->get('ORIG_SCRIPT_NAME', '')) === $filename) { $baseUrl = $this->server->get('ORIG_SCRIPT_NAME'); // 1and1 shared hosting compatibility } else { // Backtrack up the script_filename to find the portion matching // php_self $path = $this->server->get('PHP_SELF', ''); $file = $this->server->get('SCRIPT_FILENAME', ''); $segs = explode('/', trim($file, '/')); $segs = array_reverse($segs); $index = 0; $last = \count($segs); $baseUrl = ''; do { $seg = $segs[$index]; $baseUrl = '/'.$seg.$baseUrl; ++$index; } while ($last > $index && (false !== $pos = strpos($path, $baseUrl)) && 0 != $pos); } // Does the baseUrl have anything in common with the request_uri? $requestUri = $this->getRequestUri(); if ('' !== $requestUri && '/' !== $requestUri[0]) { $requestUri = '/'.$requestUri; } if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) { // full $baseUrl matches return $prefix; } if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, rtrim(\dirname($baseUrl), '/'.\DIRECTORY_SEPARATOR).'/')) { // directory portion of $baseUrl matches return rtrim($prefix, '/'.\DIRECTORY_SEPARATOR); } $truncatedRequestUri = $requestUri; if (false !== $pos = strpos($requestUri, '?')) { $truncatedRequestUri = substr($requestUri, 0, $pos); } $basename = basename($baseUrl ?? ''); if (empty($basename) || !strpos(rawurldecode($truncatedRequestUri), $basename)) { // no match whatsoever; set it blank return ''; } // If using mod_rewrite or ISAPI_Rewrite strip the script filename // out of baseUrl. $pos !== 0 makes sure it is not matching a value // from PATH_INFO or QUERY_STRING if (\strlen($requestUri) >= \strlen($baseUrl) && (false !== $pos = strpos($requestUri, $baseUrl)) && 0 !== $pos) { $baseUrl = substr($requestUri, 0, $pos + \strlen($baseUrl)); } return rtrim($baseUrl, '/'.\DIRECTORY_SEPARATOR); } /** * Prepares the base path. */ protected function prepareBasePath(): string { $baseUrl = $this->getBaseUrl(); if (empty($baseUrl)) { return ''; } $filename = basename($this->server->get('SCRIPT_FILENAME')); if (basename($baseUrl) === $filename) { $basePath = \dirname($baseUrl); } else { $basePath = $baseUrl; } if ('\\' === \DIRECTORY_SEPARATOR) { $basePath = str_replace('\\', '/', $basePath); } return rtrim($basePath, '/'); } /** * Prepares the path info. */ protected function preparePathInfo(): string { if (null === ($requestUri = $this->getRequestUri())) { return '/'; } // Remove the query string from REQUEST_URI if (false !== $pos = strpos($requestUri, '?')) { $requestUri = substr($requestUri, 0, $pos); } if ('' !== $requestUri && '/' !== $requestUri[0]) { $requestUri = '/'.$requestUri; } if (null === ($baseUrl = $this->getBaseUrlReal())) { return $requestUri; } $pathInfo = substr($requestUri, \strlen($baseUrl)); if (false === $pathInfo || '' === $pathInfo) { // If substr() returns false then PATH_INFO is set to an empty string return '/'; } return $pathInfo; } /** * Initializes HTTP request formats. */ protected static function initializeFormats() { static::$formats = [ 'html' => ['text/html', 'application/xhtml+xml'], 'txt' => ['text/plain'], 'js' => ['application/javascript', 'application/x-javascript', 'text/javascript'], 'css' => ['text/css'], 'json' => ['application/json', 'application/x-json'], 'jsonld' => ['application/ld+json'], 'xml' => ['text/xml', 'application/xml', 'application/x-xml'], 'rdf' => ['application/rdf+xml'], 'atom' => ['application/atom+xml'], 'rss' => ['application/rss+xml'], 'form' => ['application/x-www-form-urlencoded', 'multipart/form-data'], ]; } private function setPhpDefaultLocale(string $locale): void { // if either the class Locale doesn't exist, or an exception is thrown when // setting the default locale, the intl module is not installed, and // the call can be ignored: try { if (class_exists(\Locale::class, false)) { \Locale::setDefault($locale); } } catch (\Exception $e) { } } /** * Returns the prefix as encoded in the string when the string starts with * the given prefix, null otherwise. */ private function getUrlencodedPrefix(string $string, string $prefix): ?string { if (!str_starts_with(rawurldecode($string), $prefix)) { return null; } $len = \strlen($prefix); if (preg_match(sprintf('#^(%%[[:xdigit:]]{2}|.){%d}#', $len), $string, $match)) { return $match[0]; } return null; } private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null): static { if (self::$requestFactory) { $request = (self::$requestFactory)($query, $request, $attributes, $cookies, $files, $server, $content); if (!$request instanceof self) { throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.'); } return $request; } return new static($query, $request, $attributes, $cookies, $files, $server, $content); } /** * Indicates whether this request originated from a trusted proxy. * * This can be useful to determine whether or not to trust the * contents of a proxy-specific header. */ public function isFromTrustedProxy(): bool { return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies); } private function getTrustedValues(int $type, string $ip = null): array { $clientValues = []; $forwardedValues = []; if ((self::$trustedHeaderSet & $type) && $this->headers->has(self::TRUSTED_HEADERS[$type])) { foreach (explode(',', $this->headers->get(self::TRUSTED_HEADERS[$type])) as $v) { $clientValues[] = (self::HEADER_X_FORWARDED_PORT === $type ? '0.0.0.0:' : '').trim($v); } } if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && (isset(self::FORWARDED_PARAMS[$type])) && $this->headers->has(self::TRUSTED_HEADERS[self::HEADER_FORWARDED])) { $forwarded = $this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]); $parts = HeaderUtils::split($forwarded, ',;='); $forwardedValues = []; $param = self::FORWARDED_PARAMS[$type]; foreach ($parts as $subParts) { if (null === $v = HeaderUtils::combine($subParts)[$param] ?? null) { continue; } if (self::HEADER_X_FORWARDED_PORT === $type) { if (str_ends_with($v, ']') || false === $v = strrchr($v, ':')) { $v = $this->isSecure() ? ':443' : ':80'; } $v = '0.0.0.0'.$v; } $forwardedValues[] = $v; } } if (null !== $ip) { $clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip); $forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip); } if ($forwardedValues === $clientValues || !$clientValues) { return $forwardedValues; } if (!$forwardedValues) { return $clientValues; } if (!$this->isForwardedValid) { return null !== $ip ? ['0.0.0.0', $ip] : []; } $this->isForwardedValid = false; throw new ConflictingHeadersException(sprintf('The request has both a trusted "%s" header and a trusted "%s" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.', self::TRUSTED_HEADERS[self::HEADER_FORWARDED], self::TRUSTED_HEADERS[$type])); } private function normalizeAndFilterClientIps(array $clientIps, string $ip): array { if (!$clientIps) { return []; } $clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from $firstTrustedIp = null; foreach ($clientIps as $key => $clientIp) { if (strpos($clientIp, '.')) { // Strip :port from IPv4 addresses. This is allowed in Forwarded // and may occur in X-Forwarded-For. $i = strpos($clientIp, ':'); if ($i) { $clientIps[$key] = $clientIp = substr($clientIp, 0, $i); } } elseif (str_starts_with($clientIp, '[')) { // Strip brackets and :port from IPv6 addresses. $i = strpos($clientIp, ']', 1); $clientIps[$key] = $clientIp = substr($clientIp, 1, $i - 1); } if (!filter_var($clientIp, \FILTER_VALIDATE_IP)) { unset($clientIps[$key]); continue; } if (IpUtils::checkIp($clientIp, self::$trustedProxies)) { unset($clientIps[$key]); // Fallback to this when the client IP falls into the range of trusted proxies if (null === $firstTrustedIp) { $firstTrustedIp = $clientIp; } } } // Now the IP chain contains only untrusted proxies and the client IP return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp]; } } http-foundation/ServerBag.php 0000644 00000007627 15025017654 0012301 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * ServerBag is a container for HTTP headers from the $_SERVER variable. * * @author Fabien Potencier <fabien@symfony.com> * @author Bulat Shakirzyanov <mallluhuct@gmail.com> * @author Robert Kiss <kepten@gmail.com> */ class ServerBag extends ParameterBag { /** * Gets the HTTP headers. */ public function getHeaders(): array { $headers = []; foreach ($this->parameters as $key => $value) { if (str_starts_with($key, 'HTTP_')) { $headers[substr($key, 5)] = $value; } elseif (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) { $headers[$key] = $value; } } if (isset($this->parameters['PHP_AUTH_USER'])) { $headers['PHP_AUTH_USER'] = $this->parameters['PHP_AUTH_USER']; $headers['PHP_AUTH_PW'] = $this->parameters['PHP_AUTH_PW'] ?? ''; } else { /* * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default * For this workaround to work, add these lines to your .htaccess file: * RewriteCond %{HTTP:Authorization} .+ * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] * * A sample .htaccess file: * RewriteEngine On * RewriteCond %{HTTP:Authorization} .+ * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] * RewriteCond %{REQUEST_FILENAME} !-f * RewriteRule ^(.*)$ app.php [QSA,L] */ $authorizationHeader = null; if (isset($this->parameters['HTTP_AUTHORIZATION'])) { $authorizationHeader = $this->parameters['HTTP_AUTHORIZATION']; } elseif (isset($this->parameters['REDIRECT_HTTP_AUTHORIZATION'])) { $authorizationHeader = $this->parameters['REDIRECT_HTTP_AUTHORIZATION']; } if (null !== $authorizationHeader) { if (0 === stripos($authorizationHeader, 'basic ')) { // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic $exploded = explode(':', base64_decode(substr($authorizationHeader, 6)), 2); if (2 == \count($exploded)) { [$headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']] = $exploded; } } elseif (empty($this->parameters['PHP_AUTH_DIGEST']) && (0 === stripos($authorizationHeader, 'digest '))) { // In some circumstances PHP_AUTH_DIGEST needs to be set $headers['PHP_AUTH_DIGEST'] = $authorizationHeader; $this->parameters['PHP_AUTH_DIGEST'] = $authorizationHeader; } elseif (0 === stripos($authorizationHeader, 'bearer ')) { /* * XXX: Since there is no PHP_AUTH_BEARER in PHP predefined variables, * I'll just set $headers['AUTHORIZATION'] here. * https://php.net/reserved.variables.server */ $headers['AUTHORIZATION'] = $authorizationHeader; } } } if (isset($headers['AUTHORIZATION'])) { return $headers; } // PHP_AUTH_USER/PHP_AUTH_PW if (isset($headers['PHP_AUTH_USER'])) { $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.($headers['PHP_AUTH_PW'] ?? '')); } elseif (isset($headers['PHP_AUTH_DIGEST'])) { $headers['AUTHORIZATION'] = $headers['PHP_AUTH_DIGEST']; } return $headers; } } http-foundation/composer.json 0000644 00000002325 15025017654 0012420 0 ustar 00 { "name": "symfony/http-foundation", "type": "library", "description": "Defines an object-oriented layer for the HTTP specification", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.1" }, "require-dev": { "predis/predis": "~1.0", "symfony/cache": "^5.4|^6.0", "symfony/dependency-injection": "^5.4|^6.0", "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", "symfony/mime": "^5.4|^6.0", "symfony/expression-language": "^5.4|^6.0", "symfony/rate-limiter": "^5.2|^6.0" }, "suggest" : { "symfony/mime": "To use the file extension guesser" }, "autoload": { "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } http-foundation/StreamedResponse.php 0000644 00000005104 15025017654 0013670 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * StreamedResponse represents a streamed HTTP response. * * A StreamedResponse uses a callback for its content. * * The callback should use the standard PHP functions like echo * to stream the response back to the client. The flush() function * can also be used if needed. * * @see flush() * * @author Fabien Potencier <fabien@symfony.com> */ class StreamedResponse extends Response { protected $callback; protected $streamed; private bool $headersSent; public function __construct(callable $callback = null, int $status = 200, array $headers = []) { parent::__construct(null, $status, $headers); if (null !== $callback) { $this->setCallback($callback); } $this->streamed = false; $this->headersSent = false; } /** * Sets the PHP callback associated with this Response. * * @return $this */ public function setCallback(callable $callback): static { $this->callback = $callback; return $this; } /** * {@inheritdoc} * * This method only sends the headers once. * * @return $this */ public function sendHeaders(): static { if ($this->headersSent) { return $this; } $this->headersSent = true; return parent::sendHeaders(); } /** * {@inheritdoc} * * This method only sends the content once. * * @return $this */ public function sendContent(): static { if ($this->streamed) { return $this; } $this->streamed = true; if (null === $this->callback) { throw new \LogicException('The Response callback must not be null.'); } ($this->callback)(); return $this; } /** * {@inheritdoc} * * @throws \LogicException when the content is not null * * @return $this */ public function setContent(?string $content): static { if (null !== $content) { throw new \LogicException('The content cannot be set on a StreamedResponse instance.'); } $this->streamed = true; return $this; } /** * {@inheritdoc} */ public function getContent(): string|false { return false; } } http-foundation/CHANGELOG.md 0000644 00000041716 15025017654 0011516 0 ustar 00 CHANGELOG ========= 6.0 --- * Remove the `NamespacedAttributeBag` class * Removed `Response::create()`, `JsonResponse::create()`, `RedirectResponse::create()`, `StreamedResponse::create()` and `BinaryFileResponse::create()` methods (use `__construct()` instead) * Not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()` throws an `\InvalidArgumentException`; wrap your filter in a closure instead * Not passing a `Closure` together with `FILTER_CALLBACK` to `InputBag::filter()` throws an `\InvalidArgumentException`; wrap your filter in a closure instead * Removed the `Request::HEADER_X_FORWARDED_ALL` constant, use either `Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO` or `Request::HEADER_X_FORWARDED_AWS_ELB` or `Request::HEADER_X_FORWARDED_TRAEFIK`constants instead * Rename `RequestStack::getMasterRequest()` to `getMainRequest()` * Not passing `FILTER_REQUIRE_ARRAY` or `FILTER_FORCE_ARRAY` flags to `InputBag::filter()` when filtering an array will throw `BadRequestException` * Removed the `Request::HEADER_X_FORWARDED_ALL` constant * Retrieving non-scalar values using `InputBag::get()` will throw `BadRequestException` (use `InputBad::all()` instead to retrieve an array) * Passing non-scalar default value as the second argument `InputBag::get()` will throw `\InvalidArgumentException` * Passing non-scalar, non-array value as the second argument `InputBag::set()` will throw `\InvalidArgumentException` * Passing `null` as `$requestIp` to `IpUtils::__checkIp()`, `IpUtils::__checkIp4()` or `IpUtils::__checkIp6()` is not supported anymore. 5.4 --- * Deprecate passing `null` as `$requestIp` to `IpUtils::__checkIp()`, `IpUtils::__checkIp4()` or `IpUtils::__checkIp6()`, pass an empty string instead. * Add the `litespeed_finish_request` method to work with Litespeed * Deprecate `upload_progress.*` and `url_rewriter.tags` session options * Allow setting session options via DSN 5.3 --- * Add the `SessionFactory`, `NativeSessionStorageFactory`, `PhpBridgeSessionStorageFactory` and `MockFileSessionStorageFactory` classes * Calling `Request::getSession()` when there is no available session throws a `SessionNotFoundException` * Add the `RequestStack::getSession` method * Deprecate the `NamespacedAttributeBag` class * Add `ResponseFormatSame` PHPUnit constraint * Deprecate the `RequestStack::getMasterRequest()` method and add `getMainRequest()` as replacement 5.2.0 ----- * added support for `X-Forwarded-Prefix` header * added `HeaderUtils::parseQuery()`: it does the same as `parse_str()` but preserves dots in variable names * added `File::getContent()` * added ability to use comma separated ip addresses for `RequestMatcher::matchIps()` * added `Request::toArray()` to parse a JSON request body to an array * added `RateLimiter\RequestRateLimiterInterface` and `RateLimiter\AbstractRequestRateLimiter` * deprecated not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()`; wrap your filter in a closure instead. * Deprecated the `Request::HEADER_X_FORWARDED_ALL` constant, use either `HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO` or `HEADER_X_FORWARDED_AWS_ELB` or `HEADER_X_FORWARDED_TRAEFIK` constants instead. * Deprecated `BinaryFileResponse::create()`, use `__construct()` instead 5.1.0 ----- * added `Cookie::withValue`, `Cookie::withDomain`, `Cookie::withExpires`, `Cookie::withPath`, `Cookie::withSecure`, `Cookie::withHttpOnly`, `Cookie::withRaw`, `Cookie::withSameSite` * Deprecate `Response::create()`, `JsonResponse::create()`, `RedirectResponse::create()`, and `StreamedResponse::create()` methods (use `__construct()` instead) * added `Request::preferSafeContent()` and `Response::setContentSafe()` to handle "safe" HTTP preference according to [RFC 8674](https://tools.ietf.org/html/rfc8674) * made the Mime component an optional dependency * added `MarshallingSessionHandler`, `IdentityMarshaller` * made `Session` accept a callback to report when the session is being used * Add support for all core cache control directives * Added `Symfony\Component\HttpFoundation\InputBag` * Deprecated retrieving non-string values using `InputBag::get()`, use `InputBag::all()` if you need access to the collection of values 5.0.0 ----- * made `Cookie` auto-secure and lax by default * removed classes in the `MimeType` namespace, use the Symfony Mime component instead * removed method `UploadedFile::getClientSize()` and the related constructor argument * made `Request::getSession()` throw if the session has not been set before * removed `Response::HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL` * passing a null url when instantiating a `RedirectResponse` is not allowed 4.4.0 ----- * passing arguments to `Request::isMethodSafe()` is deprecated. * `ApacheRequest` is deprecated, use the `Request` class instead. * passing a third argument to `HeaderBag::get()` is deprecated, use method `all()` instead * [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column, make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to update your database. * `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column, make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database to speed up garbage collection of expired sessions. * added `SessionHandlerFactory` to create session handlers with a DSN * added `IpUtils::anonymize()` to help with GDPR compliance. 4.3.0 ----- * added PHPUnit constraints: `RequestAttributeValueSame`, `ResponseCookieValueSame`, `ResponseHasCookie`, `ResponseHasHeader`, `ResponseHeaderSame`, `ResponseIsRedirected`, `ResponseIsSuccessful`, and `ResponseStatusCodeSame` * deprecated `MimeTypeGuesserInterface` and `ExtensionGuesserInterface` in favor of `Symfony\Component\Mime\MimeTypesInterface`. * deprecated `MimeType` and `MimeTypeExtensionGuesser` in favor of `Symfony\Component\Mime\MimeTypes`. * deprecated `FileBinaryMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileBinaryMimeTypeGuesser`. * deprecated `FileinfoMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileinfoMimeTypeGuesser`. * added `UrlHelper` that allows to get an absolute URL and a relative path for a given path 4.2.0 ----- * the default value of the "$secure" and "$samesite" arguments of Cookie's constructor will respectively change from "false" to "null" and from "null" to "lax" in Symfony 5.0, you should define their values explicitly or use "Cookie::create()" instead. * added `matchPort()` in RequestMatcher 4.1.3 ----- * [BC BREAK] Support for the IIS-only `X_ORIGINAL_URL` and `X_REWRITE_URL` HTTP headers has been dropped for security reasons. 4.1.0 ----- * Query string normalization uses `parse_str()` instead of custom parsing logic. * Passing the file size to the constructor of the `UploadedFile` class is deprecated. * The `getClientSize()` method of the `UploadedFile` class is deprecated. Use `getSize()` instead. * added `RedisSessionHandler` to use Redis as a session storage * The `get()` method of the `AcceptHeader` class now takes into account the `*` and `*/*` default values (if they are present in the Accept HTTP header) when looking for items. * deprecated `Request::getSession()` when no session has been set. Use `Request::hasSession()` instead. * added `CannotWriteFileException`, `ExtensionFileException`, `FormSizeFileException`, `IniSizeFileException`, `NoFileException`, `NoTmpDirFileException`, `PartialFileException` to handle failed `UploadedFile`. * added `MigratingSessionHandler` for migrating between two session handlers without losing sessions * added `HeaderUtils`. 4.0.0 ----- * the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()` methods have been removed * the `Request::HEADER_CLIENT_IP` constant has been removed, use `Request::HEADER_X_FORWARDED_FOR` instead * the `Request::HEADER_CLIENT_HOST` constant has been removed, use `Request::HEADER_X_FORWARDED_HOST` instead * the `Request::HEADER_CLIENT_PROTO` constant has been removed, use `Request::HEADER_X_FORWARDED_PROTO` instead * the `Request::HEADER_CLIENT_PORT` constant has been removed, use `Request::HEADER_X_FORWARDED_PORT` instead * checking for cacheable HTTP methods using the `Request::isMethodSafe()` method (by not passing `false` as its argument) is not supported anymore and throws a `\BadMethodCallException` * the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes have been removed * setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()` is not supported anymore and throws a `\TypeError` 3.4.0 ----- * implemented PHP 7.0's `SessionUpdateTimestampHandlerInterface` with a new `AbstractSessionHandler` base class and a new `StrictSessionHandler` wrapper * deprecated the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes * deprecated setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()` * deprecated using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead * deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` instead 3.3.0 ----- * the `Request::setTrustedProxies()` method takes a new `$trustedHeaderSet` argument, see https://symfony.com/doc/current/deployment/proxies.html for more info, * deprecated the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()` methods, * added `File\Stream`, to be passed to `BinaryFileResponse` when the size of the served file is unknown, disabling `Range` and `Content-Length` handling, switching to chunked encoding instead * added the `Cookie::fromString()` method that allows to create a cookie from a raw header string 3.1.0 ----- * Added support for creating `JsonResponse` with a string of JSON data 3.0.0 ----- * The precedence of parameters returned from `Request::get()` changed from "GET, PATH, BODY" to "PATH, GET, BODY" 2.8.0 ----- * Finding deep items in `ParameterBag::get()` is deprecated since version 2.8 and will be removed in 3.0. 2.6.0 ----- * PdoSessionHandler changes - implemented different session locking strategies to prevent loss of data by concurrent access to the same session - [BC BREAK] save session data in a binary column without base64_encode - [BC BREAK] added lifetime column to the session table which allows to have different lifetimes for each session - implemented lazy connections that are only opened when a session is used by either passing a dsn string explicitly or falling back to session.save_path ini setting - added a createTable method that initializes a correctly defined table depending on the database vendor 2.5.0 ----- * added `JsonResponse::setEncodingOptions()` & `JsonResponse::getEncodingOptions()` for easier manipulation of the options used while encoding data to JSON format. 2.4.0 ----- * added RequestStack * added Request::getEncodings() * added accessors methods to session handlers 2.3.0 ----- * added support for ranges of IPs in trusted proxies * `UploadedFile::isValid` now returns false if the file was not uploaded via HTTP (in a non-test mode) * Improved error-handling of `\Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler` to ensure the supplied PDO handler throws Exceptions on error (as the class expects). Added related test cases to verify that Exceptions are properly thrown when the PDO queries fail. 2.2.0 ----- * fixed the Request::create() precedence (URI information always take precedence now) * added Request::getTrustedProxies() * deprecated Request::isProxyTrusted() * [BC BREAK] JsonResponse does not turn a top level empty array to an object anymore, use an ArrayObject to enforce objects * added a IpUtils class to check if an IP belongs to a CIDR * added Request::getRealMethod() to get the "real" HTTP method (getMethod() returns the "intended" HTTP method) * disabled _method request parameter support by default (call Request::enableHttpMethodParameterOverride() to enable it, and Request::getHttpMethodParameterOverride() to check if it is supported) * Request::splitHttpAcceptHeader() method is deprecated and will be removed in 2.3 * Deprecated Flashbag::count() and \Countable interface, will be removed in 2.3 2.1.0 ----- * added Request::getSchemeAndHttpHost() and Request::getUserInfo() * added a fluent interface to the Response class * added Request::isProxyTrusted() * added JsonResponse * added a getTargetUrl method to RedirectResponse * added support for streamed responses * made Response::prepare() method the place to enforce HTTP specification * [BC BREAK] moved management of the locale from the Session class to the Request class * added a generic access to the PHP built-in filter mechanism: ParameterBag::filter() * made FileBinaryMimeTypeGuesser command configurable * added Request::getUser() and Request::getPassword() * added support for the PATCH method in Request * removed the ContentTypeMimeTypeGuesser class as it is deprecated and never used on PHP 5.3 * added ResponseHeaderBag::makeDisposition() (implements RFC 6266) * made mimetype to extension conversion configurable * [BC BREAK] Moved all session related classes and interfaces into own namespace, as `Symfony\Component\HttpFoundation\Session` and renamed classes accordingly. Session handlers are located in the subnamespace `Symfony\Component\HttpFoundation\Session\Handler`. * SessionHandlers must implement `\SessionHandlerInterface` or extend from the `Symfony\Component\HttpFoundation\Storage\Handler\NativeSessionHandler` base class. * Added internal storage driver proxy mechanism for forward compatibility with PHP 5.4 `\SessionHandler` class. * Added session handlers for custom Memcache, Memcached and Null session save handlers. * [BC BREAK] Removed `NativeSessionStorage` and replaced with `NativeFileSessionHandler`. * [BC BREAK] `SessionStorageInterface` methods removed: `write()`, `read()` and `remove()`. Added `getBag()`, `registerBag()`. The `NativeSessionStorage` class is a mediator for the session storage internals including the session handlers which do the real work of participating in the internal PHP session workflow. * [BC BREAK] Introduced mock implementations of `SessionStorage` to enable unit and functional testing without starting real PHP sessions. Removed `ArraySessionStorage`, and replaced with `MockArraySessionStorage` for unit tests; removed `FilesystemSessionStorage`, and replaced with`MockFileSessionStorage` for functional tests. These do not interact with global session ini configuration values, session functions or `$_SESSION` superglobal. This means they can be configured directly allowing multiple instances to work without conflicting in the same PHP process. * [BC BREAK] Removed the `close()` method from the `Session` class, as this is now redundant. * Deprecated the following methods from the Session class: `setFlash()`, `setFlashes()` `getFlash()`, `hasFlash()`, and `removeFlash()`. Use `getFlashBag()` instead which returns a `FlashBagInterface`. * `Session->clear()` now only clears session attributes as before it cleared flash messages and attributes. `Session->getFlashBag()->all()` clears flashes now. * Session data is now managed by `SessionBagInterface` to better encapsulate session data. * Refactored session attribute and flash messages system to their own `SessionBagInterface` implementations. * Added `FlashBag`. Flashes expire when retrieved by `get()` or `all()`. This implementation is ESI compatible. * Added `AutoExpireFlashBag` (default) to replicate Symfony 2.0.x auto expire behavior of messages auto expiring after one page page load. Messages must be retrieved by `get()` or `all()`. * Added `Symfony\Component\HttpFoundation\Attribute\AttributeBag` to replicate attributes storage behavior from 2.0.x (default). * Added `Symfony\Component\HttpFoundation\Attribute\NamespacedAttributeBag` for namespace session attributes. * Flash API can stores messages in an array so there may be multiple messages per flash type. The old `Session` class API remains without BC break as it will allow single messages as before. * Added basic session meta-data to the session to record session create time, last updated time, and the lifetime of the session cookie that was provided to the client. * Request::getClientIp() method doesn't take a parameter anymore but bases itself on the trustProxy parameter. * Added isMethod() to Request object. * [BC BREAK] The methods `getPathInfo()`, `getBaseUrl()` and `getBasePath()` of a `Request` now all return a raw value (vs a urldecoded value before). Any call to one of these methods must be checked and wrapped in a `rawurldecode()` if needed. http-foundation/IpUtils.php 0000644 00000013624 15025017654 0012004 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * Http utility functions. * * @author Fabien Potencier <fabien@symfony.com> */ class IpUtils { private static array $checkedIps = []; /** * This class should not be instantiated. */ private function __construct() { } /** * Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets. * * @param string|array $ips List of IPs or subnets (can be a string if only a single one) */ public static function checkIp(string $requestIp, string|array $ips): bool { if (!\is_array($ips)) { $ips = [$ips]; } $method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4'; foreach ($ips as $ip) { if (self::$method($requestIp, $ip)) { return true; } } return false; } /** * Compares two IPv4 addresses. * In case a subnet is given, it checks if it contains the request IP. * * @param string $ip IPv4 address or subnet in CIDR notation * * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet */ public static function checkIp4(string $requestIp, string $ip): bool { $cacheKey = $requestIp.'-'.$ip; if (isset(self::$checkedIps[$cacheKey])) { return self::$checkedIps[$cacheKey]; } if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { return self::$checkedIps[$cacheKey] = false; } if (str_contains($ip, '/')) { [$address, $netmask] = explode('/', $ip, 2); if ('0' === $netmask) { return self::$checkedIps[$cacheKey] = false !== filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); } if ($netmask < 0 || $netmask > 32) { return self::$checkedIps[$cacheKey] = false; } } else { $address = $ip; $netmask = 32; } if (false === ip2long($address)) { return self::$checkedIps[$cacheKey] = false; } return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); } /** * Compares two IPv6 addresses. * In case a subnet is given, it checks if it contains the request IP. * * @author David Soria Parra <dsp at php dot net> * * @see https://github.com/dsp/v6tools * * @param string $ip IPv6 address or subnet in CIDR notation * * @throws \RuntimeException When IPV6 support is not enabled */ public static function checkIp6(string $requestIp, string $ip): bool { $cacheKey = $requestIp.'-'.$ip; if (isset(self::$checkedIps[$cacheKey])) { return self::$checkedIps[$cacheKey]; } if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) { throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".'); } // Check to see if we were given a IP4 $requestIp or $ip by mistake if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { return self::$checkedIps[$cacheKey] = false; } if (str_contains($ip, '/')) { [$address, $netmask] = explode('/', $ip, 2); if (!filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { return self::$checkedIps[$cacheKey] = false; } if ('0' === $netmask) { return (bool) unpack('n*', @inet_pton($address)); } if ($netmask < 1 || $netmask > 128) { return self::$checkedIps[$cacheKey] = false; } } else { if (!filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { return self::$checkedIps[$cacheKey] = false; } $address = $ip; $netmask = 128; } $bytesAddr = unpack('n*', @inet_pton($address)); $bytesTest = unpack('n*', @inet_pton($requestIp)); if (!$bytesAddr || !$bytesTest) { return self::$checkedIps[$cacheKey] = false; } for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { $left = $netmask - 16 * ($i - 1); $left = ($left <= 16) ? $left : 16; $mask = ~(0xFFFF >> $left) & 0xFFFF; if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) { return self::$checkedIps[$cacheKey] = false; } } return self::$checkedIps[$cacheKey] = true; } /** * Anonymizes an IP/IPv6. * * Removes the last byte for v4 and the last 8 bytes for v6 IPs */ public static function anonymize(string $ip): string { $wrappedIPv6 = false; if ('[' === substr($ip, 0, 1) && ']' === substr($ip, -1, 1)) { $wrappedIPv6 = true; $ip = substr($ip, 1, -1); } $packedAddress = inet_pton($ip); if (4 === \strlen($packedAddress)) { $mask = '255.255.255.0'; } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) { $mask = '::ffff:ffff:ff00'; } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) { $mask = '::ffff:ff00'; } else { $mask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; } $ip = inet_ntop($packedAddress & inet_pton($mask)); if ($wrappedIPv6) { $ip = '['.$ip.']'; } return $ip; } } http-foundation/Exception/SessionNotFoundException.php 0000644 00000001512 15025017654 0017321 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Exception; /** * Raised when a session does not exist. This happens in the following cases: * - the session is not enabled * - attempt to read a session outside a request context (ie. cli script). * * @author Jérémy Derussé <jeremy@derusse.com> */ class SessionNotFoundException extends \LogicException implements RequestExceptionInterface { public function __construct(string $message = 'There is currently no session available.', int $code = 0, \Throwable $previous = null) { parent::__construct($message, $code, $previous); } } http-foundation/Exception/BadRequestException.php 0000644 00000000703 15025017654 0016261 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Exception; /** * Raised when a user sends a malformed request. */ class BadRequestException extends \UnexpectedValueException implements RequestExceptionInterface { } http-foundation/Exception/ConflictingHeadersException.php 0000644 00000001017 15025017654 0017754 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Exception; /** * The HTTP request contains headers with conflicting information. * * @author Magnus Nordlander <magnus@fervo.se> */ class ConflictingHeadersException extends \UnexpectedValueException implements RequestExceptionInterface { } http-foundation/Exception/SuspiciousOperationException.php 0000644 00000001021 15025017654 0020243 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Exception; /** * Raised when a user has performed an operation that should be considered * suspicious from a security perspective. */ class SuspiciousOperationException extends \UnexpectedValueException implements RequestExceptionInterface { } http-foundation/Exception/RequestExceptionInterface.php 0000644 00000000744 15025017654 0017500 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Exception; /** * Interface for Request exceptions. * * Exceptions implementing this interface should trigger an HTTP 400 response in the application code. */ interface RequestExceptionInterface { } http-foundation/Exception/JsonException.php 0000644 00000001021 15025017654 0015125 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Exception; /** * Thrown by Request::toArray() when the content cannot be JSON-decoded. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ final class JsonException extends \UnexpectedValueException implements RequestExceptionInterface { } http-foundation/RequestStack.php 0000644 00000005162 15025017654 0013027 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; use Symfony\Component\HttpFoundation\Session\SessionInterface; /** * Request stack that controls the lifecycle of requests. * * @author Benjamin Eberlei <kontakt@beberlei.de> */ class RequestStack { /** * @var Request[] */ private array $requests = []; /** * Pushes a Request on the stack. * * This method should generally not be called directly as the stack * management should be taken care of by the application itself. */ public function push(Request $request) { $this->requests[] = $request; } /** * Pops the current request from the stack. * * This operation lets the current request go out of scope. * * This method should generally not be called directly as the stack * management should be taken care of by the application itself. */ public function pop(): ?Request { if (!$this->requests) { return null; } return array_pop($this->requests); } public function getCurrentRequest(): ?Request { return end($this->requests) ?: null; } /** * Gets the main request. * * Be warned that making your code aware of the main request * might make it un-compatible with other features of your framework * like ESI support. */ public function getMainRequest(): ?Request { if (!$this->requests) { return null; } return $this->requests[0]; } /** * Returns the parent request of the current. * * Be warned that making your code aware of the parent request * might make it un-compatible with other features of your framework * like ESI support. * * If current Request is the main request, it returns null. */ public function getParentRequest(): ?Request { $pos = \count($this->requests) - 2; return $this->requests[$pos] ?? null; } /** * Gets the current session. * * @throws SessionNotFoundException */ public function getSession(): SessionInterface { if ((null !== $request = end($this->requests) ?: null) && $request->hasSession()) { return $request->getSession(); } throw new SessionNotFoundException(); } } http-foundation/FileBag.php 0000644 00000007413 15025017654 0011703 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\File\UploadedFile; /** * FileBag is a container for uploaded files. * * @author Fabien Potencier <fabien@symfony.com> * @author Bulat Shakirzyanov <mallluhuct@gmail.com> */ class FileBag extends ParameterBag { private const FILE_KEYS = ['error', 'name', 'size', 'tmp_name', 'type']; /** * @param array|UploadedFile[] $parameters An array of HTTP files */ public function __construct(array $parameters = []) { $this->replace($parameters); } /** * {@inheritdoc} */ public function replace(array $files = []) { $this->parameters = []; $this->add($files); } /** * {@inheritdoc} */ public function set(string $key, mixed $value) { if (!\is_array($value) && !$value instanceof UploadedFile) { throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.'); } parent::set($key, $this->convertFileInformation($value)); } /** * {@inheritdoc} */ public function add(array $files = []) { foreach ($files as $key => $file) { $this->set($key, $file); } } /** * Converts uploaded files to UploadedFile instances. * * @return UploadedFile[]|UploadedFile|null */ protected function convertFileInformation(array|UploadedFile $file): array|UploadedFile|null { if ($file instanceof UploadedFile) { return $file; } $file = $this->fixPhpFilesArray($file); $keys = array_keys($file); sort($keys); if (self::FILE_KEYS == $keys) { if (\UPLOAD_ERR_NO_FILE == $file['error']) { $file = null; } else { $file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error'], false); } } else { $file = array_map(function ($v) { return $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v; }, $file); if (array_keys($keys) === $keys) { $file = array_filter($file); } } return $file; } /** * Fixes a malformed PHP $_FILES array. * * PHP has a bug that the format of the $_FILES array differs, depending on * whether the uploaded file fields had normal field names or array-like * field names ("normal" vs. "parent[child]"). * * This method fixes the array to look like the "normal" $_FILES array. * * It's safe to pass an already converted array, in which case this method * just returns the original array unmodified. */ protected function fixPhpFilesArray(array $data): array { // Remove extra key added by PHP 8.1. unset($data['full_path']); $keys = array_keys($data); sort($keys); if (self::FILE_KEYS != $keys || !isset($data['name']) || !\is_array($data['name'])) { return $data; } $files = $data; foreach (self::FILE_KEYS as $k) { unset($files[$k]); } foreach ($data['name'] as $key => $name) { $files[$key] = $this->fixPhpFilesArray([ 'error' => $data['error'][$key], 'name' => $name, 'type' => $data['type'][$key], 'tmp_name' => $data['tmp_name'][$key], 'size' => $data['size'][$key], ]); } return $files; } } http-foundation/UrlHelper.php 0000644 00000006026 15025017654 0012313 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; use Symfony\Component\Routing\RequestContext; /** * A helper service for manipulating URLs within and outside the request scope. * * @author Valentin Udaltsov <udaltsov.valentin@gmail.com> */ final class UrlHelper { private $requestStack; private $requestContext; public function __construct(RequestStack $requestStack, RequestContext $requestContext = null) { $this->requestStack = $requestStack; $this->requestContext = $requestContext; } public function getAbsoluteUrl(string $path): string { if (str_contains($path, '://') || '//' === substr($path, 0, 2)) { return $path; } if (null === $request = $this->requestStack->getMainRequest()) { return $this->getAbsoluteUrlFromContext($path); } if ('#' === $path[0]) { $path = $request->getRequestUri().$path; } elseif ('?' === $path[0]) { $path = $request->getPathInfo().$path; } if (!$path || '/' !== $path[0]) { $prefix = $request->getPathInfo(); $last = \strlen($prefix) - 1; if ($last !== $pos = strrpos($prefix, '/')) { $prefix = substr($prefix, 0, $pos).'/'; } return $request->getUriForPath($prefix.$path); } return $request->getSchemeAndHttpHost().$path; } public function getRelativePath(string $path): string { if (str_contains($path, '://') || '//' === substr($path, 0, 2)) { return $path; } if (null === $request = $this->requestStack->getMainRequest()) { return $path; } return $request->getRelativeUriForPath($path); } private function getAbsoluteUrlFromContext(string $path): string { if (null === $this->requestContext || '' === $host = $this->requestContext->getHost()) { return $path; } $scheme = $this->requestContext->getScheme(); $port = ''; if ('http' === $scheme && 80 !== $this->requestContext->getHttpPort()) { $port = ':'.$this->requestContext->getHttpPort(); } elseif ('https' === $scheme && 443 !== $this->requestContext->getHttpsPort()) { $port = ':'.$this->requestContext->getHttpsPort(); } if ('#' === $path[0]) { $queryString = $this->requestContext->getQueryString(); $path = $this->requestContext->getPathInfo().($queryString ? '?'.$queryString : '').$path; } elseif ('?' === $path[0]) { $path = $this->requestContext->getPathInfo().$path; } if ('/' !== $path[0]) { $path = rtrim($this->requestContext->getBaseUrl(), '/').'/'.$path; } return $scheme.'://'.$host.$port.$path; } } http-foundation/ParameterBag.php 0000644 00000011714 15025017654 0012743 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\Exception\BadRequestException; /** * ParameterBag is a container for key/value pairs. * * @author Fabien Potencier <fabien@symfony.com> * * @implements \IteratorAggregate<string, mixed> */ class ParameterBag implements \IteratorAggregate, \Countable { /** * Parameter storage. */ protected $parameters; public function __construct(array $parameters = []) { $this->parameters = $parameters; } /** * Returns the parameters. * * @param string|null $key The name of the parameter to return or null to get them all */ public function all(string $key = null): array { if (null === $key) { return $this->parameters; } if (!\is_array($value = $this->parameters[$key] ?? [])) { throw new BadRequestException(sprintf('Unexpected value for parameter "%s": expecting "array", got "%s".', $key, get_debug_type($value))); } return $value; } /** * Returns the parameter keys. */ public function keys(): array { return array_keys($this->parameters); } /** * Replaces the current parameters by a new set. */ public function replace(array $parameters = []) { $this->parameters = $parameters; } /** * Adds parameters. */ public function add(array $parameters = []) { $this->parameters = array_replace($this->parameters, $parameters); } public function get(string $key, mixed $default = null): mixed { return \array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default; } public function set(string $key, mixed $value) { $this->parameters[$key] = $value; } /** * Returns true if the parameter is defined. */ public function has(string $key): bool { return \array_key_exists($key, $this->parameters); } /** * Removes a parameter. */ public function remove(string $key) { unset($this->parameters[$key]); } /** * Returns the alphabetic characters of the parameter value. */ public function getAlpha(string $key, string $default = ''): string { return preg_replace('/[^[:alpha:]]/', '', $this->get($key, $default)); } /** * Returns the alphabetic characters and digits of the parameter value. */ public function getAlnum(string $key, string $default = ''): string { return preg_replace('/[^[:alnum:]]/', '', $this->get($key, $default)); } /** * Returns the digits of the parameter value. */ public function getDigits(string $key, string $default = ''): string { // we need to remove - and + because they're allowed in the filter return str_replace(['-', '+'], '', $this->filter($key, $default, \FILTER_SANITIZE_NUMBER_INT)); } /** * Returns the parameter value converted to integer. */ public function getInt(string $key, int $default = 0): int { return (int) $this->get($key, $default); } /** * Returns the parameter value converted to boolean. */ public function getBoolean(string $key, bool $default = false): bool { return $this->filter($key, $default, \FILTER_VALIDATE_BOOLEAN); } /** * Filter key. * * @param int $filter FILTER_* constant * * @see https://php.net/filter-var */ public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed { $value = $this->get($key, $default); // Always turn $options into an array - this allows filter_var option shortcuts. if (!\is_array($options) && $options) { $options = ['flags' => $options]; } // Add a convenience check for arrays. if (\is_array($value) && !isset($options['flags'])) { $options['flags'] = \FILTER_REQUIRE_ARRAY; } if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); } return filter_var($value, $filter, $options); } /** * Returns an iterator for parameters. * * @return \ArrayIterator<string, mixed> */ public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->parameters); } /** * Returns the number of parameters. */ public function count(): int { return \count($this->parameters); } } http-foundation/InputBag.php 0000644 00000006504 15025017654 0012123 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\Exception\BadRequestException; /** * InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE. * * @author Saif Eddin Gmati <azjezz@protonmail.com> */ final class InputBag extends ParameterBag { /** * Returns a scalar input value by name. * * @param string|int|float|bool|null $default The default value if the input key does not exist */ public function get(string $key, mixed $default = null): string|int|float|bool|null { if (null !== $default && !\is_scalar($default) && !$default instanceof \Stringable) { throw new \InvalidArgumentException(sprintf('Expected a scalar value as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($default))); } $value = parent::get($key, $this); if (null !== $value && $this !== $value && !\is_scalar($value) && !$value instanceof \Stringable) { throw new BadRequestException(sprintf('Input value "%s" contains a non-scalar value.', $key)); } return $this === $value ? $default : $value; } /** * Replaces the current input values by a new set. */ public function replace(array $inputs = []) { $this->parameters = []; $this->add($inputs); } /** * Adds input values. */ public function add(array $inputs = []) { foreach ($inputs as $input => $value) { $this->set($input, $value); } } /** * Sets an input by name. * * @param string|int|float|bool|array|null $value */ public function set(string $key, mixed $value) { if (null !== $value && !\is_scalar($value) && !\is_array($value) && !$value instanceof \Stringable) { throw new \InvalidArgumentException(sprintf('Expected a scalar, or an array as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($value))); } $this->parameters[$key] = $value; } /** * {@inheritdoc} */ public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed { $value = $this->has($key) ? $this->all()[$key] : $default; // Always turn $options into an array - this allows filter_var option shortcuts. if (!\is_array($options) && $options) { $options = ['flags' => $options]; } if (\is_array($value) && !(($options['flags'] ?? 0) & (\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY))) { throw new BadRequestException(sprintf('Input value "%s" contains an array, but "FILTER_REQUIRE_ARRAY" or "FILTER_FORCE_ARRAY" flags were not set.', $key)); } if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); } return filter_var($value, $filter, $options); } } http-foundation/README.md 0000644 00000001654 15025017654 0011161 0 ustar 00 HttpFoundation Component ======================== The HttpFoundation component defines an object-oriented layer for the HTTP specification. Sponsor ------- The HttpFoundation component for Symfony 5.4/6.0 is [backed][1] by [Laravel][2]. Laravel is a PHP web development framework that is passionate about maximum developer happiness. Laravel is built using a variety of bespoke and Symfony based components. Help Symfony by [sponsoring][3] its development! Resources --------- * [Documentation](https://symfony.com/doc/current/components/http_foundation.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) [1]: https://symfony.com/backers [2]: https://laravel.com/ [3]: https://symfony.com/sponsor http-foundation/Test/Constraint/ResponseHasHeader.php 0000644 00000002154 15025017654 0017015 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\HttpFoundation\Response; final class ResponseHasHeader extends Constraint { private string $headerName; public function __construct(string $headerName) { $this->headerName = $headerName; } /** * {@inheritdoc} */ public function toString(): string { return sprintf('has header "%s"', $this->headerName); } /** * @param Response $response * * {@inheritdoc} */ protected function matches($response): bool { return $response->headers->has($this->headerName); } /** * @param Response $response * * {@inheritdoc} */ protected function failureDescription($response): string { return 'the Response '.$this->toString(); } } http-foundation/Test/Constraint/ResponseIsUnprocessable.php 0000644 00000002220 15025017654 0020264 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\HttpFoundation\Response; final class ResponseIsUnprocessable extends Constraint { /** * {@inheritdoc} */ public function toString(): string { return 'is unprocessable'; } /** * @param Response $other * * {@inheritdoc} */ protected function matches($other): bool { return Response::HTTP_UNPROCESSABLE_ENTITY === $other->getStatusCode(); } /** * @param Response $other * * {@inheritdoc} */ protected function failureDescription($other): string { return 'the Response '.$this->toString(); } /** * @param Response $other * * {@inheritdoc} */ protected function additionalFailureDescription($other): string { return (string) $other; } } http-foundation/Test/Constraint/ResponseHeaderSame.php 0000644 00000002433 15025017654 0017167 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\HttpFoundation\Response; final class ResponseHeaderSame extends Constraint { private string $headerName; private string $expectedValue; public function __construct(string $headerName, string $expectedValue) { $this->headerName = $headerName; $this->expectedValue = $expectedValue; } /** * {@inheritdoc} */ public function toString(): string { return sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue); } /** * @param Response $response * * {@inheritdoc} */ protected function matches($response): bool { return $this->expectedValue === $response->headers->get($this->headerName, null); } /** * @param Response $response * * {@inheritdoc} */ protected function failureDescription($response): string { return 'the Response '.$this->toString(); } } http-foundation/Test/Constraint/ResponseHasCookie.php 0000644 00000003615 15025017654 0017041 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Response; final class ResponseHasCookie extends Constraint { private string $name; private string $path; private ?string $domain; public function __construct(string $name, string $path = '/', string $domain = null) { $this->name = $name; $this->path = $path; $this->domain = $domain; } /** * {@inheritdoc} */ public function toString(): string { $str = sprintf('has cookie "%s"', $this->name); if ('/' !== $this->path) { $str .= sprintf(' with path "%s"', $this->path); } if ($this->domain) { $str .= sprintf(' for domain "%s"', $this->domain); } return $str; } /** * @param Response $response * * {@inheritdoc} */ protected function matches($response): bool { return null !== $this->getCookie($response); } /** * @param Response $response * * {@inheritdoc} */ protected function failureDescription($response): string { return 'the Response '.$this->toString(); } private function getCookie(Response $response): ?Cookie { $cookies = $response->headers->getCookies(); $filteredCookies = array_filter($cookies, function (Cookie $cookie) { return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain; }); return reset($filteredCookies) ?: null; } } http-foundation/Test/Constraint/ResponseIsRedirected.php 0000644 00000002167 15025017654 0017543 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\HttpFoundation\Response; final class ResponseIsRedirected extends Constraint { /** * {@inheritdoc} */ public function toString(): string { return 'is redirected'; } /** * @param Response $response * * {@inheritdoc} */ protected function matches($response): bool { return $response->isRedirect(); } /** * @param Response $response * * {@inheritdoc} */ protected function failureDescription($response): string { return 'the Response '.$this->toString(); } /** * @param Response $response * * {@inheritdoc} */ protected function additionalFailureDescription($response): string { return (string) $response; } } http-foundation/Test/Constraint/RequestAttributeValueSame.php 0000644 00000002305 15025017654 0020567 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\HttpFoundation\Request; final class RequestAttributeValueSame extends Constraint { private string $name; private string $value; public function __construct(string $name, string $value) { $this->name = $name; $this->value = $value; } /** * {@inheritdoc} */ public function toString(): string { return sprintf('has attribute "%s" with value "%s"', $this->name, $this->value); } /** * @param Request $request * * {@inheritdoc} */ protected function matches($request): bool { return $this->value === $request->attributes->get($this->name); } /** * @param Request $request * * {@inheritdoc} */ protected function failureDescription($request): string { return 'the Request '.$this->toString(); } } http-foundation/Test/Constraint/ResponseCookieValueSame.php 0000644 00000004216 15025017654 0020206 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Response; final class ResponseCookieValueSame extends Constraint { private string $name; private string $value; private string $path; private ?string $domain; public function __construct(string $name, string $value, string $path = '/', string $domain = null) { $this->name = $name; $this->value = $value; $this->path = $path; $this->domain = $domain; } /** * {@inheritdoc} */ public function toString(): string { $str = sprintf('has cookie "%s"', $this->name); if ('/' !== $this->path) { $str .= sprintf(' with path "%s"', $this->path); } if ($this->domain) { $str .= sprintf(' for domain "%s"', $this->domain); } $str .= sprintf(' with value "%s"', $this->value); return $str; } /** * @param Response $response * * {@inheritdoc} */ protected function matches($response): bool { $cookie = $this->getCookie($response); if (!$cookie) { return false; } return $this->value === (string) $cookie->getValue(); } /** * @param Response $response * * {@inheritdoc} */ protected function failureDescription($response): string { return 'the Response '.$this->toString(); } protected function getCookie(Response $response): ?Cookie { $cookies = $response->headers->getCookies(); $filteredCookies = array_filter($cookies, function (Cookie $cookie) { return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain; }); return reset($filteredCookies) ?: null; } } http-foundation/Test/Constraint/ResponseStatusCodeSame.php 0000644 00000002453 15025017654 0020057 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\HttpFoundation\Response; final class ResponseStatusCodeSame extends Constraint { private int $statusCode; public function __construct(int $statusCode) { $this->statusCode = $statusCode; } /** * {@inheritdoc} */ public function toString(): string { return 'status code is '.$this->statusCode; } /** * @param Response $response * * {@inheritdoc} */ protected function matches($response): bool { return $this->statusCode === $response->getStatusCode(); } /** * @param Response $response * * {@inheritdoc} */ protected function failureDescription($response): string { return 'the Response '.$this->toString(); } /** * @param Response $response * * {@inheritdoc} */ protected function additionalFailureDescription($response): string { return (string) $response; } } http-foundation/Test/Constraint/ResponseFormatSame.php 0000644 00000003055 15025017654 0017230 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * Asserts that the response is in the given format. * * @author Kévin Dunglas <dunglas@gmail.com> */ final class ResponseFormatSame extends Constraint { private $request; private ?string $format; public function __construct(Request $request, ?string $format) { $this->request = $request; $this->format = $format; } /** * {@inheritdoc} */ public function toString(): string { return 'format is '.($this->format ?? 'null'); } /** * @param Response $response * * {@inheritdoc} */ protected function matches($response): bool { return $this->format === $this->request->getFormat($response->headers->get('Content-Type')); } /** * @param Response $response * * {@inheritdoc} */ protected function failureDescription($response): string { return 'the Response '.$this->toString(); } /** * @param Response $response * * {@inheritdoc} */ protected function additionalFailureDescription($response): string { return (string) $response; } } http-foundation/Test/Constraint/ResponseIsSuccessful.php 0000644 00000002171 15025017654 0017603 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\HttpFoundation\Response; final class ResponseIsSuccessful extends Constraint { /** * {@inheritdoc} */ public function toString(): string { return 'is successful'; } /** * @param Response $response * * {@inheritdoc} */ protected function matches($response): bool { return $response->isSuccessful(); } /** * @param Response $response * * {@inheritdoc} */ protected function failureDescription($response): string { return 'the Response '.$this->toString(); } /** * @param Response $response * * {@inheritdoc} */ protected function additionalFailureDescription($response): string { return (string) $response; } } http-foundation/LICENSE 0000644 00000002051 15025017654 0010677 0 ustar 00 Copyright (c) 2004-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. http-foundation/RequestMatcherInterface.php 0000644 00000001155 15025017654 0015164 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * RequestMatcherInterface is an interface for strategies to match a Request. * * @author Fabien Potencier <fabien@symfony.com> */ interface RequestMatcherInterface { /** * Decides whether the rule(s) implemented by the strategy matches the supplied request. */ public function matches(Request $request): bool; } http-foundation/RequestMatcher.php 0000644 00000011526 15025017654 0013346 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * RequestMatcher compares a pre-defined set of checks against a Request instance. * * @author Fabien Potencier <fabien@symfony.com> */ class RequestMatcher implements RequestMatcherInterface { private ?string $path = null; private ?string $host = null; private ?int $port = null; /** * @var string[] */ private array $methods = []; /** * @var string[] */ private array $ips = []; /** * @var string[] */ private array $attributes = []; /** * @var string[] */ private array $schemes = []; /** * @param string|string[]|null $methods * @param string|string[]|null $ips * @param string|string[]|null $schemes */ public function __construct(string $path = null, string $host = null, string|array $methods = null, string|array $ips = null, array $attributes = [], string|array $schemes = null, int $port = null) { $this->matchPath($path); $this->matchHost($host); $this->matchMethod($methods); $this->matchIps($ips); $this->matchScheme($schemes); $this->matchPort($port); foreach ($attributes as $k => $v) { $this->matchAttribute($k, $v); } } /** * Adds a check for the HTTP scheme. * * @param string|string[]|null $scheme An HTTP scheme or an array of HTTP schemes */ public function matchScheme(string|array|null $scheme) { $this->schemes = null !== $scheme ? array_map('strtolower', (array) $scheme) : []; } /** * Adds a check for the URL host name. */ public function matchHost(?string $regexp) { $this->host = $regexp; } /** * Adds a check for the the URL port. * * @param int|null $port The port number to connect to */ public function matchPort(?int $port) { $this->port = $port; } /** * Adds a check for the URL path info. */ public function matchPath(?string $regexp) { $this->path = $regexp; } /** * Adds a check for the client IP. * * @param string $ip A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 */ public function matchIp(string $ip) { $this->matchIps($ip); } /** * Adds a check for the client IP. * * @param string|string[]|null $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 */ public function matchIps(string|array|null $ips) { $ips = null !== $ips ? (array) $ips : []; $this->ips = array_reduce($ips, static function (array $ips, string $ip) { return array_merge($ips, preg_split('/\s*,\s*/', $ip)); }, []); } /** * Adds a check for the HTTP method. * * @param string|string[]|null $method An HTTP method or an array of HTTP methods */ public function matchMethod(string|array|null $method) { $this->methods = null !== $method ? array_map('strtoupper', (array) $method) : []; } /** * Adds a check for request attribute. */ public function matchAttribute(string $key, string $regexp) { $this->attributes[$key] = $regexp; } /** * {@inheritdoc} */ public function matches(Request $request): bool { if ($this->schemes && !\in_array($request->getScheme(), $this->schemes, true)) { return false; } if ($this->methods && !\in_array($request->getMethod(), $this->methods, true)) { return false; } foreach ($this->attributes as $key => $pattern) { $requestAttribute = $request->attributes->get($key); if (!\is_string($requestAttribute)) { return false; } if (!preg_match('{'.$pattern.'}', $requestAttribute)) { return false; } } if (null !== $this->path && !preg_match('{'.$this->path.'}', rawurldecode($request->getPathInfo()))) { return false; } if (null !== $this->host && !preg_match('{'.$this->host.'}i', $request->getHost())) { return false; } if (null !== $this->port && 0 < $this->port && $request->getPort() !== $this->port) { return false; } if (IpUtils::checkIp($request->getClientIp() ?? '', $this->ips)) { return true; } // Note to future implementors: add additional checks above the // foreach above or else your check might not be run! return 0 === \count($this->ips); } } http-foundation/RedirectResponse.php 0000644 00000004746 15025017654 0013700 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * RedirectResponse represents an HTTP response doing a redirect. * * @author Fabien Potencier <fabien@symfony.com> */ class RedirectResponse extends Response { protected $targetUrl; /** * Creates a redirect response so that it conforms to the rules defined for a redirect status code. * * @param string $url The URL to redirect to. The URL should be a full URL, with schema etc., * but practically every browser redirects on paths only as well * @param int $status The status code (302 by default) * @param array $headers The headers (Location is always set to the given URL) * * @throws \InvalidArgumentException * * @see https://tools.ietf.org/html/rfc2616#section-10.3 */ public function __construct(string $url, int $status = 302, array $headers = []) { parent::__construct('', $status, $headers); $this->setTargetUrl($url); if (!$this->isRedirect()) { throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $status)); } if (301 == $status && !\array_key_exists('cache-control', array_change_key_case($headers, \CASE_LOWER))) { $this->headers->remove('cache-control'); } } /** * Returns the target URL. */ public function getTargetUrl(): string { return $this->targetUrl; } /** * Sets the redirect target of this response. * * @return $this * * @throws \InvalidArgumentException */ public function setTargetUrl(string $url): static { if ('' === $url) { throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); } $this->targetUrl = $url; $this->setContent( sprintf('<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta http-equiv="refresh" content="0;url=\'%1$s\'" /> <title>Redirecting to %1$s</title> </head> <body> Redirecting to <a href="%1$s">%1$s</a>. </body> </html>', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8'))); $this->headers->set('Location', $url); return $this; } } http-foundation/Cookie.php 0000644 00000025611 15025017654 0011623 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * Represents a cookie. * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ class Cookie { public const SAMESITE_NONE = 'none'; public const SAMESITE_LAX = 'lax'; public const SAMESITE_STRICT = 'strict'; protected $name; protected $value; protected $domain; protected $expire; protected $path; protected $secure; protected $httpOnly; private bool $raw; private ?string $sameSite = null; private bool $secureDefault = false; private const RESERVED_CHARS_LIST = "=,; \t\r\n\v\f"; private const RESERVED_CHARS_FROM = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"]; private const RESERVED_CHARS_TO = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C']; /** * Creates cookie from raw header string. */ public static function fromString(string $cookie, bool $decode = false): static { $data = [ 'expires' => 0, 'path' => '/', 'domain' => null, 'secure' => false, 'httponly' => false, 'raw' => !$decode, 'samesite' => null, ]; $parts = HeaderUtils::split($cookie, ';='); $part = array_shift($parts); $name = $decode ? urldecode($part[0]) : $part[0]; $value = isset($part[1]) ? ($decode ? urldecode($part[1]) : $part[1]) : null; $data = HeaderUtils::combine($parts) + $data; $data['expires'] = self::expiresTimestamp($data['expires']); if (isset($data['max-age']) && ($data['max-age'] > 0 || $data['expires'] > time())) { $data['expires'] = time() + (int) $data['max-age']; } return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']); } public static function create(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self { return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite); } /** * @param string $name The name of the cookie * @param string|null $value The value of the cookie * @param int|string|\DateTimeInterface $expire The time the cookie expires * @param string $path The path on the server in which the cookie will be available on * @param string|null $domain The domain that the cookie is available to * @param bool|null $secure Whether the client should send back the cookie only over HTTPS or null to auto-enable this when the request is already using HTTPS * @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol * @param bool $raw Whether the cookie value should be sent with no url encoding * @param string|null $sameSite Whether the cookie will be available for cross-site requests * * @throws \InvalidArgumentException */ public function __construct(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = 'lax') { // from PHP source code if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) { throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name)); } if (empty($name)) { throw new \InvalidArgumentException('The cookie name cannot be empty.'); } $this->name = $name; $this->value = $value; $this->domain = $domain; $this->expire = self::expiresTimestamp($expire); $this->path = empty($path) ? '/' : $path; $this->secure = $secure; $this->httpOnly = $httpOnly; $this->raw = $raw; $this->sameSite = $this->withSameSite($sameSite)->sameSite; } /** * Creates a cookie copy with a new value. */ public function withValue(?string $value): static { $cookie = clone $this; $cookie->value = $value; return $cookie; } /** * Creates a cookie copy with a new domain that the cookie is available to. */ public function withDomain(?string $domain): static { $cookie = clone $this; $cookie->domain = $domain; return $cookie; } /** * Creates a cookie copy with a new time the cookie expires. */ public function withExpires(int|string|\DateTimeInterface $expire = 0): static { $cookie = clone $this; $cookie->expire = self::expiresTimestamp($expire); return $cookie; } /** * Converts expires formats to a unix timestamp. */ private static function expiresTimestamp(int|string|\DateTimeInterface $expire = 0): int { // convert expiration time to a Unix timestamp if ($expire instanceof \DateTimeInterface) { $expire = $expire->format('U'); } elseif (!is_numeric($expire)) { $expire = strtotime($expire); if (false === $expire) { throw new \InvalidArgumentException('The cookie expiration time is not valid.'); } } return 0 < $expire ? (int) $expire : 0; } /** * Creates a cookie copy with a new path on the server in which the cookie will be available on. */ public function withPath(string $path): static { $cookie = clone $this; $cookie->path = '' === $path ? '/' : $path; return $cookie; } /** * Creates a cookie copy that only be transmitted over a secure HTTPS connection from the client. */ public function withSecure(bool $secure = true): static { $cookie = clone $this; $cookie->secure = $secure; return $cookie; } /** * Creates a cookie copy that be accessible only through the HTTP protocol. */ public function withHttpOnly(bool $httpOnly = true): static { $cookie = clone $this; $cookie->httpOnly = $httpOnly; return $cookie; } /** * Creates a cookie copy that uses no url encoding. */ public function withRaw(bool $raw = true): static { if ($raw && false !== strpbrk($this->name, self::RESERVED_CHARS_LIST)) { throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $this->name)); } $cookie = clone $this; $cookie->raw = $raw; return $cookie; } /** * Creates a cookie copy with SameSite attribute. */ public function withSameSite(?string $sameSite): static { if ('' === $sameSite) { $sameSite = null; } elseif (null !== $sameSite) { $sameSite = strtolower($sameSite); } if (!\in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE, null], true)) { throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.'); } $cookie = clone $this; $cookie->sameSite = $sameSite; return $cookie; } /** * Returns the cookie as a string. */ public function __toString(): string { if ($this->isRaw()) { $str = $this->getName(); } else { $str = str_replace(self::RESERVED_CHARS_FROM, self::RESERVED_CHARS_TO, $this->getName()); } $str .= '='; if ('' === (string) $this->getValue()) { $str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0'; } else { $str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue()); if (0 !== $this->getExpiresTime()) { $str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge(); } } if ($this->getPath()) { $str .= '; path='.$this->getPath(); } if ($this->getDomain()) { $str .= '; domain='.$this->getDomain(); } if (true === $this->isSecure()) { $str .= '; secure'; } if (true === $this->isHttpOnly()) { $str .= '; httponly'; } if (null !== $this->getSameSite()) { $str .= '; samesite='.$this->getSameSite(); } return $str; } /** * Gets the name of the cookie. */ public function getName(): string { return $this->name; } /** * Gets the value of the cookie. */ public function getValue(): ?string { return $this->value; } /** * Gets the domain that the cookie is available to. */ public function getDomain(): ?string { return $this->domain; } /** * Gets the time the cookie expires. */ public function getExpiresTime(): int { return $this->expire; } /** * Gets the max-age attribute. */ public function getMaxAge(): int { $maxAge = $this->expire - time(); return 0 >= $maxAge ? 0 : $maxAge; } /** * Gets the path on the server in which the cookie will be available on. */ public function getPath(): string { return $this->path; } /** * Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client. */ public function isSecure(): bool { return $this->secure ?? $this->secureDefault; } /** * Checks whether the cookie will be made accessible only through the HTTP protocol. */ public function isHttpOnly(): bool { return $this->httpOnly; } /** * Whether this cookie is about to be cleared. */ public function isCleared(): bool { return 0 !== $this->expire && $this->expire < time(); } /** * Checks if the cookie value should be sent with no url encoding. */ public function isRaw(): bool { return $this->raw; } /** * Gets the SameSite attribute. */ public function getSameSite(): ?string { return $this->sameSite; } /** * @param bool $default The default value of the "secure" flag when it is set to null */ public function setSecureDefault(bool $default): void { $this->secureDefault = $default; } } http-foundation/HeaderBag.php 0000644 00000015433 15025017654 0012215 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * HeaderBag is a container for HTTP headers. * * @author Fabien Potencier <fabien@symfony.com> * * @implements \IteratorAggregate<string, list<string|null>> */ class HeaderBag implements \IteratorAggregate, \Countable { protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ'; protected const LOWER = '-abcdefghijklmnopqrstuvwxyz'; /** * @var array<string, list<string|null>> */ protected $headers = []; protected $cacheControl = []; public function __construct(array $headers = []) { foreach ($headers as $key => $values) { $this->set($key, $values); } } /** * Returns the headers as a string. */ public function __toString(): string { if (!$headers = $this->all()) { return ''; } ksort($headers); $max = max(array_map('strlen', array_keys($headers))) + 1; $content = ''; foreach ($headers as $name => $values) { $name = ucwords($name, '-'); foreach ($values as $value) { $content .= sprintf("%-{$max}s %s\r\n", $name.':', $value); } } return $content; } /** * Returns the headers. * * @param string|null $key The name of the headers to return or null to get them all * * @return array<string, array<int, string|null>>|array<int, string|null> */ public function all(string $key = null): array { if (null !== $key) { return $this->headers[strtr($key, self::UPPER, self::LOWER)] ?? []; } return $this->headers; } /** * Returns the parameter keys. * * @return string[] */ public function keys(): array { return array_keys($this->all()); } /** * Replaces the current HTTP headers by a new set. */ public function replace(array $headers = []) { $this->headers = []; $this->add($headers); } /** * Adds new headers the current HTTP headers set. */ public function add(array $headers) { foreach ($headers as $key => $values) { $this->set($key, $values); } } /** * Returns the first header by name or the default one. */ public function get(string $key, string $default = null): ?string { $headers = $this->all($key); if (!$headers) { return $default; } if (null === $headers[0]) { return null; } return (string) $headers[0]; } /** * Sets a header by name. * * @param string|string[]|null $values The value or an array of values * @param bool $replace Whether to replace the actual value or not (true by default) */ public function set(string $key, string|array|null $values, bool $replace = true) { $key = strtr($key, self::UPPER, self::LOWER); if (\is_array($values)) { $values = array_values($values); if (true === $replace || !isset($this->headers[$key])) { $this->headers[$key] = $values; } else { $this->headers[$key] = array_merge($this->headers[$key], $values); } } else { if (true === $replace || !isset($this->headers[$key])) { $this->headers[$key] = [$values]; } else { $this->headers[$key][] = $values; } } if ('cache-control' === $key) { $this->cacheControl = $this->parseCacheControl(implode(', ', $this->headers[$key])); } } /** * Returns true if the HTTP header is defined. */ public function has(string $key): bool { return \array_key_exists(strtr($key, self::UPPER, self::LOWER), $this->all()); } /** * Returns true if the given HTTP header contains the given value. */ public function contains(string $key, string $value): bool { return \in_array($value, $this->all($key)); } /** * Removes a header. */ public function remove(string $key) { $key = strtr($key, self::UPPER, self::LOWER); unset($this->headers[$key]); if ('cache-control' === $key) { $this->cacheControl = []; } } /** * Returns the HTTP header value converted to a date. * * @throws \RuntimeException When the HTTP header is not parseable */ public function getDate(string $key, \DateTime $default = null): ?\DateTimeInterface { if (null === $value = $this->get($key)) { return $default; } if (false === $date = \DateTime::createFromFormat(\DATE_RFC2822, $value)) { throw new \RuntimeException(sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value)); } return $date; } /** * Adds a custom Cache-Control directive. */ public function addCacheControlDirective(string $key, bool|string $value = true) { $this->cacheControl[$key] = $value; $this->set('Cache-Control', $this->getCacheControlHeader()); } /** * Returns true if the Cache-Control directive is defined. */ public function hasCacheControlDirective(string $key): bool { return \array_key_exists($key, $this->cacheControl); } /** * Returns a Cache-Control directive value by name. */ public function getCacheControlDirective(string $key): bool|string|null { return $this->cacheControl[$key] ?? null; } /** * Removes a Cache-Control directive. */ public function removeCacheControlDirective(string $key) { unset($this->cacheControl[$key]); $this->set('Cache-Control', $this->getCacheControlHeader()); } /** * Returns an iterator for headers. * * @return \ArrayIterator<string, list<string|null>> */ public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->headers); } /** * Returns the number of headers. */ public function count(): int { return \count($this->headers); } protected function getCacheControlHeader() { ksort($this->cacheControl); return HeaderUtils::toString($this->cacheControl, ','); } /** * Parses a Cache-Control HTTP header. */ protected function parseCacheControl(string $header): array { $parts = HeaderUtils::split($header, ',='); return HeaderUtils::combine($parts); } } http-foundation/ExpressionRequestMatcher.php 0000644 00000002636 15025017654 0015430 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; /** * ExpressionRequestMatcher uses an expression to match a Request. * * @author Fabien Potencier <fabien@symfony.com> */ class ExpressionRequestMatcher extends RequestMatcher { private $language; private $expression; public function setExpression(ExpressionLanguage $language, Expression|string $expression) { $this->language = $language; $this->expression = $expression; } public function matches(Request $request): bool { if (!isset($this->language)) { throw new \LogicException('Unable to match the request as the expression language is not available.'); } return $this->language->evaluate($this->expression, [ 'request' => $request, 'method' => $request->getMethod(), 'path' => rawurldecode($request->getPathInfo()), 'host' => $request->getHost(), 'ip' => $request->getClientIp(), 'attributes' => $request->attributes->all(), ]) && parent::matches($request); } } http-foundation/HeaderUtils.php 0000644 00000022175 15025017654 0012625 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; /** * HTTP header utility functions. * * @author Christian Schmidt <github@chsc.dk> */ class HeaderUtils { public const DISPOSITION_ATTACHMENT = 'attachment'; public const DISPOSITION_INLINE = 'inline'; /** * This class should not be instantiated. */ private function __construct() { } /** * Splits an HTTP header by one or more separators. * * Example: * * HeaderUtils::split("da, en-gb;q=0.8", ",;") * // => ['da'], ['en-gb', 'q=0.8']] * * @param string $separators List of characters to split on, ordered by * precedence, e.g. ",", ";=", or ",;=" * * @return array Nested array with as many levels as there are characters in * $separators */ public static function split(string $header, string $separators): array { $quotedSeparators = preg_quote($separators, '/'); preg_match_all(' / (?!\s) (?: # quoted-string "(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$) | # token [^"'.$quotedSeparators.']+ )+ (?<!\s) | # separator \s* (?<separator>['.$quotedSeparators.']) \s* /x', trim($header), $matches, \PREG_SET_ORDER); return self::groupParts($matches, $separators); } /** * Combines an array of arrays into one associative array. * * Each of the nested arrays should have one or two elements. The first * value will be used as the keys in the associative array, and the second * will be used as the values, or true if the nested array only contains one * element. Array keys are lowercased. * * Example: * * HeaderUtils::combine([["foo", "abc"], ["bar"]]) * // => ["foo" => "abc", "bar" => true] */ public static function combine(array $parts): array { $assoc = []; foreach ($parts as $part) { $name = strtolower($part[0]); $value = $part[1] ?? true; $assoc[$name] = $value; } return $assoc; } /** * Joins an associative array into a string for use in an HTTP header. * * The key and value of each entry are joined with "=", and all entries * are joined with the specified separator and an additional space (for * readability). Values are quoted if necessary. * * Example: * * HeaderUtils::toString(["foo" => "abc", "bar" => true, "baz" => "a b c"], ",") * // => 'foo=abc, bar, baz="a b c"' */ public static function toString(array $assoc, string $separator): string { $parts = []; foreach ($assoc as $name => $value) { if (true === $value) { $parts[] = $name; } else { $parts[] = $name.'='.self::quote($value); } } return implode($separator.' ', $parts); } /** * Encodes a string as a quoted string, if necessary. * * If a string contains characters not allowed by the "token" construct in * the HTTP specification, it is backslash-escaped and enclosed in quotes * to match the "quoted-string" construct. */ public static function quote(string $s): string { if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) { return $s; } return '"'.addcslashes($s, '"\\"').'"'; } /** * Decodes a quoted string. * * If passed an unquoted string that matches the "token" construct (as * defined in the HTTP specification), it is passed through verbatimly. */ public static function unquote(string $s): string { return preg_replace('/\\\\(.)|"/', '$1', $s); } /** * Generates an HTTP Content-Disposition field-value. * * @param string $disposition One of "inline" or "attachment" * @param string $filename A unicode string * @param string $filenameFallback A string containing only ASCII characters that * is semantically equivalent to $filename. If the filename is already ASCII, * it can be omitted, or just copied from $filename * * @throws \InvalidArgumentException * * @see RFC 6266 */ public static function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string { if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) { throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE)); } if ('' === $filenameFallback) { $filenameFallback = $filename; } // filenameFallback is not ASCII. if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) { throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.'); } // percent characters aren't safe in fallback. if (str_contains($filenameFallback, '%')) { throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.'); } // path separators aren't allowed in either. if (str_contains($filename, '/') || str_contains($filename, '\\') || str_contains($filenameFallback, '/') || str_contains($filenameFallback, '\\')) { throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.'); } $params = ['filename' => $filenameFallback]; if ($filename !== $filenameFallback) { $params['filename*'] = "utf-8''".rawurlencode($filename); } return $disposition.'; '.self::toString($params, ';'); } /** * Like parse_str(), but preserves dots in variable names. */ public static function parseQuery(string $query, bool $ignoreBrackets = false, string $separator = '&'): array { $q = []; foreach (explode($separator, $query) as $v) { if (false !== $i = strpos($v, "\0")) { $v = substr($v, 0, $i); } if (false === $i = strpos($v, '=')) { $k = urldecode($v); $v = ''; } else { $k = urldecode(substr($v, 0, $i)); $v = substr($v, $i); } if (false !== $i = strpos($k, "\0")) { $k = substr($k, 0, $i); } $k = ltrim($k, ' '); if ($ignoreBrackets) { $q[$k][] = urldecode(substr($v, 1)); continue; } if (false === $i = strpos($k, '[')) { $q[] = bin2hex($k).$v; } else { $q[] = bin2hex(substr($k, 0, $i)).rawurlencode(substr($k, $i)).$v; } } if ($ignoreBrackets) { return $q; } parse_str(implode('&', $q), $q); $query = []; foreach ($q as $k => $v) { if (false !== $i = strpos($k, '_')) { $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v; } else { $query[hex2bin($k)] = $v; } } return $query; } private static function groupParts(array $matches, string $separators, bool $first = true): array { $separator = $separators[0]; $partSeparators = substr($separators, 1); $i = 0; $partMatches = []; $previousMatchWasSeparator = false; foreach ($matches as $match) { if (!$first && $previousMatchWasSeparator && isset($match['separator']) && $match['separator'] === $separator) { $previousMatchWasSeparator = true; $partMatches[$i][] = $match; } elseif (isset($match['separator']) && $match['separator'] === $separator) { $previousMatchWasSeparator = true; ++$i; } else { $previousMatchWasSeparator = false; $partMatches[$i][] = $match; } } $parts = []; if ($partSeparators) { foreach ($partMatches as $matches) { $parts[] = self::groupParts($matches, $partSeparators, false); } } else { foreach ($partMatches as $matches) { $parts[] = self::unquote($matches[0][0]); } if (!$first && 2 < \count($parts)) { $parts = [ $parts[0], implode($separator, \array_slice($parts, 1)), ]; } } return $parts; } } http-foundation/BinaryFileResponse.php 0000644 00000030200 15025017654 0014143 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\File; /** * BinaryFileResponse represents an HTTP response delivering a file. * * @author Niklas Fiekas <niklas.fiekas@tu-clausthal.de> * @author stealth35 <stealth35-php@live.fr> * @author Igor Wiedler <igor@wiedler.ch> * @author Jordan Alliot <jordan.alliot@gmail.com> * @author Sergey Linnik <linniksa@gmail.com> */ class BinaryFileResponse extends Response { protected static $trustXSendfileTypeHeader = false; /** * @var File */ protected $file; protected $offset = 0; protected $maxlen = -1; protected $deleteFileAfterSend = false; protected $chunkSize = 8 * 1024; /** * @param \SplFileInfo|string $file The file to stream * @param int $status The response status code * @param array $headers An array of response headers * @param bool $public Files are public by default * @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename * @param bool $autoEtag Whether the ETag header should be automatically set * @param bool $autoLastModified Whether the Last-Modified header should be automatically set */ public function __construct(\SplFileInfo|string $file, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) { parent::__construct(null, $status, $headers); $this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified); if ($public) { $this->setPublic(); } } /** * Sets the file to stream. * * @return $this * * @throws FileException */ public function setFile(\SplFileInfo|string $file, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true): static { if (!$file instanceof File) { if ($file instanceof \SplFileInfo) { $file = new File($file->getPathname()); } else { $file = new File((string) $file); } } if (!$file->isReadable()) { throw new FileException('File must be readable.'); } $this->file = $file; if ($autoEtag) { $this->setAutoEtag(); } if ($autoLastModified) { $this->setAutoLastModified(); } if ($contentDisposition) { $this->setContentDisposition($contentDisposition); } return $this; } /** * Gets the file. */ public function getFile(): File { return $this->file; } /** * Sets the response stream chunk size. * * @return $this */ public function setChunkSize(int $chunkSize): static { if ($chunkSize < 1 || $chunkSize > \PHP_INT_MAX) { throw new \LogicException('The chunk size of a BinaryFileResponse cannot be less than 1 or greater than PHP_INT_MAX.'); } $this->chunkSize = $chunkSize; return $this; } /** * Automatically sets the Last-Modified header according the file modification date. * * @return $this */ public function setAutoLastModified(): static { $this->setLastModified(\DateTime::createFromFormat('U', $this->file->getMTime())); return $this; } /** * Automatically sets the ETag header according to the checksum of the file. * * @return $this */ public function setAutoEtag(): static { $this->setEtag(base64_encode(hash_file('sha256', $this->file->getPathname(), true))); return $this; } /** * Sets the Content-Disposition header with the given filename. * * @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT * @param string $filename Optionally use this UTF-8 encoded filename instead of the real name of the file * @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename * * @return $this */ public function setContentDisposition(string $disposition, string $filename = '', string $filenameFallback = ''): static { if ('' === $filename) { $filename = $this->file->getFilename(); } if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || str_contains($filename, '%'))) { $encoding = mb_detect_encoding($filename, null, true) ?: '8bit'; for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) { $char = mb_substr($filename, $i, 1, $encoding); if ('%' === $char || \ord($char) < 32 || \ord($char) > 126) { $filenameFallback .= '_'; } else { $filenameFallback .= $char; } } } $dispositionHeader = $this->headers->makeDisposition($disposition, $filename, $filenameFallback); $this->headers->set('Content-Disposition', $dispositionHeader); return $this; } /** * {@inheritdoc} */ public function prepare(Request $request): static { if ($this->isInformational() || $this->isEmpty()) { parent::prepare($request); $this->maxlen = 0; return $this; } if (!$this->headers->has('Content-Type')) { $this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream'); } parent::prepare($request); $this->offset = 0; $this->maxlen = -1; if (false === $fileSize = $this->file->getSize()) { return $this; } $this->headers->remove('Transfer-Encoding'); $this->headers->set('Content-Length', $fileSize); if (!$this->headers->has('Accept-Ranges')) { // Only accept ranges on safe HTTP methods $this->headers->set('Accept-Ranges', $request->isMethodSafe() ? 'bytes' : 'none'); } if (self::$trustXSendfileTypeHeader && $request->headers->has('X-Sendfile-Type')) { // Use X-Sendfile, do not send any content. $type = $request->headers->get('X-Sendfile-Type'); $path = $this->file->getRealPath(); // Fall back to scheme://path for stream wrapped locations. if (false === $path) { $path = $this->file->getPathname(); } if ('x-accel-redirect' === strtolower($type)) { // Do X-Accel-Mapping substitutions. // @link https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/#x-accel-redirect $parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping', ''), ',='); foreach ($parts as $part) { [$pathPrefix, $location] = $part; if (substr($path, 0, \strlen($pathPrefix)) === $pathPrefix) { $path = $location.substr($path, \strlen($pathPrefix)); // Only set X-Accel-Redirect header if a valid URI can be produced // as nginx does not serve arbitrary file paths. $this->headers->set($type, $path); $this->maxlen = 0; break; } } } else { $this->headers->set($type, $path); $this->maxlen = 0; } } elseif ($request->headers->has('Range') && $request->isMethod('GET')) { // Process the range headers. if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) { $range = $request->headers->get('Range'); if (str_starts_with($range, 'bytes=')) { [$start, $end] = explode('-', substr($range, 6), 2) + [0]; $end = ('' === $end) ? $fileSize - 1 : (int) $end; if ('' === $start) { $start = $fileSize - $end; $end = $fileSize - 1; } else { $start = (int) $start; } if ($start <= $end) { $end = min($end, $fileSize - 1); if ($start < 0 || $start > $end) { $this->setStatusCode(416); $this->headers->set('Content-Range', sprintf('bytes */%s', $fileSize)); } elseif ($end - $start < $fileSize - 1) { $this->maxlen = $end < $fileSize ? $end - $start + 1 : -1; $this->offset = $start; $this->setStatusCode(206); $this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $fileSize)); $this->headers->set('Content-Length', $end - $start + 1); } } } } } if ($request->isMethod('HEAD')) { $this->maxlen = 0; } return $this; } private function hasValidIfRangeHeader(?string $header): bool { if ($this->getEtag() === $header) { return true; } if (null === $lastModified = $this->getLastModified()) { return false; } return $lastModified->format('D, d M Y H:i:s').' GMT' === $header; } /** * {@inheritdoc} */ public function sendContent(): static { try { if (!$this->isSuccessful()) { return parent::sendContent(); } if (0 === $this->maxlen) { return $this; } $out = fopen('php://output', 'w'); $file = fopen($this->file->getPathname(), 'r'); ignore_user_abort(true); if (0 !== $this->offset) { fseek($file, $this->offset); } $length = $this->maxlen; while ($length && !feof($file)) { $read = ($length > $this->chunkSize) ? $this->chunkSize : $length; $length -= $read; stream_copy_to_stream($file, $out, $read); if (connection_aborted()) { break; } } fclose($out); fclose($file); } finally { if ($this->deleteFileAfterSend && is_file($this->file->getPathname())) { unlink($this->file->getPathname()); } } return $this; } /** * {@inheritdoc} * * @throws \LogicException when the content is not null */ public function setContent(?string $content): static { if (null !== $content) { throw new \LogicException('The content cannot be set on a BinaryFileResponse instance.'); } return $this; } /** * {@inheritdoc} */ public function getContent(): string|false { return false; } /** * Trust X-Sendfile-Type header. */ public static function trustXSendfileTypeHeader() { self::$trustXSendfileTypeHeader = true; } /** * If this is set to true, the file will be unlinked after the request is sent * Note: If the X-Sendfile header is used, the deleteFileAfterSend setting will not be used. * * @return $this */ public function deleteFileAfterSend(bool $shouldDelete = true): static { $this->deleteFileAfterSend = $shouldDelete; return $this; } } polyfill-mbstring/bootstrap.php 0000644 00000016105 15025017654 0012757 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Mbstring as p; if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; } if (!function_exists('mb_convert_encoding')) { function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); } } if (!function_exists('mb_decode_mimeheader')) { function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); } } if (!function_exists('mb_encode_mimeheader')) { function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); } } if (!function_exists('mb_decode_numericentity')) { function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); } } if (!function_exists('mb_encode_numericentity')) { function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); } } if (!function_exists('mb_convert_case')) { function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); } } if (!function_exists('mb_internal_encoding')) { function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); } } if (!function_exists('mb_language')) { function mb_language($language = null) { return p\Mbstring::mb_language($language); } } if (!function_exists('mb_list_encodings')) { function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } } if (!function_exists('mb_encoding_aliases')) { function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } } if (!function_exists('mb_check_encoding')) { function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); } } if (!function_exists('mb_detect_encoding')) { function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); } } if (!function_exists('mb_detect_order')) { function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); } } if (!function_exists('mb_parse_str')) { function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; } } if (!function_exists('mb_strlen')) { function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); } } if (!function_exists('mb_strpos')) { function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strtolower')) { function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); } } if (!function_exists('mb_strtoupper')) { function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); } } if (!function_exists('mb_substitute_character')) { function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); } } if (!function_exists('mb_substr')) { function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); } } if (!function_exists('mb_stripos')) { function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_stristr')) { function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strrchr')) { function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strrichr')) { function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strripos')) { function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strrpos')) { function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strstr')) { function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_get_info')) { function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } } if (!function_exists('mb_http_output')) { function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); } } if (!function_exists('mb_strwidth')) { function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); } } if (!function_exists('mb_substr_count')) { function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); } } if (!function_exists('mb_output_handler')) { function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); } } if (!function_exists('mb_http_input')) { function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); } } if (!function_exists('mb_convert_variables')) { function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); } } if (!function_exists('mb_ord')) { function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); } } if (!function_exists('mb_chr')) { function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); } } if (!function_exists('mb_scrub')) { function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); } } if (!function_exists('mb_str_split')) { function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); } } if (extension_loaded('mbstring')) { return; } if (!defined('MB_CASE_UPPER')) { define('MB_CASE_UPPER', 0); } if (!defined('MB_CASE_LOWER')) { define('MB_CASE_LOWER', 1); } if (!defined('MB_CASE_TITLE')) { define('MB_CASE_TITLE', 2); } polyfill-mbstring/Mbstring.php 0000644 00000070735 15025017654 0012540 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Mbstring; /** * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. * * Implemented: * - mb_chr - Returns a specific character from its Unicode code point * - mb_convert_encoding - Convert character encoding * - mb_convert_variables - Convert character code in variable(s) * - mb_decode_mimeheader - Decode string in MIME header field * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED * - mb_decode_numericentity - Decode HTML numeric string reference to character * - mb_encode_numericentity - Encode character to HTML numeric string reference * - mb_convert_case - Perform case folding on a string * - mb_detect_encoding - Detect character encoding * - mb_get_info - Get internal settings of mbstring * - mb_http_input - Detect HTTP input character encoding * - mb_http_output - Set/Get HTTP output character encoding * - mb_internal_encoding - Set/Get internal character encoding * - mb_list_encodings - Returns an array of all supported encodings * - mb_ord - Returns the Unicode code point of a character * - mb_output_handler - Callback function converts character encoding in output buffer * - mb_scrub - Replaces ill-formed byte sequences with substitute characters * - mb_strlen - Get string length * - mb_strpos - Find position of first occurrence of string in a string * - mb_strrpos - Find position of last occurrence of a string in a string * - mb_str_split - Convert a string to an array * - mb_strtolower - Make a string lowercase * - mb_strtoupper - Make a string uppercase * - mb_substitute_character - Set/Get substitution character * - mb_substr - Get part of string * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive * - mb_stristr - Finds first occurrence of a string within another, case insensitive * - mb_strrchr - Finds the last occurrence of a character in a string within another * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive * - mb_strstr - Finds first occurrence of a string within another * - mb_strwidth - Return width of string * - mb_substr_count - Count the number of substring occurrences * * Not implemented: * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) * - mb_ereg_* - Regular expression with multibyte support * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable * - mb_preferred_mime_name - Get MIME charset string * - mb_regex_encoding - Returns current encoding for multibyte regex as string * - mb_regex_set_options - Set/Get the default options for mbregex functions * - mb_send_mail - Send encoded mail * - mb_split - Split multibyte string using regular expression * - mb_strcut - Get part of string * - mb_strimwidth - Get truncated string with specified width * * @author Nicolas Grekas <p@tchwork.com> * * @internal */ final class Mbstring { public const MB_CASE_FOLD = \PHP_INT_MAX; private const CASE_FOLD = [ ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], ]; private static $encodingList = ['ASCII', 'UTF-8']; private static $language = 'neutral'; private static $internalEncoding = 'UTF-8'; public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) { if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) { $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); } else { $fromEncoding = self::getEncoding($fromEncoding); } $toEncoding = self::getEncoding($toEncoding); if ('BASE64' === $fromEncoding) { $s = base64_decode($s); $fromEncoding = $toEncoding; } if ('BASE64' === $toEncoding) { return base64_encode($s); } if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { $fromEncoding = 'Windows-1252'; } if ('UTF-8' !== $fromEncoding) { $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); } return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s); } if ('HTML-ENTITIES' === $fromEncoding) { $s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8'); $fromEncoding = 'UTF-8'; } return iconv($fromEncoding, $toEncoding.'//IGNORE', $s); } public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars) { $ok = true; array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { $ok = false; } }); return $ok ? $fromEncoding : false; } public static function mb_decode_mimeheader($s) { return iconv_mime_decode($s, 2, self::$internalEncoding); } public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) { trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING); } public static function mb_decode_numericentity($s, $convmap, $encoding = null) { if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); return null; } if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { return false; } if (null !== $encoding && !\is_scalar($encoding)) { trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); return ''; // Instead of null (cf. mb_encode_numericentity). } $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } $cnt = floor(\count($convmap) / 4) * 4; for ($i = 0; $i < $cnt; $i += 4) { // collector_decode_htmlnumericentity ignores $convmap[$i + 3] $convmap[$i] += $convmap[$i + 2]; $convmap[$i + 1] += $convmap[$i + 2]; } $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) { $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1]; for ($i = 0; $i < $cnt; $i += 4) { if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) { return self::mb_chr($c - $convmap[$i + 2]); } } return $m[0]; }, $s); if (null === $encoding) { return $s; } return iconv('UTF-8', $encoding.'//IGNORE', $s); } public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false) { if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); return null; } if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { return false; } if (null !== $encoding && !\is_scalar($encoding)) { trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); return null; // Instead of '' (cf. mb_decode_numericentity). } if (null !== $is_hex && !\is_scalar($is_hex)) { trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING); return null; } $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; $cnt = floor(\count($convmap) / 4) * 4; $i = 0; $len = \strlen($s); $result = ''; while ($i < $len) { $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; $c = self::mb_ord($uchr); for ($j = 0; $j < $cnt; $j += 4) { if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) { $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3]; $result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';'; continue 2; } } $result .= $uchr; } if (null === $encoding) { return $result; } return iconv('UTF-8', $encoding.'//IGNORE', $result); } public static function mb_convert_case($s, $mode, $encoding = null) { $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } if (\MB_CASE_TITLE == $mode) { static $titleRegexp = null; if (null === $titleRegexp) { $titleRegexp = self::getData('titleCaseRegexp'); } $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s); } else { if (\MB_CASE_UPPER == $mode) { static $upper = null; if (null === $upper) { $upper = self::getData('upperCase'); } $map = $upper; } else { if (self::MB_CASE_FOLD === $mode) { $s = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $s); } static $lower = null; if (null === $lower) { $lower = self::getData('lowerCase'); } $map = $lower; } static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; $i = 0; $len = \strlen($s); while ($i < $len) { $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; if (isset($map[$uchr])) { $uchr = $map[$uchr]; $nlen = \strlen($uchr); if ($nlen == $ulen) { $nlen = $i; do { $s[--$nlen] = $uchr[--$ulen]; } while ($ulen); } else { $s = substr_replace($s, $uchr, $i - $ulen, $ulen); $len += $nlen - $ulen; $i += $nlen - $ulen; } } } } if (null === $encoding) { return $s; } return iconv('UTF-8', $encoding.'//IGNORE', $s); } public static function mb_internal_encoding($encoding = null) { if (null === $encoding) { return self::$internalEncoding; } $normalizedEncoding = self::getEncoding($encoding); if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) { self::$internalEncoding = $normalizedEncoding; return true; } if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding)); } public static function mb_language($lang = null) { if (null === $lang) { return self::$language; } switch ($normalizedLang = strtolower($lang)) { case 'uni': case 'neutral': self::$language = $normalizedLang; return true; } if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang)); } public static function mb_list_encodings() { return ['UTF-8']; } public static function mb_encoding_aliases($encoding) { switch (strtoupper($encoding)) { case 'UTF8': case 'UTF-8': return ['utf8']; } return false; } public static function mb_check_encoding($var = null, $encoding = null) { if (null === $encoding) { if (null === $var) { return false; } $encoding = self::$internalEncoding; } return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var); } public static function mb_detect_encoding($str, $encodingList = null, $strict = false) { if (null === $encodingList) { $encodingList = self::$encodingList; } else { if (!\is_array($encodingList)) { $encodingList = array_map('trim', explode(',', $encodingList)); } $encodingList = array_map('strtoupper', $encodingList); } foreach ($encodingList as $enc) { switch ($enc) { case 'ASCII': if (!preg_match('/[\x80-\xFF]/', $str)) { return $enc; } break; case 'UTF8': case 'UTF-8': if (preg_match('//u', $str)) { return 'UTF-8'; } break; default: if (0 === strncmp($enc, 'ISO-8859-', 9)) { return $enc; } } } return false; } public static function mb_detect_order($encodingList = null) { if (null === $encodingList) { return self::$encodingList; } if (!\is_array($encodingList)) { $encodingList = array_map('trim', explode(',', $encodingList)); } $encodingList = array_map('strtoupper', $encodingList); foreach ($encodingList as $enc) { switch ($enc) { default: if (strncmp($enc, 'ISO-8859-', 9)) { return false; } // no break case 'ASCII': case 'UTF8': case 'UTF-8': } } self::$encodingList = $encodingList; return true; } public static function mb_strlen($s, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return \strlen($s); } return @iconv_strlen($s, $encoding); } public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return strpos($haystack, $needle, $offset); } $needle = (string) $needle; if ('' === $needle) { if (80000 > \PHP_VERSION_ID) { trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING); return false; } return 0; } return iconv_strpos($haystack, $needle, $offset, $encoding); } public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return strrpos($haystack, $needle, $offset); } if ($offset != (int) $offset) { $offset = 0; } elseif ($offset = (int) $offset) { if ($offset < 0) { if (0 > $offset += self::mb_strlen($needle)) { $haystack = self::mb_substr($haystack, 0, $offset, $encoding); } $offset = 0; } else { $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); } } $pos = '' !== $needle || 80000 > \PHP_VERSION_ID ? iconv_strrpos($haystack, $needle, $encoding) : self::mb_strlen($haystack, $encoding); return false !== $pos ? $offset + $pos : false; } public static function mb_str_split($string, $split_length = 1, $encoding = null) { if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) { trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING); return null; } if (1 > $split_length = (int) $split_length) { if (80000 > \PHP_VERSION_ID) { trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING); return false; } throw new \ValueError('Argument #2 ($length) must be greater than 0'); } if (null === $encoding) { $encoding = mb_internal_encoding(); } if ('UTF-8' === $encoding = self::getEncoding($encoding)) { $rx = '/('; while (65535 < $split_length) { $rx .= '.{65535}'; $split_length -= 65535; } $rx .= '.{'.$split_length.'})/us'; return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY); } $result = []; $length = mb_strlen($string, $encoding); for ($i = 0; $i < $length; $i += $split_length) { $result[] = mb_substr($string, $i, $split_length, $encoding); } return $result; } public static function mb_strtolower($s, $encoding = null) { return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding); } public static function mb_strtoupper($s, $encoding = null) { return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding); } public static function mb_substitute_character($c = null) { if (null === $c) { return 'none'; } if (0 === strcasecmp($c, 'none')) { return true; } if (80000 > \PHP_VERSION_ID) { return false; } if (\is_int($c) || 'long' === $c || 'entity' === $c) { return false; } throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint'); } public static function mb_substr($s, $start, $length = null, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return (string) substr($s, $start, null === $length ? 2147483647 : $length); } if ($start < 0) { $start = iconv_strlen($s, $encoding) + $start; if ($start < 0) { $start = 0; } } if (null === $length) { $length = 2147483647; } elseif ($length < 0) { $length = iconv_strlen($s, $encoding) + $length - $start; if ($length < 0) { return ''; } } return (string) iconv_substr($s, $start, $length, $encoding); } public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); return self::mb_strpos($haystack, $needle, $offset, $encoding); } public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) { $pos = self::mb_stripos($haystack, $needle, 0, $encoding); return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { $pos = strrpos($haystack, $needle); } else { $needle = self::mb_substr($needle, 0, 1, $encoding); $pos = iconv_strrpos($haystack, $needle, $encoding); } return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) { $needle = self::mb_substr($needle, 0, 1, $encoding); $pos = self::mb_strripos($haystack, $needle, $encoding); return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); return self::mb_strrpos($haystack, $needle, $offset, $encoding); } public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) { $pos = strpos($haystack, $needle); if (false === $pos) { return false; } if ($part) { return substr($haystack, 0, $pos); } return substr($haystack, $pos); } public static function mb_get_info($type = 'all') { $info = [ 'internal_encoding' => self::$internalEncoding, 'http_output' => 'pass', 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', 'func_overload' => 0, 'func_overload_list' => 'no overload', 'mail_charset' => 'UTF-8', 'mail_header_encoding' => 'BASE64', 'mail_body_encoding' => 'BASE64', 'illegal_chars' => 0, 'encoding_translation' => 'Off', 'language' => self::$language, 'detect_order' => self::$encodingList, 'substitute_character' => 'none', 'strict_detection' => 'Off', ]; if ('all' === $type) { return $info; } if (isset($info[$type])) { return $info[$type]; } return false; } public static function mb_http_input($type = '') { return false; } public static function mb_http_output($encoding = null) { return null !== $encoding ? 'pass' === $encoding : 'pass'; } public static function mb_strwidth($s, $encoding = null) { $encoding = self::getEncoding($encoding); if ('UTF-8' !== $encoding) { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); return ($wide << 1) + iconv_strlen($s, 'UTF-8'); } public static function mb_substr_count($haystack, $needle, $encoding = null) { return substr_count($haystack, $needle); } public static function mb_output_handler($contents, $status) { return $contents; } public static function mb_chr($code, $encoding = null) { if (0x80 > $code %= 0x200000) { $s = \chr($code); } elseif (0x800 > $code) { $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); } elseif (0x10000 > $code) { $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } else { $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { $s = mb_convert_encoding($s, $encoding, 'UTF-8'); } return $s; } public static function mb_ord($s, $encoding = null) { if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { $s = mb_convert_encoding($s, 'UTF-8', $encoding); } if (1 === \strlen($s)) { return \ord($s); } $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; if (0xF0 <= $code) { return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; } if (0xE0 <= $code) { return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; } if (0xC0 <= $code) { return (($code - 0xC0) << 6) + $s[2] - 0x80; } return $code; } private static function getSubpart($pos, $part, $haystack, $encoding) { if (false === $pos) { return false; } if ($part) { return self::mb_substr($haystack, 0, $pos, $encoding); } return self::mb_substr($haystack, $pos, null, $encoding); } private static function html_encoding_callback(array $m) { $i = 1; $entities = ''; $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8')); while (isset($m[$i])) { if (0x80 > $m[$i]) { $entities .= \chr($m[$i++]); continue; } if (0xF0 <= $m[$i]) { $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; } elseif (0xE0 <= $m[$i]) { $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; } else { $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; } $entities .= '&#'.$c.';'; } return $entities; } private static function title_case(array $s) { return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8'); } private static function getData($file) { if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { return require $file; } return false; } private static function getEncoding($encoding) { if (null === $encoding) { return self::$internalEncoding; } if ('UTF-8' === $encoding) { return 'UTF-8'; } $encoding = strtoupper($encoding); if ('8BIT' === $encoding || 'BINARY' === $encoding) { return 'CP850'; } if ('UTF8' === $encoding) { return 'UTF-8'; } return $encoding; } } polyfill-mbstring/composer.json 0000644 00000002040 15025017654 0012744 0 ustar 00 { "name": "symfony/polyfill-mbstring", "type": "library", "description": "Symfony polyfill for the Mbstring extension", "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.1" }, "provide": { "ext-mbstring": "*" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" }, "files": [ "bootstrap.php" ] }, "suggest": { "ext-mbstring": "For best performance" }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } polyfill-mbstring/README.md 0000644 00000000562 15025017654 0011510 0 ustar 00 Symfony Polyfill / Mbstring =========================== This component provides a partial, native PHP implementation for the [Mbstring](https://php.net/mbstring) extension. More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). polyfill-mbstring/LICENSE 0000644 00000002051 15025017654 0011231 0 ustar 00 Copyright (c) 2015-2019 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. polyfill-mbstring/bootstrap80.php 0000644 00000021120 15025017654 0013120 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Mbstring as p; if (!function_exists('mb_convert_encoding')) { function mb_convert_encoding(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); } } if (!function_exists('mb_decode_mimeheader')) { function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); } } if (!function_exists('mb_encode_mimeheader')) { function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); } } if (!function_exists('mb_decode_numericentity')) { function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); } } if (!function_exists('mb_encode_numericentity')) { function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); } } if (!function_exists('mb_convert_case')) { function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); } } if (!function_exists('mb_internal_encoding')) { function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); } } if (!function_exists('mb_language')) { function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); } } if (!function_exists('mb_list_encodings')) { function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); } } if (!function_exists('mb_encoding_aliases')) { function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); } } if (!function_exists('mb_check_encoding')) { function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); } } if (!function_exists('mb_detect_encoding')) { function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); } } if (!function_exists('mb_detect_order')) { function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); } } if (!function_exists('mb_parse_str')) { function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; } } if (!function_exists('mb_strlen')) { function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); } } if (!function_exists('mb_strpos')) { function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } } if (!function_exists('mb_strtolower')) { function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); } } if (!function_exists('mb_strtoupper')) { function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); } } if (!function_exists('mb_substitute_character')) { function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); } } if (!function_exists('mb_substr')) { function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); } } if (!function_exists('mb_stripos')) { function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } } if (!function_exists('mb_stristr')) { function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } } if (!function_exists('mb_strrchr')) { function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } } if (!function_exists('mb_strrichr')) { function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } } if (!function_exists('mb_strripos')) { function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } } if (!function_exists('mb_strrpos')) { function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } } if (!function_exists('mb_strstr')) { function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } } if (!function_exists('mb_get_info')) { function mb_get_info(?string $type = 'all'): array|string|int|false { return p\Mbstring::mb_get_info((string) $type); } } if (!function_exists('mb_http_output')) { function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); } } if (!function_exists('mb_strwidth')) { function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); } } if (!function_exists('mb_substr_count')) { function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); } } if (!function_exists('mb_output_handler')) { function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); } } if (!function_exists('mb_http_input')) { function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); } } if (!function_exists('mb_convert_variables')) { function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); } } if (!function_exists('mb_ord')) { function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); } } if (!function_exists('mb_chr')) { function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); } } if (!function_exists('mb_scrub')) { function mb_scrub(?string $string, ?string $encoding = null): string { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $encoding); } } if (!function_exists('mb_str_split')) { function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); } } if (extension_loaded('mbstring')) { return; } if (!defined('MB_CASE_UPPER')) { define('MB_CASE_UPPER', 0); } if (!defined('MB_CASE_LOWER')) { define('MB_CASE_LOWER', 1); } if (!defined('MB_CASE_TITLE')) { define('MB_CASE_TITLE', 2); } polyfill-mbstring/Resources/unidata/lowerCase.php 0000644 00000057733 15025017654 0016301 0 ustar 00 <?php return array ( 'A' => 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', 'E' => 'e', 'F' => 'f', 'G' => 'g', 'H' => 'h', 'I' => 'i', 'J' => 'j', 'K' => 'k', 'L' => 'l', 'M' => 'm', 'N' => 'n', 'O' => 'o', 'P' => 'p', 'Q' => 'q', 'R' => 'r', 'S' => 's', 'T' => 't', 'U' => 'u', 'V' => 'v', 'W' => 'w', 'X' => 'x', 'Y' => 'y', 'Z' => 'z', 'À' => 'à', 'Á' => 'á', 'Â' => 'â', 'Ã' => 'ã', 'Ä' => 'ä', 'Å' => 'å', 'Æ' => 'æ', 'Ç' => 'ç', 'È' => 'è', 'É' => 'é', 'Ê' => 'ê', 'Ë' => 'ë', 'Ì' => 'ì', 'Í' => 'í', 'Î' => 'î', 'Ï' => 'ï', 'Ð' => 'ð', 'Ñ' => 'ñ', 'Ò' => 'ò', 'Ó' => 'ó', 'Ô' => 'ô', 'Õ' => 'õ', 'Ö' => 'ö', 'Ø' => 'ø', 'Ù' => 'ù', 'Ú' => 'ú', 'Û' => 'û', 'Ü' => 'ü', 'Ý' => 'ý', 'Þ' => 'þ', 'Ā' => 'ā', 'Ă' => 'ă', 'Ą' => 'ą', 'Ć' => 'ć', 'Ĉ' => 'ĉ', 'Ċ' => 'ċ', 'Č' => 'č', 'Ď' => 'ď', 'Đ' => 'đ', 'Ē' => 'ē', 'Ĕ' => 'ĕ', 'Ė' => 'ė', 'Ę' => 'ę', 'Ě' => 'ě', 'Ĝ' => 'ĝ', 'Ğ' => 'ğ', 'Ġ' => 'ġ', 'Ģ' => 'ģ', 'Ĥ' => 'ĥ', 'Ħ' => 'ħ', 'Ĩ' => 'ĩ', 'Ī' => 'ī', 'Ĭ' => 'ĭ', 'Į' => 'į', 'İ' => 'i̇', 'IJ' => 'ij', 'Ĵ' => 'ĵ', 'Ķ' => 'ķ', 'Ĺ' => 'ĺ', 'Ļ' => 'ļ', 'Ľ' => 'ľ', 'Ŀ' => 'ŀ', 'Ł' => 'ł', 'Ń' => 'ń', 'Ņ' => 'ņ', 'Ň' => 'ň', 'Ŋ' => 'ŋ', 'Ō' => 'ō', 'Ŏ' => 'ŏ', 'Ő' => 'ő', 'Œ' => 'œ', 'Ŕ' => 'ŕ', 'Ŗ' => 'ŗ', 'Ř' => 'ř', 'Ś' => 'ś', 'Ŝ' => 'ŝ', 'Ş' => 'ş', 'Š' => 'š', 'Ţ' => 'ţ', 'Ť' => 'ť', 'Ŧ' => 'ŧ', 'Ũ' => 'ũ', 'Ū' => 'ū', 'Ŭ' => 'ŭ', 'Ů' => 'ů', 'Ű' => 'ű', 'Ų' => 'ų', 'Ŵ' => 'ŵ', 'Ŷ' => 'ŷ', 'Ÿ' => 'ÿ', 'Ź' => 'ź', 'Ż' => 'ż', 'Ž' => 'ž', 'Ɓ' => 'ɓ', 'Ƃ' => 'ƃ', 'Ƅ' => 'ƅ', 'Ɔ' => 'ɔ', 'Ƈ' => 'ƈ', 'Ɖ' => 'ɖ', 'Ɗ' => 'ɗ', 'Ƌ' => 'ƌ', 'Ǝ' => 'ǝ', 'Ə' => 'ə', 'Ɛ' => 'ɛ', 'Ƒ' => 'ƒ', 'Ɠ' => 'ɠ', 'Ɣ' => 'ɣ', 'Ɩ' => 'ɩ', 'Ɨ' => 'ɨ', 'Ƙ' => 'ƙ', 'Ɯ' => 'ɯ', 'Ɲ' => 'ɲ', 'Ɵ' => 'ɵ', 'Ơ' => 'ơ', 'Ƣ' => 'ƣ', 'Ƥ' => 'ƥ', 'Ʀ' => 'ʀ', 'Ƨ' => 'ƨ', 'Ʃ' => 'ʃ', 'Ƭ' => 'ƭ', 'Ʈ' => 'ʈ', 'Ư' => 'ư', 'Ʊ' => 'ʊ', 'Ʋ' => 'ʋ', 'Ƴ' => 'ƴ', 'Ƶ' => 'ƶ', 'Ʒ' => 'ʒ', 'Ƹ' => 'ƹ', 'Ƽ' => 'ƽ', 'DŽ' => 'dž', 'Dž' => 'dž', 'LJ' => 'lj', 'Lj' => 'lj', 'NJ' => 'nj', 'Nj' => 'nj', 'Ǎ' => 'ǎ', 'Ǐ' => 'ǐ', 'Ǒ' => 'ǒ', 'Ǔ' => 'ǔ', 'Ǖ' => 'ǖ', 'Ǘ' => 'ǘ', 'Ǚ' => 'ǚ', 'Ǜ' => 'ǜ', 'Ǟ' => 'ǟ', 'Ǡ' => 'ǡ', 'Ǣ' => 'ǣ', 'Ǥ' => 'ǥ', 'Ǧ' => 'ǧ', 'Ǩ' => 'ǩ', 'Ǫ' => 'ǫ', 'Ǭ' => 'ǭ', 'Ǯ' => 'ǯ', 'DZ' => 'dz', 'Dz' => 'dz', 'Ǵ' => 'ǵ', 'Ƕ' => 'ƕ', 'Ƿ' => 'ƿ', 'Ǹ' => 'ǹ', 'Ǻ' => 'ǻ', 'Ǽ' => 'ǽ', 'Ǿ' => 'ǿ', 'Ȁ' => 'ȁ', 'Ȃ' => 'ȃ', 'Ȅ' => 'ȅ', 'Ȇ' => 'ȇ', 'Ȉ' => 'ȉ', 'Ȋ' => 'ȋ', 'Ȍ' => 'ȍ', 'Ȏ' => 'ȏ', 'Ȑ' => 'ȑ', 'Ȓ' => 'ȓ', 'Ȕ' => 'ȕ', 'Ȗ' => 'ȗ', 'Ș' => 'ș', 'Ț' => 'ț', 'Ȝ' => 'ȝ', 'Ȟ' => 'ȟ', 'Ƞ' => 'ƞ', 'Ȣ' => 'ȣ', 'Ȥ' => 'ȥ', 'Ȧ' => 'ȧ', 'Ȩ' => 'ȩ', 'Ȫ' => 'ȫ', 'Ȭ' => 'ȭ', 'Ȯ' => 'ȯ', 'Ȱ' => 'ȱ', 'Ȳ' => 'ȳ', 'Ⱥ' => 'ⱥ', 'Ȼ' => 'ȼ', 'Ƚ' => 'ƚ', 'Ⱦ' => 'ⱦ', 'Ɂ' => 'ɂ', 'Ƀ' => 'ƀ', 'Ʉ' => 'ʉ', 'Ʌ' => 'ʌ', 'Ɇ' => 'ɇ', 'Ɉ' => 'ɉ', 'Ɋ' => 'ɋ', 'Ɍ' => 'ɍ', 'Ɏ' => 'ɏ', 'Ͱ' => 'ͱ', 'Ͳ' => 'ͳ', 'Ͷ' => 'ͷ', 'Ϳ' => 'ϳ', 'Ά' => 'ά', 'Έ' => 'έ', 'Ή' => 'ή', 'Ί' => 'ί', 'Ό' => 'ό', 'Ύ' => 'ύ', 'Ώ' => 'ώ', 'Α' => 'α', 'Β' => 'β', 'Γ' => 'γ', 'Δ' => 'δ', 'Ε' => 'ε', 'Ζ' => 'ζ', 'Η' => 'η', 'Θ' => 'θ', 'Ι' => 'ι', 'Κ' => 'κ', 'Λ' => 'λ', 'Μ' => 'μ', 'Ν' => 'ν', 'Ξ' => 'ξ', 'Ο' => 'ο', 'Π' => 'π', 'Ρ' => 'ρ', 'Σ' => 'σ', 'Τ' => 'τ', 'Υ' => 'υ', 'Φ' => 'φ', 'Χ' => 'χ', 'Ψ' => 'ψ', 'Ω' => 'ω', 'Ϊ' => 'ϊ', 'Ϋ' => 'ϋ', 'Ϗ' => 'ϗ', 'Ϙ' => 'ϙ', 'Ϛ' => 'ϛ', 'Ϝ' => 'ϝ', 'Ϟ' => 'ϟ', 'Ϡ' => 'ϡ', 'Ϣ' => 'ϣ', 'Ϥ' => 'ϥ', 'Ϧ' => 'ϧ', 'Ϩ' => 'ϩ', 'Ϫ' => 'ϫ', 'Ϭ' => 'ϭ', 'Ϯ' => 'ϯ', 'ϴ' => 'θ', 'Ϸ' => 'ϸ', 'Ϲ' => 'ϲ', 'Ϻ' => 'ϻ', 'Ͻ' => 'ͻ', 'Ͼ' => 'ͼ', 'Ͽ' => 'ͽ', 'Ѐ' => 'ѐ', 'Ё' => 'ё', 'Ђ' => 'ђ', 'Ѓ' => 'ѓ', 'Є' => 'є', 'Ѕ' => 'ѕ', 'І' => 'і', 'Ї' => 'ї', 'Ј' => 'ј', 'Љ' => 'љ', 'Њ' => 'њ', 'Ћ' => 'ћ', 'Ќ' => 'ќ', 'Ѝ' => 'ѝ', 'Ў' => 'ў', 'Џ' => 'џ', 'А' => 'а', 'Б' => 'б', 'В' => 'в', 'Г' => 'г', 'Д' => 'д', 'Е' => 'е', 'Ж' => 'ж', 'З' => 'з', 'И' => 'и', 'Й' => 'й', 'К' => 'к', 'Л' => 'л', 'М' => 'м', 'Н' => 'н', 'О' => 'о', 'П' => 'п', 'Р' => 'р', 'С' => 'с', 'Т' => 'т', 'У' => 'у', 'Ф' => 'ф', 'Х' => 'х', 'Ц' => 'ц', 'Ч' => 'ч', 'Ш' => 'ш', 'Щ' => 'щ', 'Ъ' => 'ъ', 'Ы' => 'ы', 'Ь' => 'ь', 'Э' => 'э', 'Ю' => 'ю', 'Я' => 'я', 'Ѡ' => 'ѡ', 'Ѣ' => 'ѣ', 'Ѥ' => 'ѥ', 'Ѧ' => 'ѧ', 'Ѩ' => 'ѩ', 'Ѫ' => 'ѫ', 'Ѭ' => 'ѭ', 'Ѯ' => 'ѯ', 'Ѱ' => 'ѱ', 'Ѳ' => 'ѳ', 'Ѵ' => 'ѵ', 'Ѷ' => 'ѷ', 'Ѹ' => 'ѹ', 'Ѻ' => 'ѻ', 'Ѽ' => 'ѽ', 'Ѿ' => 'ѿ', 'Ҁ' => 'ҁ', 'Ҋ' => 'ҋ', 'Ҍ' => 'ҍ', 'Ҏ' => 'ҏ', 'Ґ' => 'ґ', 'Ғ' => 'ғ', 'Ҕ' => 'ҕ', 'Җ' => 'җ', 'Ҙ' => 'ҙ', 'Қ' => 'қ', 'Ҝ' => 'ҝ', 'Ҟ' => 'ҟ', 'Ҡ' => 'ҡ', 'Ң' => 'ң', 'Ҥ' => 'ҥ', 'Ҧ' => 'ҧ', 'Ҩ' => 'ҩ', 'Ҫ' => 'ҫ', 'Ҭ' => 'ҭ', 'Ү' => 'ү', 'Ұ' => 'ұ', 'Ҳ' => 'ҳ', 'Ҵ' => 'ҵ', 'Ҷ' => 'ҷ', 'Ҹ' => 'ҹ', 'Һ' => 'һ', 'Ҽ' => 'ҽ', 'Ҿ' => 'ҿ', 'Ӏ' => 'ӏ', 'Ӂ' => 'ӂ', 'Ӄ' => 'ӄ', 'Ӆ' => 'ӆ', 'Ӈ' => 'ӈ', 'Ӊ' => 'ӊ', 'Ӌ' => 'ӌ', 'Ӎ' => 'ӎ', 'Ӑ' => 'ӑ', 'Ӓ' => 'ӓ', 'Ӕ' => 'ӕ', 'Ӗ' => 'ӗ', 'Ә' => 'ә', 'Ӛ' => 'ӛ', 'Ӝ' => 'ӝ', 'Ӟ' => 'ӟ', 'Ӡ' => 'ӡ', 'Ӣ' => 'ӣ', 'Ӥ' => 'ӥ', 'Ӧ' => 'ӧ', 'Ө' => 'ө', 'Ӫ' => 'ӫ', 'Ӭ' => 'ӭ', 'Ӯ' => 'ӯ', 'Ӱ' => 'ӱ', 'Ӳ' => 'ӳ', 'Ӵ' => 'ӵ', 'Ӷ' => 'ӷ', 'Ӹ' => 'ӹ', 'Ӻ' => 'ӻ', 'Ӽ' => 'ӽ', 'Ӿ' => 'ӿ', 'Ԁ' => 'ԁ', 'Ԃ' => 'ԃ', 'Ԅ' => 'ԅ', 'Ԇ' => 'ԇ', 'Ԉ' => 'ԉ', 'Ԋ' => 'ԋ', 'Ԍ' => 'ԍ', 'Ԏ' => 'ԏ', 'Ԑ' => 'ԑ', 'Ԓ' => 'ԓ', 'Ԕ' => 'ԕ', 'Ԗ' => 'ԗ', 'Ԙ' => 'ԙ', 'Ԛ' => 'ԛ', 'Ԝ' => 'ԝ', 'Ԟ' => 'ԟ', 'Ԡ' => 'ԡ', 'Ԣ' => 'ԣ', 'Ԥ' => 'ԥ', 'Ԧ' => 'ԧ', 'Ԩ' => 'ԩ', 'Ԫ' => 'ԫ', 'Ԭ' => 'ԭ', 'Ԯ' => 'ԯ', 'Ա' => 'ա', 'Բ' => 'բ', 'Գ' => 'գ', 'Դ' => 'դ', 'Ե' => 'ե', 'Զ' => 'զ', 'Է' => 'է', 'Ը' => 'ը', 'Թ' => 'թ', 'Ժ' => 'ժ', 'Ի' => 'ի', 'Լ' => 'լ', 'Խ' => 'խ', 'Ծ' => 'ծ', 'Կ' => 'կ', 'Հ' => 'հ', 'Ձ' => 'ձ', 'Ղ' => 'ղ', 'Ճ' => 'ճ', 'Մ' => 'մ', 'Յ' => 'յ', 'Ն' => 'ն', 'Շ' => 'շ', 'Ո' => 'ո', 'Չ' => 'չ', 'Պ' => 'պ', 'Ջ' => 'ջ', 'Ռ' => 'ռ', 'Ս' => 'ս', 'Վ' => 'վ', 'Տ' => 'տ', 'Ր' => 'ր', 'Ց' => 'ց', 'Ւ' => 'ւ', 'Փ' => 'փ', 'Ք' => 'ք', 'Օ' => 'օ', 'Ֆ' => 'ֆ', 'Ⴀ' => 'ⴀ', 'Ⴁ' => 'ⴁ', 'Ⴂ' => 'ⴂ', 'Ⴃ' => 'ⴃ', 'Ⴄ' => 'ⴄ', 'Ⴅ' => 'ⴅ', 'Ⴆ' => 'ⴆ', 'Ⴇ' => 'ⴇ', 'Ⴈ' => 'ⴈ', 'Ⴉ' => 'ⴉ', 'Ⴊ' => 'ⴊ', 'Ⴋ' => 'ⴋ', 'Ⴌ' => 'ⴌ', 'Ⴍ' => 'ⴍ', 'Ⴎ' => 'ⴎ', 'Ⴏ' => 'ⴏ', 'Ⴐ' => 'ⴐ', 'Ⴑ' => 'ⴑ', 'Ⴒ' => 'ⴒ', 'Ⴓ' => 'ⴓ', 'Ⴔ' => 'ⴔ', 'Ⴕ' => 'ⴕ', 'Ⴖ' => 'ⴖ', 'Ⴗ' => 'ⴗ', 'Ⴘ' => 'ⴘ', 'Ⴙ' => 'ⴙ', 'Ⴚ' => 'ⴚ', 'Ⴛ' => 'ⴛ', 'Ⴜ' => 'ⴜ', 'Ⴝ' => 'ⴝ', 'Ⴞ' => 'ⴞ', 'Ⴟ' => 'ⴟ', 'Ⴠ' => 'ⴠ', 'Ⴡ' => 'ⴡ', 'Ⴢ' => 'ⴢ', 'Ⴣ' => 'ⴣ', 'Ⴤ' => 'ⴤ', 'Ⴥ' => 'ⴥ', 'Ⴧ' => 'ⴧ', 'Ⴭ' => 'ⴭ', 'Ꭰ' => 'ꭰ', 'Ꭱ' => 'ꭱ', 'Ꭲ' => 'ꭲ', 'Ꭳ' => 'ꭳ', 'Ꭴ' => 'ꭴ', 'Ꭵ' => 'ꭵ', 'Ꭶ' => 'ꭶ', 'Ꭷ' => 'ꭷ', 'Ꭸ' => 'ꭸ', 'Ꭹ' => 'ꭹ', 'Ꭺ' => 'ꭺ', 'Ꭻ' => 'ꭻ', 'Ꭼ' => 'ꭼ', 'Ꭽ' => 'ꭽ', 'Ꭾ' => 'ꭾ', 'Ꭿ' => 'ꭿ', 'Ꮀ' => 'ꮀ', 'Ꮁ' => 'ꮁ', 'Ꮂ' => 'ꮂ', 'Ꮃ' => 'ꮃ', 'Ꮄ' => 'ꮄ', 'Ꮅ' => 'ꮅ', 'Ꮆ' => 'ꮆ', 'Ꮇ' => 'ꮇ', 'Ꮈ' => 'ꮈ', 'Ꮉ' => 'ꮉ', 'Ꮊ' => 'ꮊ', 'Ꮋ' => 'ꮋ', 'Ꮌ' => 'ꮌ', 'Ꮍ' => 'ꮍ', 'Ꮎ' => 'ꮎ', 'Ꮏ' => 'ꮏ', 'Ꮐ' => 'ꮐ', 'Ꮑ' => 'ꮑ', 'Ꮒ' => 'ꮒ', 'Ꮓ' => 'ꮓ', 'Ꮔ' => 'ꮔ', 'Ꮕ' => 'ꮕ', 'Ꮖ' => 'ꮖ', 'Ꮗ' => 'ꮗ', 'Ꮘ' => 'ꮘ', 'Ꮙ' => 'ꮙ', 'Ꮚ' => 'ꮚ', 'Ꮛ' => 'ꮛ', 'Ꮜ' => 'ꮜ', 'Ꮝ' => 'ꮝ', 'Ꮞ' => 'ꮞ', 'Ꮟ' => 'ꮟ', 'Ꮠ' => 'ꮠ', 'Ꮡ' => 'ꮡ', 'Ꮢ' => 'ꮢ', 'Ꮣ' => 'ꮣ', 'Ꮤ' => 'ꮤ', 'Ꮥ' => 'ꮥ', 'Ꮦ' => 'ꮦ', 'Ꮧ' => 'ꮧ', 'Ꮨ' => 'ꮨ', 'Ꮩ' => 'ꮩ', 'Ꮪ' => 'ꮪ', 'Ꮫ' => 'ꮫ', 'Ꮬ' => 'ꮬ', 'Ꮭ' => 'ꮭ', 'Ꮮ' => 'ꮮ', 'Ꮯ' => 'ꮯ', 'Ꮰ' => 'ꮰ', 'Ꮱ' => 'ꮱ', 'Ꮲ' => 'ꮲ', 'Ꮳ' => 'ꮳ', 'Ꮴ' => 'ꮴ', 'Ꮵ' => 'ꮵ', 'Ꮶ' => 'ꮶ', 'Ꮷ' => 'ꮷ', 'Ꮸ' => 'ꮸ', 'Ꮹ' => 'ꮹ', 'Ꮺ' => 'ꮺ', 'Ꮻ' => 'ꮻ', 'Ꮼ' => 'ꮼ', 'Ꮽ' => 'ꮽ', 'Ꮾ' => 'ꮾ', 'Ꮿ' => 'ꮿ', 'Ᏸ' => 'ᏸ', 'Ᏹ' => 'ᏹ', 'Ᏺ' => 'ᏺ', 'Ᏻ' => 'ᏻ', 'Ᏼ' => 'ᏼ', 'Ᏽ' => 'ᏽ', 'Ა' => 'ა', 'Ბ' => 'ბ', 'Გ' => 'გ', 'Დ' => 'დ', 'Ე' => 'ე', 'Ვ' => 'ვ', 'Ზ' => 'ზ', 'Თ' => 'თ', 'Ი' => 'ი', 'Კ' => 'კ', 'Ლ' => 'ლ', 'Მ' => 'მ', 'Ნ' => 'ნ', 'Ო' => 'ო', 'Პ' => 'პ', 'Ჟ' => 'ჟ', 'Რ' => 'რ', 'Ს' => 'ს', 'Ტ' => 'ტ', 'Უ' => 'უ', 'Ფ' => 'ფ', 'Ქ' => 'ქ', 'Ღ' => 'ღ', 'Ყ' => 'ყ', 'Შ' => 'შ', 'Ჩ' => 'ჩ', 'Ც' => 'ც', 'Ძ' => 'ძ', 'Წ' => 'წ', 'Ჭ' => 'ჭ', 'Ხ' => 'ხ', 'Ჯ' => 'ჯ', 'Ჰ' => 'ჰ', 'Ჱ' => 'ჱ', 'Ჲ' => 'ჲ', 'Ჳ' => 'ჳ', 'Ჴ' => 'ჴ', 'Ჵ' => 'ჵ', 'Ჶ' => 'ჶ', 'Ჷ' => 'ჷ', 'Ჸ' => 'ჸ', 'Ჹ' => 'ჹ', 'Ჺ' => 'ჺ', 'Ჽ' => 'ჽ', 'Ჾ' => 'ჾ', 'Ჿ' => 'ჿ', 'Ḁ' => 'ḁ', 'Ḃ' => 'ḃ', 'Ḅ' => 'ḅ', 'Ḇ' => 'ḇ', 'Ḉ' => 'ḉ', 'Ḋ' => 'ḋ', 'Ḍ' => 'ḍ', 'Ḏ' => 'ḏ', 'Ḑ' => 'ḑ', 'Ḓ' => 'ḓ', 'Ḕ' => 'ḕ', 'Ḗ' => 'ḗ', 'Ḙ' => 'ḙ', 'Ḛ' => 'ḛ', 'Ḝ' => 'ḝ', 'Ḟ' => 'ḟ', 'Ḡ' => 'ḡ', 'Ḣ' => 'ḣ', 'Ḥ' => 'ḥ', 'Ḧ' => 'ḧ', 'Ḩ' => 'ḩ', 'Ḫ' => 'ḫ', 'Ḭ' => 'ḭ', 'Ḯ' => 'ḯ', 'Ḱ' => 'ḱ', 'Ḳ' => 'ḳ', 'Ḵ' => 'ḵ', 'Ḷ' => 'ḷ', 'Ḹ' => 'ḹ', 'Ḻ' => 'ḻ', 'Ḽ' => 'ḽ', 'Ḿ' => 'ḿ', 'Ṁ' => 'ṁ', 'Ṃ' => 'ṃ', 'Ṅ' => 'ṅ', 'Ṇ' => 'ṇ', 'Ṉ' => 'ṉ', 'Ṋ' => 'ṋ', 'Ṍ' => 'ṍ', 'Ṏ' => 'ṏ', 'Ṑ' => 'ṑ', 'Ṓ' => 'ṓ', 'Ṕ' => 'ṕ', 'Ṗ' => 'ṗ', 'Ṙ' => 'ṙ', 'Ṛ' => 'ṛ', 'Ṝ' => 'ṝ', 'Ṟ' => 'ṟ', 'Ṡ' => 'ṡ', 'Ṣ' => 'ṣ', 'Ṥ' => 'ṥ', 'Ṧ' => 'ṧ', 'Ṩ' => 'ṩ', 'Ṫ' => 'ṫ', 'Ṭ' => 'ṭ', 'Ṯ' => 'ṯ', 'Ṱ' => 'ṱ', 'Ṳ' => 'ṳ', 'Ṵ' => 'ṵ', 'Ṷ' => 'ṷ', 'Ṹ' => 'ṹ', 'Ṻ' => 'ṻ', 'Ṽ' => 'ṽ', 'Ṿ' => 'ṿ', 'Ẁ' => 'ẁ', 'Ẃ' => 'ẃ', 'Ẅ' => 'ẅ', 'Ẇ' => 'ẇ', 'Ẉ' => 'ẉ', 'Ẋ' => 'ẋ', 'Ẍ' => 'ẍ', 'Ẏ' => 'ẏ', 'Ẑ' => 'ẑ', 'Ẓ' => 'ẓ', 'Ẕ' => 'ẕ', 'ẞ' => 'ß', 'Ạ' => 'ạ', 'Ả' => 'ả', 'Ấ' => 'ấ', 'Ầ' => 'ầ', 'Ẩ' => 'ẩ', 'Ẫ' => 'ẫ', 'Ậ' => 'ậ', 'Ắ' => 'ắ', 'Ằ' => 'ằ', 'Ẳ' => 'ẳ', 'Ẵ' => 'ẵ', 'Ặ' => 'ặ', 'Ẹ' => 'ẹ', 'Ẻ' => 'ẻ', 'Ẽ' => 'ẽ', 'Ế' => 'ế', 'Ề' => 'ề', 'Ể' => 'ể', 'Ễ' => 'ễ', 'Ệ' => 'ệ', 'Ỉ' => 'ỉ', 'Ị' => 'ị', 'Ọ' => 'ọ', 'Ỏ' => 'ỏ', 'Ố' => 'ố', 'Ồ' => 'ồ', 'Ổ' => 'ổ', 'Ỗ' => 'ỗ', 'Ộ' => 'ộ', 'Ớ' => 'ớ', 'Ờ' => 'ờ', 'Ở' => 'ở', 'Ỡ' => 'ỡ', 'Ợ' => 'ợ', 'Ụ' => 'ụ', 'Ủ' => 'ủ', 'Ứ' => 'ứ', 'Ừ' => 'ừ', 'Ử' => 'ử', 'Ữ' => 'ữ', 'Ự' => 'ự', 'Ỳ' => 'ỳ', 'Ỵ' => 'ỵ', 'Ỷ' => 'ỷ', 'Ỹ' => 'ỹ', 'Ỻ' => 'ỻ', 'Ỽ' => 'ỽ', 'Ỿ' => 'ỿ', 'Ἀ' => 'ἀ', 'Ἁ' => 'ἁ', 'Ἂ' => 'ἂ', 'Ἃ' => 'ἃ', 'Ἄ' => 'ἄ', 'Ἅ' => 'ἅ', 'Ἆ' => 'ἆ', 'Ἇ' => 'ἇ', 'Ἐ' => 'ἐ', 'Ἑ' => 'ἑ', 'Ἒ' => 'ἒ', 'Ἓ' => 'ἓ', 'Ἔ' => 'ἔ', 'Ἕ' => 'ἕ', 'Ἠ' => 'ἠ', 'Ἡ' => 'ἡ', 'Ἢ' => 'ἢ', 'Ἣ' => 'ἣ', 'Ἤ' => 'ἤ', 'Ἥ' => 'ἥ', 'Ἦ' => 'ἦ', 'Ἧ' => 'ἧ', 'Ἰ' => 'ἰ', 'Ἱ' => 'ἱ', 'Ἲ' => 'ἲ', 'Ἳ' => 'ἳ', 'Ἴ' => 'ἴ', 'Ἵ' => 'ἵ', 'Ἶ' => 'ἶ', 'Ἷ' => 'ἷ', 'Ὀ' => 'ὀ', 'Ὁ' => 'ὁ', 'Ὂ' => 'ὂ', 'Ὃ' => 'ὃ', 'Ὄ' => 'ὄ', 'Ὅ' => 'ὅ', 'Ὑ' => 'ὑ', 'Ὓ' => 'ὓ', 'Ὕ' => 'ὕ', 'Ὗ' => 'ὗ', 'Ὠ' => 'ὠ', 'Ὡ' => 'ὡ', 'Ὢ' => 'ὢ', 'Ὣ' => 'ὣ', 'Ὤ' => 'ὤ', 'Ὥ' => 'ὥ', 'Ὦ' => 'ὦ', 'Ὧ' => 'ὧ', 'ᾈ' => 'ᾀ', 'ᾉ' => 'ᾁ', 'ᾊ' => 'ᾂ', 'ᾋ' => 'ᾃ', 'ᾌ' => 'ᾄ', 'ᾍ' => 'ᾅ', 'ᾎ' => 'ᾆ', 'ᾏ' => 'ᾇ', 'ᾘ' => 'ᾐ', 'ᾙ' => 'ᾑ', 'ᾚ' => 'ᾒ', 'ᾛ' => 'ᾓ', 'ᾜ' => 'ᾔ', 'ᾝ' => 'ᾕ', 'ᾞ' => 'ᾖ', 'ᾟ' => 'ᾗ', 'ᾨ' => 'ᾠ', 'ᾩ' => 'ᾡ', 'ᾪ' => 'ᾢ', 'ᾫ' => 'ᾣ', 'ᾬ' => 'ᾤ', 'ᾭ' => 'ᾥ', 'ᾮ' => 'ᾦ', 'ᾯ' => 'ᾧ', 'Ᾰ' => 'ᾰ', 'Ᾱ' => 'ᾱ', 'Ὰ' => 'ὰ', 'Ά' => 'ά', 'ᾼ' => 'ᾳ', 'Ὲ' => 'ὲ', 'Έ' => 'έ', 'Ὴ' => 'ὴ', 'Ή' => 'ή', 'ῌ' => 'ῃ', 'Ῐ' => 'ῐ', 'Ῑ' => 'ῑ', 'Ὶ' => 'ὶ', 'Ί' => 'ί', 'Ῠ' => 'ῠ', 'Ῡ' => 'ῡ', 'Ὺ' => 'ὺ', 'Ύ' => 'ύ', 'Ῥ' => 'ῥ', 'Ὸ' => 'ὸ', 'Ό' => 'ό', 'Ὼ' => 'ὼ', 'Ώ' => 'ώ', 'ῼ' => 'ῳ', 'Ω' => 'ω', 'K' => 'k', 'Å' => 'å', 'Ⅎ' => 'ⅎ', 'Ⅰ' => 'ⅰ', 'Ⅱ' => 'ⅱ', 'Ⅲ' => 'ⅲ', 'Ⅳ' => 'ⅳ', 'Ⅴ' => 'ⅴ', 'Ⅵ' => 'ⅵ', 'Ⅶ' => 'ⅶ', 'Ⅷ' => 'ⅷ', 'Ⅸ' => 'ⅸ', 'Ⅹ' => 'ⅹ', 'Ⅺ' => 'ⅺ', 'Ⅻ' => 'ⅻ', 'Ⅼ' => 'ⅼ', 'Ⅽ' => 'ⅽ', 'Ⅾ' => 'ⅾ', 'Ⅿ' => 'ⅿ', 'Ↄ' => 'ↄ', 'Ⓐ' => 'ⓐ', 'Ⓑ' => 'ⓑ', 'Ⓒ' => 'ⓒ', 'Ⓓ' => 'ⓓ', 'Ⓔ' => 'ⓔ', 'Ⓕ' => 'ⓕ', 'Ⓖ' => 'ⓖ', 'Ⓗ' => 'ⓗ', 'Ⓘ' => 'ⓘ', 'Ⓙ' => 'ⓙ', 'Ⓚ' => 'ⓚ', 'Ⓛ' => 'ⓛ', 'Ⓜ' => 'ⓜ', 'Ⓝ' => 'ⓝ', 'Ⓞ' => 'ⓞ', 'Ⓟ' => 'ⓟ', 'Ⓠ' => 'ⓠ', 'Ⓡ' => 'ⓡ', 'Ⓢ' => 'ⓢ', 'Ⓣ' => 'ⓣ', 'Ⓤ' => 'ⓤ', 'Ⓥ' => 'ⓥ', 'Ⓦ' => 'ⓦ', 'Ⓧ' => 'ⓧ', 'Ⓨ' => 'ⓨ', 'Ⓩ' => 'ⓩ', 'Ⰰ' => 'ⰰ', 'Ⰱ' => 'ⰱ', 'Ⰲ' => 'ⰲ', 'Ⰳ' => 'ⰳ', 'Ⰴ' => 'ⰴ', 'Ⰵ' => 'ⰵ', 'Ⰶ' => 'ⰶ', 'Ⰷ' => 'ⰷ', 'Ⰸ' => 'ⰸ', 'Ⰹ' => 'ⰹ', 'Ⰺ' => 'ⰺ', 'Ⰻ' => 'ⰻ', 'Ⰼ' => 'ⰼ', 'Ⰽ' => 'ⰽ', 'Ⰾ' => 'ⰾ', 'Ⰿ' => 'ⰿ', 'Ⱀ' => 'ⱀ', 'Ⱁ' => 'ⱁ', 'Ⱂ' => 'ⱂ', 'Ⱃ' => 'ⱃ', 'Ⱄ' => 'ⱄ', 'Ⱅ' => 'ⱅ', 'Ⱆ' => 'ⱆ', 'Ⱇ' => 'ⱇ', 'Ⱈ' => 'ⱈ', 'Ⱉ' => 'ⱉ', 'Ⱊ' => 'ⱊ', 'Ⱋ' => 'ⱋ', 'Ⱌ' => 'ⱌ', 'Ⱍ' => 'ⱍ', 'Ⱎ' => 'ⱎ', 'Ⱏ' => 'ⱏ', 'Ⱐ' => 'ⱐ', 'Ⱑ' => 'ⱑ', 'Ⱒ' => 'ⱒ', 'Ⱓ' => 'ⱓ', 'Ⱔ' => 'ⱔ', 'Ⱕ' => 'ⱕ', 'Ⱖ' => 'ⱖ', 'Ⱗ' => 'ⱗ', 'Ⱘ' => 'ⱘ', 'Ⱙ' => 'ⱙ', 'Ⱚ' => 'ⱚ', 'Ⱛ' => 'ⱛ', 'Ⱜ' => 'ⱜ', 'Ⱝ' => 'ⱝ', 'Ⱞ' => 'ⱞ', 'Ⱡ' => 'ⱡ', 'Ɫ' => 'ɫ', 'Ᵽ' => 'ᵽ', 'Ɽ' => 'ɽ', 'Ⱨ' => 'ⱨ', 'Ⱪ' => 'ⱪ', 'Ⱬ' => 'ⱬ', 'Ɑ' => 'ɑ', 'Ɱ' => 'ɱ', 'Ɐ' => 'ɐ', 'Ɒ' => 'ɒ', 'Ⱳ' => 'ⱳ', 'Ⱶ' => 'ⱶ', 'Ȿ' => 'ȿ', 'Ɀ' => 'ɀ', 'Ⲁ' => 'ⲁ', 'Ⲃ' => 'ⲃ', 'Ⲅ' => 'ⲅ', 'Ⲇ' => 'ⲇ', 'Ⲉ' => 'ⲉ', 'Ⲋ' => 'ⲋ', 'Ⲍ' => 'ⲍ', 'Ⲏ' => 'ⲏ', 'Ⲑ' => 'ⲑ', 'Ⲓ' => 'ⲓ', 'Ⲕ' => 'ⲕ', 'Ⲗ' => 'ⲗ', 'Ⲙ' => 'ⲙ', 'Ⲛ' => 'ⲛ', 'Ⲝ' => 'ⲝ', 'Ⲟ' => 'ⲟ', 'Ⲡ' => 'ⲡ', 'Ⲣ' => 'ⲣ', 'Ⲥ' => 'ⲥ', 'Ⲧ' => 'ⲧ', 'Ⲩ' => 'ⲩ', 'Ⲫ' => 'ⲫ', 'Ⲭ' => 'ⲭ', 'Ⲯ' => 'ⲯ', 'Ⲱ' => 'ⲱ', 'Ⲳ' => 'ⲳ', 'Ⲵ' => 'ⲵ', 'Ⲷ' => 'ⲷ', 'Ⲹ' => 'ⲹ', 'Ⲻ' => 'ⲻ', 'Ⲽ' => 'ⲽ', 'Ⲿ' => 'ⲿ', 'Ⳁ' => 'ⳁ', 'Ⳃ' => 'ⳃ', 'Ⳅ' => 'ⳅ', 'Ⳇ' => 'ⳇ', 'Ⳉ' => 'ⳉ', 'Ⳋ' => 'ⳋ', 'Ⳍ' => 'ⳍ', 'Ⳏ' => 'ⳏ', 'Ⳑ' => 'ⳑ', 'Ⳓ' => 'ⳓ', 'Ⳕ' => 'ⳕ', 'Ⳗ' => 'ⳗ', 'Ⳙ' => 'ⳙ', 'Ⳛ' => 'ⳛ', 'Ⳝ' => 'ⳝ', 'Ⳟ' => 'ⳟ', 'Ⳡ' => 'ⳡ', 'Ⳣ' => 'ⳣ', 'Ⳬ' => 'ⳬ', 'Ⳮ' => 'ⳮ', 'Ⳳ' => 'ⳳ', 'Ꙁ' => 'ꙁ', 'Ꙃ' => 'ꙃ', 'Ꙅ' => 'ꙅ', 'Ꙇ' => 'ꙇ', 'Ꙉ' => 'ꙉ', 'Ꙋ' => 'ꙋ', 'Ꙍ' => 'ꙍ', 'Ꙏ' => 'ꙏ', 'Ꙑ' => 'ꙑ', 'Ꙓ' => 'ꙓ', 'Ꙕ' => 'ꙕ', 'Ꙗ' => 'ꙗ', 'Ꙙ' => 'ꙙ', 'Ꙛ' => 'ꙛ', 'Ꙝ' => 'ꙝ', 'Ꙟ' => 'ꙟ', 'Ꙡ' => 'ꙡ', 'Ꙣ' => 'ꙣ', 'Ꙥ' => 'ꙥ', 'Ꙧ' => 'ꙧ', 'Ꙩ' => 'ꙩ', 'Ꙫ' => 'ꙫ', 'Ꙭ' => 'ꙭ', 'Ꚁ' => 'ꚁ', 'Ꚃ' => 'ꚃ', 'Ꚅ' => 'ꚅ', 'Ꚇ' => 'ꚇ', 'Ꚉ' => 'ꚉ', 'Ꚋ' => 'ꚋ', 'Ꚍ' => 'ꚍ', 'Ꚏ' => 'ꚏ', 'Ꚑ' => 'ꚑ', 'Ꚓ' => 'ꚓ', 'Ꚕ' => 'ꚕ', 'Ꚗ' => 'ꚗ', 'Ꚙ' => 'ꚙ', 'Ꚛ' => 'ꚛ', 'Ꜣ' => 'ꜣ', 'Ꜥ' => 'ꜥ', 'Ꜧ' => 'ꜧ', 'Ꜩ' => 'ꜩ', 'Ꜫ' => 'ꜫ', 'Ꜭ' => 'ꜭ', 'Ꜯ' => 'ꜯ', 'Ꜳ' => 'ꜳ', 'Ꜵ' => 'ꜵ', 'Ꜷ' => 'ꜷ', 'Ꜹ' => 'ꜹ', 'Ꜻ' => 'ꜻ', 'Ꜽ' => 'ꜽ', 'Ꜿ' => 'ꜿ', 'Ꝁ' => 'ꝁ', 'Ꝃ' => 'ꝃ', 'Ꝅ' => 'ꝅ', 'Ꝇ' => 'ꝇ', 'Ꝉ' => 'ꝉ', 'Ꝋ' => 'ꝋ', 'Ꝍ' => 'ꝍ', 'Ꝏ' => 'ꝏ', 'Ꝑ' => 'ꝑ', 'Ꝓ' => 'ꝓ', 'Ꝕ' => 'ꝕ', 'Ꝗ' => 'ꝗ', 'Ꝙ' => 'ꝙ', 'Ꝛ' => 'ꝛ', 'Ꝝ' => 'ꝝ', 'Ꝟ' => 'ꝟ', 'Ꝡ' => 'ꝡ', 'Ꝣ' => 'ꝣ', 'Ꝥ' => 'ꝥ', 'Ꝧ' => 'ꝧ', 'Ꝩ' => 'ꝩ', 'Ꝫ' => 'ꝫ', 'Ꝭ' => 'ꝭ', 'Ꝯ' => 'ꝯ', 'Ꝺ' => 'ꝺ', 'Ꝼ' => 'ꝼ', 'Ᵹ' => 'ᵹ', 'Ꝿ' => 'ꝿ', 'Ꞁ' => 'ꞁ', 'Ꞃ' => 'ꞃ', 'Ꞅ' => 'ꞅ', 'Ꞇ' => 'ꞇ', 'Ꞌ' => 'ꞌ', 'Ɥ' => 'ɥ', 'Ꞑ' => 'ꞑ', 'Ꞓ' => 'ꞓ', 'Ꞗ' => 'ꞗ', 'Ꞙ' => 'ꞙ', 'Ꞛ' => 'ꞛ', 'Ꞝ' => 'ꞝ', 'Ꞟ' => 'ꞟ', 'Ꞡ' => 'ꞡ', 'Ꞣ' => 'ꞣ', 'Ꞥ' => 'ꞥ', 'Ꞧ' => 'ꞧ', 'Ꞩ' => 'ꞩ', 'Ɦ' => 'ɦ', 'Ɜ' => 'ɜ', 'Ɡ' => 'ɡ', 'Ɬ' => 'ɬ', 'Ɪ' => 'ɪ', 'Ʞ' => 'ʞ', 'Ʇ' => 'ʇ', 'Ʝ' => 'ʝ', 'Ꭓ' => 'ꭓ', 'Ꞵ' => 'ꞵ', 'Ꞷ' => 'ꞷ', 'Ꞹ' => 'ꞹ', 'Ꞻ' => 'ꞻ', 'Ꞽ' => 'ꞽ', 'Ꞿ' => 'ꞿ', 'Ꟃ' => 'ꟃ', 'Ꞔ' => 'ꞔ', 'Ʂ' => 'ʂ', 'Ᶎ' => 'ᶎ', 'Ꟈ' => 'ꟈ', 'Ꟊ' => 'ꟊ', 'Ꟶ' => 'ꟶ', 'A' => 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', 'E' => 'e', 'F' => 'f', 'G' => 'g', 'H' => 'h', 'I' => 'i', 'J' => 'j', 'K' => 'k', 'L' => 'l', 'M' => 'm', 'N' => 'n', 'O' => 'o', 'P' => 'p', 'Q' => 'q', 'R' => 'r', 'S' => 's', 'T' => 't', 'U' => 'u', 'V' => 'v', 'W' => 'w', 'X' => 'x', 'Y' => 'y', 'Z' => 'z', '𐐀' => '𐐨', '𐐁' => '𐐩', '𐐂' => '𐐪', '𐐃' => '𐐫', '𐐄' => '𐐬', '𐐅' => '𐐭', '𐐆' => '𐐮', '𐐇' => '𐐯', '𐐈' => '𐐰', '𐐉' => '𐐱', '𐐊' => '𐐲', '𐐋' => '𐐳', '𐐌' => '𐐴', '𐐍' => '𐐵', '𐐎' => '𐐶', '𐐏' => '𐐷', '𐐐' => '𐐸', '𐐑' => '𐐹', '𐐒' => '𐐺', '𐐓' => '𐐻', '𐐔' => '𐐼', '𐐕' => '𐐽', '𐐖' => '𐐾', '𐐗' => '𐐿', '𐐘' => '𐑀', '𐐙' => '𐑁', '𐐚' => '𐑂', '𐐛' => '𐑃', '𐐜' => '𐑄', '𐐝' => '𐑅', '𐐞' => '𐑆', '𐐟' => '𐑇', '𐐠' => '𐑈', '𐐡' => '𐑉', '𐐢' => '𐑊', '𐐣' => '𐑋', '𐐤' => '𐑌', '𐐥' => '𐑍', '𐐦' => '𐑎', '𐐧' => '𐑏', '𐒰' => '𐓘', '𐒱' => '𐓙', '𐒲' => '𐓚', '𐒳' => '𐓛', '𐒴' => '𐓜', '𐒵' => '𐓝', '𐒶' => '𐓞', '𐒷' => '𐓟', '𐒸' => '𐓠', '𐒹' => '𐓡', '𐒺' => '𐓢', '𐒻' => '𐓣', '𐒼' => '𐓤', '𐒽' => '𐓥', '𐒾' => '𐓦', '𐒿' => '𐓧', '𐓀' => '𐓨', '𐓁' => '𐓩', '𐓂' => '𐓪', '𐓃' => '𐓫', '𐓄' => '𐓬', '𐓅' => '𐓭', '𐓆' => '𐓮', '𐓇' => '𐓯', '𐓈' => '𐓰', '𐓉' => '𐓱', '𐓊' => '𐓲', '𐓋' => '𐓳', '𐓌' => '𐓴', '𐓍' => '𐓵', '𐓎' => '𐓶', '𐓏' => '𐓷', '𐓐' => '𐓸', '𐓑' => '𐓹', '𐓒' => '𐓺', '𐓓' => '𐓻', '𐲀' => '𐳀', '𐲁' => '𐳁', '𐲂' => '𐳂', '𐲃' => '𐳃', '𐲄' => '𐳄', '𐲅' => '𐳅', '𐲆' => '𐳆', '𐲇' => '𐳇', '𐲈' => '𐳈', '𐲉' => '𐳉', '𐲊' => '𐳊', '𐲋' => '𐳋', '𐲌' => '𐳌', '𐲍' => '𐳍', '𐲎' => '𐳎', '𐲏' => '𐳏', '𐲐' => '𐳐', '𐲑' => '𐳑', '𐲒' => '𐳒', '𐲓' => '𐳓', '𐲔' => '𐳔', '𐲕' => '𐳕', '𐲖' => '𐳖', '𐲗' => '𐳗', '𐲘' => '𐳘', '𐲙' => '𐳙', '𐲚' => '𐳚', '𐲛' => '𐳛', '𐲜' => '𐳜', '𐲝' => '𐳝', '𐲞' => '𐳞', '𐲟' => '𐳟', '𐲠' => '𐳠', '𐲡' => '𐳡', '𐲢' => '𐳢', '𐲣' => '𐳣', '𐲤' => '𐳤', '𐲥' => '𐳥', '𐲦' => '𐳦', '𐲧' => '𐳧', '𐲨' => '𐳨', '𐲩' => '𐳩', '𐲪' => '𐳪', '𐲫' => '𐳫', '𐲬' => '𐳬', '𐲭' => '𐳭', '𐲮' => '𐳮', '𐲯' => '𐳯', '𐲰' => '𐳰', '𐲱' => '𐳱', '𐲲' => '𐳲', '𑢠' => '𑣀', '𑢡' => '𑣁', '𑢢' => '𑣂', '𑢣' => '𑣃', '𑢤' => '𑣄', '𑢥' => '𑣅', '𑢦' => '𑣆', '𑢧' => '𑣇', '𑢨' => '𑣈', '𑢩' => '𑣉', '𑢪' => '𑣊', '𑢫' => '𑣋', '𑢬' => '𑣌', '𑢭' => '𑣍', '𑢮' => '𑣎', '𑢯' => '𑣏', '𑢰' => '𑣐', '𑢱' => '𑣑', '𑢲' => '𑣒', '𑢳' => '𑣓', '𑢴' => '𑣔', '𑢵' => '𑣕', '𑢶' => '𑣖', '𑢷' => '𑣗', '𑢸' => '𑣘', '𑢹' => '𑣙', '𑢺' => '𑣚', '𑢻' => '𑣛', '𑢼' => '𑣜', '𑢽' => '𑣝', '𑢾' => '𑣞', '𑢿' => '𑣟', '𖹀' => '𖹠', '𖹁' => '𖹡', '𖹂' => '𖹢', '𖹃' => '𖹣', '𖹄' => '𖹤', '𖹅' => '𖹥', '𖹆' => '𖹦', '𖹇' => '𖹧', '𖹈' => '𖹨', '𖹉' => '𖹩', '𖹊' => '𖹪', '𖹋' => '𖹫', '𖹌' => '𖹬', '𖹍' => '𖹭', '𖹎' => '𖹮', '𖹏' => '𖹯', '𖹐' => '𖹰', '𖹑' => '𖹱', '𖹒' => '𖹲', '𖹓' => '𖹳', '𖹔' => '𖹴', '𖹕' => '𖹵', '𖹖' => '𖹶', '𖹗' => '𖹷', '𖹘' => '𖹸', '𖹙' => '𖹹', '𖹚' => '𖹺', '𖹛' => '𖹻', '𖹜' => '𖹼', '𖹝' => '𖹽', '𖹞' => '𖹾', '𖹟' => '𖹿', '𞤀' => '𞤢', '𞤁' => '𞤣', '𞤂' => '𞤤', '𞤃' => '𞤥', '𞤄' => '𞤦', '𞤅' => '𞤧', '𞤆' => '𞤨', '𞤇' => '𞤩', '𞤈' => '𞤪', '𞤉' => '𞤫', '𞤊' => '𞤬', '𞤋' => '𞤭', '𞤌' => '𞤮', '𞤍' => '𞤯', '𞤎' => '𞤰', '𞤏' => '𞤱', '𞤐' => '𞤲', '𞤑' => '𞤳', '𞤒' => '𞤴', '𞤓' => '𞤵', '𞤔' => '𞤶', '𞤕' => '𞤷', '𞤖' => '𞤸', '𞤗' => '𞤹', '𞤘' => '𞤺', '𞤙' => '𞤻', '𞤚' => '𞤼', '𞤛' => '𞤽', '𞤜' => '𞤾', '𞤝' => '𞤿', '𞤞' => '𞥀', '𞤟' => '𞥁', '𞤠' => '𞥂', '𞤡' => '𞥃', ); polyfill-mbstring/Resources/unidata/titleCaseRegexp.php 0000644 00000014071 15025017654 0017431 0 ustar 00 <?php // from Case_Ignorable in https://unicode.org/Public/UNIDATA/DerivedCoreProperties.txt return '/(?<![\x{0027}\x{002E}\x{003A}\x{005E}\x{0060}\x{00A8}\x{00AD}\x{00AF}\x{00B4}\x{00B7}\x{00B8}\x{02B0}-\x{02C1}\x{02C2}-\x{02C5}\x{02C6}-\x{02D1}\x{02D2}-\x{02DF}\x{02E0}-\x{02E4}\x{02E5}-\x{02EB}\x{02EC}\x{02ED}\x{02EE}\x{02EF}-\x{02FF}\x{0300}-\x{036F}\x{0374}\x{0375}\x{037A}\x{0384}-\x{0385}\x{0387}\x{0483}-\x{0487}\x{0488}-\x{0489}\x{0559}\x{0591}-\x{05BD}\x{05BF}\x{05C1}-\x{05C2}\x{05C4}-\x{05C5}\x{05C7}\x{05F4}\x{0600}-\x{0605}\x{0610}-\x{061A}\x{061C}\x{0640}\x{064B}-\x{065F}\x{0670}\x{06D6}-\x{06DC}\x{06DD}\x{06DF}-\x{06E4}\x{06E5}-\x{06E6}\x{06E7}-\x{06E8}\x{06EA}-\x{06ED}\x{070F}\x{0711}\x{0730}-\x{074A}\x{07A6}-\x{07B0}\x{07EB}-\x{07F3}\x{07F4}-\x{07F5}\x{07FA}\x{07FD}\x{0816}-\x{0819}\x{081A}\x{081B}-\x{0823}\x{0824}\x{0825}-\x{0827}\x{0828}\x{0829}-\x{082D}\x{0859}-\x{085B}\x{08D3}-\x{08E1}\x{08E2}\x{08E3}-\x{0902}\x{093A}\x{093C}\x{0941}-\x{0948}\x{094D}\x{0951}-\x{0957}\x{0962}-\x{0963}\x{0971}\x{0981}\x{09BC}\x{09C1}-\x{09C4}\x{09CD}\x{09E2}-\x{09E3}\x{09FE}\x{0A01}-\x{0A02}\x{0A3C}\x{0A41}-\x{0A42}\x{0A47}-\x{0A48}\x{0A4B}-\x{0A4D}\x{0A51}\x{0A70}-\x{0A71}\x{0A75}\x{0A81}-\x{0A82}\x{0ABC}\x{0AC1}-\x{0AC5}\x{0AC7}-\x{0AC8}\x{0ACD}\x{0AE2}-\x{0AE3}\x{0AFA}-\x{0AFF}\x{0B01}\x{0B3C}\x{0B3F}\x{0B41}-\x{0B44}\x{0B4D}\x{0B56}\x{0B62}-\x{0B63}\x{0B82}\x{0BC0}\x{0BCD}\x{0C00}\x{0C04}\x{0C3E}-\x{0C40}\x{0C46}-\x{0C48}\x{0C4A}-\x{0C4D}\x{0C55}-\x{0C56}\x{0C62}-\x{0C63}\x{0C81}\x{0CBC}\x{0CBF}\x{0CC6}\x{0CCC}-\x{0CCD}\x{0CE2}-\x{0CE3}\x{0D00}-\x{0D01}\x{0D3B}-\x{0D3C}\x{0D41}-\x{0D44}\x{0D4D}\x{0D62}-\x{0D63}\x{0DCA}\x{0DD2}-\x{0DD4}\x{0DD6}\x{0E31}\x{0E34}-\x{0E3A}\x{0E46}\x{0E47}-\x{0E4E}\x{0EB1}\x{0EB4}-\x{0EB9}\x{0EBB}-\x{0EBC}\x{0EC6}\x{0EC8}-\x{0ECD}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F39}\x{0F71}-\x{0F7E}\x{0F80}-\x{0F84}\x{0F86}-\x{0F87}\x{0F8D}-\x{0F97}\x{0F99}-\x{0FBC}\x{0FC6}\x{102D}-\x{1030}\x{1032}-\x{1037}\x{1039}-\x{103A}\x{103D}-\x{103E}\x{1058}-\x{1059}\x{105E}-\x{1060}\x{1071}-\x{1074}\x{1082}\x{1085}-\x{1086}\x{108D}\x{109D}\x{10FC}\x{135D}-\x{135F}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}-\x{1753}\x{1772}-\x{1773}\x{17B4}-\x{17B5}\x{17B7}-\x{17BD}\x{17C6}\x{17C9}-\x{17D3}\x{17D7}\x{17DD}\x{180B}-\x{180D}\x{180E}\x{1843}\x{1885}-\x{1886}\x{18A9}\x{1920}-\x{1922}\x{1927}-\x{1928}\x{1932}\x{1939}-\x{193B}\x{1A17}-\x{1A18}\x{1A1B}\x{1A56}\x{1A58}-\x{1A5E}\x{1A60}\x{1A62}\x{1A65}-\x{1A6C}\x{1A73}-\x{1A7C}\x{1A7F}\x{1AA7}\x{1AB0}-\x{1ABD}\x{1ABE}\x{1B00}-\x{1B03}\x{1B34}\x{1B36}-\x{1B3A}\x{1B3C}\x{1B42}\x{1B6B}-\x{1B73}\x{1B80}-\x{1B81}\x{1BA2}-\x{1BA5}\x{1BA8}-\x{1BA9}\x{1BAB}-\x{1BAD}\x{1BE6}\x{1BE8}-\x{1BE9}\x{1BED}\x{1BEF}-\x{1BF1}\x{1C2C}-\x{1C33}\x{1C36}-\x{1C37}\x{1C78}-\x{1C7D}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE0}\x{1CE2}-\x{1CE8}\x{1CED}\x{1CF4}\x{1CF8}-\x{1CF9}\x{1D2C}-\x{1D6A}\x{1D78}\x{1D9B}-\x{1DBF}\x{1DC0}-\x{1DF9}\x{1DFB}-\x{1DFF}\x{1FBD}\x{1FBF}-\x{1FC1}\x{1FCD}-\x{1FCF}\x{1FDD}-\x{1FDF}\x{1FED}-\x{1FEF}\x{1FFD}-\x{1FFE}\x{200B}-\x{200F}\x{2018}\x{2019}\x{2024}\x{2027}\x{202A}-\x{202E}\x{2060}-\x{2064}\x{2066}-\x{206F}\x{2071}\x{207F}\x{2090}-\x{209C}\x{20D0}-\x{20DC}\x{20DD}-\x{20E0}\x{20E1}\x{20E2}-\x{20E4}\x{20E5}-\x{20F0}\x{2C7C}-\x{2C7D}\x{2CEF}-\x{2CF1}\x{2D6F}\x{2D7F}\x{2DE0}-\x{2DFF}\x{2E2F}\x{3005}\x{302A}-\x{302D}\x{3031}-\x{3035}\x{303B}\x{3099}-\x{309A}\x{309B}-\x{309C}\x{309D}-\x{309E}\x{30FC}-\x{30FE}\x{A015}\x{A4F8}-\x{A4FD}\x{A60C}\x{A66F}\x{A670}-\x{A672}\x{A674}-\x{A67D}\x{A67F}\x{A69C}-\x{A69D}\x{A69E}-\x{A69F}\x{A6F0}-\x{A6F1}\x{A700}-\x{A716}\x{A717}-\x{A71F}\x{A720}-\x{A721}\x{A770}\x{A788}\x{A789}-\x{A78A}\x{A7F8}-\x{A7F9}\x{A802}\x{A806}\x{A80B}\x{A825}-\x{A826}\x{A8C4}-\x{A8C5}\x{A8E0}-\x{A8F1}\x{A8FF}\x{A926}-\x{A92D}\x{A947}-\x{A951}\x{A980}-\x{A982}\x{A9B3}\x{A9B6}-\x{A9B9}\x{A9BC}\x{A9CF}\x{A9E5}\x{A9E6}\x{AA29}-\x{AA2E}\x{AA31}-\x{AA32}\x{AA35}-\x{AA36}\x{AA43}\x{AA4C}\x{AA70}\x{AA7C}\x{AAB0}\x{AAB2}-\x{AAB4}\x{AAB7}-\x{AAB8}\x{AABE}-\x{AABF}\x{AAC1}\x{AADD}\x{AAEC}-\x{AAED}\x{AAF3}-\x{AAF4}\x{AAF6}\x{AB5B}\x{AB5C}-\x{AB5F}\x{ABE5}\x{ABE8}\x{ABED}\x{FB1E}\x{FBB2}-\x{FBC1}\x{FE00}-\x{FE0F}\x{FE13}\x{FE20}-\x{FE2F}\x{FE52}\x{FE55}\x{FEFF}\x{FF07}\x{FF0E}\x{FF1A}\x{FF3E}\x{FF40}\x{FF70}\x{FF9E}-\x{FF9F}\x{FFE3}\x{FFF9}-\x{FFFB}\x{101FD}\x{102E0}\x{10376}-\x{1037A}\x{10A01}-\x{10A03}\x{10A05}-\x{10A06}\x{10A0C}-\x{10A0F}\x{10A38}-\x{10A3A}\x{10A3F}\x{10AE5}-\x{10AE6}\x{10D24}-\x{10D27}\x{10F46}-\x{10F50}\x{11001}\x{11038}-\x{11046}\x{1107F}-\x{11081}\x{110B3}-\x{110B6}\x{110B9}-\x{110BA}\x{110BD}\x{110CD}\x{11100}-\x{11102}\x{11127}-\x{1112B}\x{1112D}-\x{11134}\x{11173}\x{11180}-\x{11181}\x{111B6}-\x{111BE}\x{111C9}-\x{111CC}\x{1122F}-\x{11231}\x{11234}\x{11236}-\x{11237}\x{1123E}\x{112DF}\x{112E3}-\x{112EA}\x{11300}-\x{11301}\x{1133B}-\x{1133C}\x{11340}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{11438}-\x{1143F}\x{11442}-\x{11444}\x{11446}\x{1145E}\x{114B3}-\x{114B8}\x{114BA}\x{114BF}-\x{114C0}\x{114C2}-\x{114C3}\x{115B2}-\x{115B5}\x{115BC}-\x{115BD}\x{115BF}-\x{115C0}\x{115DC}-\x{115DD}\x{11633}-\x{1163A}\x{1163D}\x{1163F}-\x{11640}\x{116AB}\x{116AD}\x{116B0}-\x{116B5}\x{116B7}\x{1171D}-\x{1171F}\x{11722}-\x{11725}\x{11727}-\x{1172B}\x{1182F}-\x{11837}\x{11839}-\x{1183A}\x{11A01}-\x{11A0A}\x{11A33}-\x{11A38}\x{11A3B}-\x{11A3E}\x{11A47}\x{11A51}-\x{11A56}\x{11A59}-\x{11A5B}\x{11A8A}-\x{11A96}\x{11A98}-\x{11A99}\x{11C30}-\x{11C36}\x{11C38}-\x{11C3D}\x{11C3F}\x{11C92}-\x{11CA7}\x{11CAA}-\x{11CB0}\x{11CB2}-\x{11CB3}\x{11CB5}-\x{11CB6}\x{11D31}-\x{11D36}\x{11D3A}\x{11D3C}-\x{11D3D}\x{11D3F}-\x{11D45}\x{11D47}\x{11D90}-\x{11D91}\x{11D95}\x{11D97}\x{11EF3}-\x{11EF4}\x{16AF0}-\x{16AF4}\x{16B30}-\x{16B36}\x{16B40}-\x{16B43}\x{16F8F}-\x{16F92}\x{16F93}-\x{16F9F}\x{16FE0}-\x{16FE1}\x{1BC9D}-\x{1BC9E}\x{1BCA0}-\x{1BCA3}\x{1D167}-\x{1D169}\x{1D173}-\x{1D17A}\x{1D17B}-\x{1D182}\x{1D185}-\x{1D18B}\x{1D1AA}-\x{1D1AD}\x{1D242}-\x{1D244}\x{1DA00}-\x{1DA36}\x{1DA3B}-\x{1DA6C}\x{1DA75}\x{1DA84}\x{1DA9B}-\x{1DA9F}\x{1DAA1}-\x{1DAAF}\x{1E000}-\x{1E006}\x{1E008}-\x{1E018}\x{1E01B}-\x{1E021}\x{1E023}-\x{1E024}\x{1E026}-\x{1E02A}\x{1E8D0}-\x{1E8D6}\x{1E944}-\x{1E94A}\x{1F3FB}-\x{1F3FF}\x{E0001}\x{E0020}-\x{E007F}\x{E0100}-\x{E01EF}])(\pL)(\pL*+)/u'; polyfill-mbstring/Resources/unidata/upperCase.php 0000644 00000063322 15025017654 0016273 0 ustar 00 <?php return array ( 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D', 'e' => 'E', 'f' => 'F', 'g' => 'G', 'h' => 'H', 'i' => 'I', 'j' => 'J', 'k' => 'K', 'l' => 'L', 'm' => 'M', 'n' => 'N', 'o' => 'O', 'p' => 'P', 'q' => 'Q', 'r' => 'R', 's' => 'S', 't' => 'T', 'u' => 'U', 'v' => 'V', 'w' => 'W', 'x' => 'X', 'y' => 'Y', 'z' => 'Z', 'µ' => 'Μ', 'à' => 'À', 'á' => 'Á', 'â' => 'Â', 'ã' => 'Ã', 'ä' => 'Ä', 'å' => 'Å', 'æ' => 'Æ', 'ç' => 'Ç', 'è' => 'È', 'é' => 'É', 'ê' => 'Ê', 'ë' => 'Ë', 'ì' => 'Ì', 'í' => 'Í', 'î' => 'Î', 'ï' => 'Ï', 'ð' => 'Ð', 'ñ' => 'Ñ', 'ò' => 'Ò', 'ó' => 'Ó', 'ô' => 'Ô', 'õ' => 'Õ', 'ö' => 'Ö', 'ø' => 'Ø', 'ù' => 'Ù', 'ú' => 'Ú', 'û' => 'Û', 'ü' => 'Ü', 'ý' => 'Ý', 'þ' => 'Þ', 'ÿ' => 'Ÿ', 'ā' => 'Ā', 'ă' => 'Ă', 'ą' => 'Ą', 'ć' => 'Ć', 'ĉ' => 'Ĉ', 'ċ' => 'Ċ', 'č' => 'Č', 'ď' => 'Ď', 'đ' => 'Đ', 'ē' => 'Ē', 'ĕ' => 'Ĕ', 'ė' => 'Ė', 'ę' => 'Ę', 'ě' => 'Ě', 'ĝ' => 'Ĝ', 'ğ' => 'Ğ', 'ġ' => 'Ġ', 'ģ' => 'Ģ', 'ĥ' => 'Ĥ', 'ħ' => 'Ħ', 'ĩ' => 'Ĩ', 'ī' => 'Ī', 'ĭ' => 'Ĭ', 'į' => 'Į', 'ı' => 'I', 'ij' => 'IJ', 'ĵ' => 'Ĵ', 'ķ' => 'Ķ', 'ĺ' => 'Ĺ', 'ļ' => 'Ļ', 'ľ' => 'Ľ', 'ŀ' => 'Ŀ', 'ł' => 'Ł', 'ń' => 'Ń', 'ņ' => 'Ņ', 'ň' => 'Ň', 'ŋ' => 'Ŋ', 'ō' => 'Ō', 'ŏ' => 'Ŏ', 'ő' => 'Ő', 'œ' => 'Œ', 'ŕ' => 'Ŕ', 'ŗ' => 'Ŗ', 'ř' => 'Ř', 'ś' => 'Ś', 'ŝ' => 'Ŝ', 'ş' => 'Ş', 'š' => 'Š', 'ţ' => 'Ţ', 'ť' => 'Ť', 'ŧ' => 'Ŧ', 'ũ' => 'Ũ', 'ū' => 'Ū', 'ŭ' => 'Ŭ', 'ů' => 'Ů', 'ű' => 'Ű', 'ų' => 'Ų', 'ŵ' => 'Ŵ', 'ŷ' => 'Ŷ', 'ź' => 'Ź', 'ż' => 'Ż', 'ž' => 'Ž', 'ſ' => 'S', 'ƀ' => 'Ƀ', 'ƃ' => 'Ƃ', 'ƅ' => 'Ƅ', 'ƈ' => 'Ƈ', 'ƌ' => 'Ƌ', 'ƒ' => 'Ƒ', 'ƕ' => 'Ƕ', 'ƙ' => 'Ƙ', 'ƚ' => 'Ƚ', 'ƞ' => 'Ƞ', 'ơ' => 'Ơ', 'ƣ' => 'Ƣ', 'ƥ' => 'Ƥ', 'ƨ' => 'Ƨ', 'ƭ' => 'Ƭ', 'ư' => 'Ư', 'ƴ' => 'Ƴ', 'ƶ' => 'Ƶ', 'ƹ' => 'Ƹ', 'ƽ' => 'Ƽ', 'ƿ' => 'Ƿ', 'Dž' => 'DŽ', 'dž' => 'DŽ', 'Lj' => 'LJ', 'lj' => 'LJ', 'Nj' => 'NJ', 'nj' => 'NJ', 'ǎ' => 'Ǎ', 'ǐ' => 'Ǐ', 'ǒ' => 'Ǒ', 'ǔ' => 'Ǔ', 'ǖ' => 'Ǖ', 'ǘ' => 'Ǘ', 'ǚ' => 'Ǚ', 'ǜ' => 'Ǜ', 'ǝ' => 'Ǝ', 'ǟ' => 'Ǟ', 'ǡ' => 'Ǡ', 'ǣ' => 'Ǣ', 'ǥ' => 'Ǥ', 'ǧ' => 'Ǧ', 'ǩ' => 'Ǩ', 'ǫ' => 'Ǫ', 'ǭ' => 'Ǭ', 'ǯ' => 'Ǯ', 'Dz' => 'DZ', 'dz' => 'DZ', 'ǵ' => 'Ǵ', 'ǹ' => 'Ǹ', 'ǻ' => 'Ǻ', 'ǽ' => 'Ǽ', 'ǿ' => 'Ǿ', 'ȁ' => 'Ȁ', 'ȃ' => 'Ȃ', 'ȅ' => 'Ȅ', 'ȇ' => 'Ȇ', 'ȉ' => 'Ȉ', 'ȋ' => 'Ȋ', 'ȍ' => 'Ȍ', 'ȏ' => 'Ȏ', 'ȑ' => 'Ȑ', 'ȓ' => 'Ȓ', 'ȕ' => 'Ȕ', 'ȗ' => 'Ȗ', 'ș' => 'Ș', 'ț' => 'Ț', 'ȝ' => 'Ȝ', 'ȟ' => 'Ȟ', 'ȣ' => 'Ȣ', 'ȥ' => 'Ȥ', 'ȧ' => 'Ȧ', 'ȩ' => 'Ȩ', 'ȫ' => 'Ȫ', 'ȭ' => 'Ȭ', 'ȯ' => 'Ȯ', 'ȱ' => 'Ȱ', 'ȳ' => 'Ȳ', 'ȼ' => 'Ȼ', 'ȿ' => 'Ȿ', 'ɀ' => 'Ɀ', 'ɂ' => 'Ɂ', 'ɇ' => 'Ɇ', 'ɉ' => 'Ɉ', 'ɋ' => 'Ɋ', 'ɍ' => 'Ɍ', 'ɏ' => 'Ɏ', 'ɐ' => 'Ɐ', 'ɑ' => 'Ɑ', 'ɒ' => 'Ɒ', 'ɓ' => 'Ɓ', 'ɔ' => 'Ɔ', 'ɖ' => 'Ɖ', 'ɗ' => 'Ɗ', 'ə' => 'Ə', 'ɛ' => 'Ɛ', 'ɜ' => 'Ɜ', 'ɠ' => 'Ɠ', 'ɡ' => 'Ɡ', 'ɣ' => 'Ɣ', 'ɥ' => 'Ɥ', 'ɦ' => 'Ɦ', 'ɨ' => 'Ɨ', 'ɩ' => 'Ɩ', 'ɪ' => 'Ɪ', 'ɫ' => 'Ɫ', 'ɬ' => 'Ɬ', 'ɯ' => 'Ɯ', 'ɱ' => 'Ɱ', 'ɲ' => 'Ɲ', 'ɵ' => 'Ɵ', 'ɽ' => 'Ɽ', 'ʀ' => 'Ʀ', 'ʂ' => 'Ʂ', 'ʃ' => 'Ʃ', 'ʇ' => 'Ʇ', 'ʈ' => 'Ʈ', 'ʉ' => 'Ʉ', 'ʊ' => 'Ʊ', 'ʋ' => 'Ʋ', 'ʌ' => 'Ʌ', 'ʒ' => 'Ʒ', 'ʝ' => 'Ʝ', 'ʞ' => 'Ʞ', 'ͅ' => 'Ι', 'ͱ' => 'Ͱ', 'ͳ' => 'Ͳ', 'ͷ' => 'Ͷ', 'ͻ' => 'Ͻ', 'ͼ' => 'Ͼ', 'ͽ' => 'Ͽ', 'ά' => 'Ά', 'έ' => 'Έ', 'ή' => 'Ή', 'ί' => 'Ί', 'α' => 'Α', 'β' => 'Β', 'γ' => 'Γ', 'δ' => 'Δ', 'ε' => 'Ε', 'ζ' => 'Ζ', 'η' => 'Η', 'θ' => 'Θ', 'ι' => 'Ι', 'κ' => 'Κ', 'λ' => 'Λ', 'μ' => 'Μ', 'ν' => 'Ν', 'ξ' => 'Ξ', 'ο' => 'Ο', 'π' => 'Π', 'ρ' => 'Ρ', 'ς' => 'Σ', 'σ' => 'Σ', 'τ' => 'Τ', 'υ' => 'Υ', 'φ' => 'Φ', 'χ' => 'Χ', 'ψ' => 'Ψ', 'ω' => 'Ω', 'ϊ' => 'Ϊ', 'ϋ' => 'Ϋ', 'ό' => 'Ό', 'ύ' => 'Ύ', 'ώ' => 'Ώ', 'ϐ' => 'Β', 'ϑ' => 'Θ', 'ϕ' => 'Φ', 'ϖ' => 'Π', 'ϗ' => 'Ϗ', 'ϙ' => 'Ϙ', 'ϛ' => 'Ϛ', 'ϝ' => 'Ϝ', 'ϟ' => 'Ϟ', 'ϡ' => 'Ϡ', 'ϣ' => 'Ϣ', 'ϥ' => 'Ϥ', 'ϧ' => 'Ϧ', 'ϩ' => 'Ϩ', 'ϫ' => 'Ϫ', 'ϭ' => 'Ϭ', 'ϯ' => 'Ϯ', 'ϰ' => 'Κ', 'ϱ' => 'Ρ', 'ϲ' => 'Ϲ', 'ϳ' => 'Ϳ', 'ϵ' => 'Ε', 'ϸ' => 'Ϸ', 'ϻ' => 'Ϻ', 'а' => 'А', 'б' => 'Б', 'в' => 'В', 'г' => 'Г', 'д' => 'Д', 'е' => 'Е', 'ж' => 'Ж', 'з' => 'З', 'и' => 'И', 'й' => 'Й', 'к' => 'К', 'л' => 'Л', 'м' => 'М', 'н' => 'Н', 'о' => 'О', 'п' => 'П', 'р' => 'Р', 'с' => 'С', 'т' => 'Т', 'у' => 'У', 'ф' => 'Ф', 'х' => 'Х', 'ц' => 'Ц', 'ч' => 'Ч', 'ш' => 'Ш', 'щ' => 'Щ', 'ъ' => 'Ъ', 'ы' => 'Ы', 'ь' => 'Ь', 'э' => 'Э', 'ю' => 'Ю', 'я' => 'Я', 'ѐ' => 'Ѐ', 'ё' => 'Ё', 'ђ' => 'Ђ', 'ѓ' => 'Ѓ', 'є' => 'Є', 'ѕ' => 'Ѕ', 'і' => 'І', 'ї' => 'Ї', 'ј' => 'Ј', 'љ' => 'Љ', 'њ' => 'Њ', 'ћ' => 'Ћ', 'ќ' => 'Ќ', 'ѝ' => 'Ѝ', 'ў' => 'Ў', 'џ' => 'Џ', 'ѡ' => 'Ѡ', 'ѣ' => 'Ѣ', 'ѥ' => 'Ѥ', 'ѧ' => 'Ѧ', 'ѩ' => 'Ѩ', 'ѫ' => 'Ѫ', 'ѭ' => 'Ѭ', 'ѯ' => 'Ѯ', 'ѱ' => 'Ѱ', 'ѳ' => 'Ѳ', 'ѵ' => 'Ѵ', 'ѷ' => 'Ѷ', 'ѹ' => 'Ѹ', 'ѻ' => 'Ѻ', 'ѽ' => 'Ѽ', 'ѿ' => 'Ѿ', 'ҁ' => 'Ҁ', 'ҋ' => 'Ҋ', 'ҍ' => 'Ҍ', 'ҏ' => 'Ҏ', 'ґ' => 'Ґ', 'ғ' => 'Ғ', 'ҕ' => 'Ҕ', 'җ' => 'Җ', 'ҙ' => 'Ҙ', 'қ' => 'Қ', 'ҝ' => 'Ҝ', 'ҟ' => 'Ҟ', 'ҡ' => 'Ҡ', 'ң' => 'Ң', 'ҥ' => 'Ҥ', 'ҧ' => 'Ҧ', 'ҩ' => 'Ҩ', 'ҫ' => 'Ҫ', 'ҭ' => 'Ҭ', 'ү' => 'Ү', 'ұ' => 'Ұ', 'ҳ' => 'Ҳ', 'ҵ' => 'Ҵ', 'ҷ' => 'Ҷ', 'ҹ' => 'Ҹ', 'һ' => 'Һ', 'ҽ' => 'Ҽ', 'ҿ' => 'Ҿ', 'ӂ' => 'Ӂ', 'ӄ' => 'Ӄ', 'ӆ' => 'Ӆ', 'ӈ' => 'Ӈ', 'ӊ' => 'Ӊ', 'ӌ' => 'Ӌ', 'ӎ' => 'Ӎ', 'ӏ' => 'Ӏ', 'ӑ' => 'Ӑ', 'ӓ' => 'Ӓ', 'ӕ' => 'Ӕ', 'ӗ' => 'Ӗ', 'ә' => 'Ә', 'ӛ' => 'Ӛ', 'ӝ' => 'Ӝ', 'ӟ' => 'Ӟ', 'ӡ' => 'Ӡ', 'ӣ' => 'Ӣ', 'ӥ' => 'Ӥ', 'ӧ' => 'Ӧ', 'ө' => 'Ө', 'ӫ' => 'Ӫ', 'ӭ' => 'Ӭ', 'ӯ' => 'Ӯ', 'ӱ' => 'Ӱ', 'ӳ' => 'Ӳ', 'ӵ' => 'Ӵ', 'ӷ' => 'Ӷ', 'ӹ' => 'Ӹ', 'ӻ' => 'Ӻ', 'ӽ' => 'Ӽ', 'ӿ' => 'Ӿ', 'ԁ' => 'Ԁ', 'ԃ' => 'Ԃ', 'ԅ' => 'Ԅ', 'ԇ' => 'Ԇ', 'ԉ' => 'Ԉ', 'ԋ' => 'Ԋ', 'ԍ' => 'Ԍ', 'ԏ' => 'Ԏ', 'ԑ' => 'Ԑ', 'ԓ' => 'Ԓ', 'ԕ' => 'Ԕ', 'ԗ' => 'Ԗ', 'ԙ' => 'Ԙ', 'ԛ' => 'Ԛ', 'ԝ' => 'Ԝ', 'ԟ' => 'Ԟ', 'ԡ' => 'Ԡ', 'ԣ' => 'Ԣ', 'ԥ' => 'Ԥ', 'ԧ' => 'Ԧ', 'ԩ' => 'Ԩ', 'ԫ' => 'Ԫ', 'ԭ' => 'Ԭ', 'ԯ' => 'Ԯ', 'ա' => 'Ա', 'բ' => 'Բ', 'գ' => 'Գ', 'դ' => 'Դ', 'ե' => 'Ե', 'զ' => 'Զ', 'է' => 'Է', 'ը' => 'Ը', 'թ' => 'Թ', 'ժ' => 'Ժ', 'ի' => 'Ի', 'լ' => 'Լ', 'խ' => 'Խ', 'ծ' => 'Ծ', 'կ' => 'Կ', 'հ' => 'Հ', 'ձ' => 'Ձ', 'ղ' => 'Ղ', 'ճ' => 'Ճ', 'մ' => 'Մ', 'յ' => 'Յ', 'ն' => 'Ն', 'շ' => 'Շ', 'ո' => 'Ո', 'չ' => 'Չ', 'պ' => 'Պ', 'ջ' => 'Ջ', 'ռ' => 'Ռ', 'ս' => 'Ս', 'վ' => 'Վ', 'տ' => 'Տ', 'ր' => 'Ր', 'ց' => 'Ց', 'ւ' => 'Ւ', 'փ' => 'Փ', 'ք' => 'Ք', 'օ' => 'Օ', 'ֆ' => 'Ֆ', 'ა' => 'Ა', 'ბ' => 'Ბ', 'გ' => 'Გ', 'დ' => 'Დ', 'ე' => 'Ე', 'ვ' => 'Ვ', 'ზ' => 'Ზ', 'თ' => 'Თ', 'ი' => 'Ი', 'კ' => 'Კ', 'ლ' => 'Ლ', 'მ' => 'Მ', 'ნ' => 'Ნ', 'ო' => 'Ო', 'პ' => 'Პ', 'ჟ' => 'Ჟ', 'რ' => 'Რ', 'ს' => 'Ს', 'ტ' => 'Ტ', 'უ' => 'Უ', 'ფ' => 'Ფ', 'ქ' => 'Ქ', 'ღ' => 'Ღ', 'ყ' => 'Ყ', 'შ' => 'Შ', 'ჩ' => 'Ჩ', 'ც' => 'Ც', 'ძ' => 'Ძ', 'წ' => 'Წ', 'ჭ' => 'Ჭ', 'ხ' => 'Ხ', 'ჯ' => 'Ჯ', 'ჰ' => 'Ჰ', 'ჱ' => 'Ჱ', 'ჲ' => 'Ჲ', 'ჳ' => 'Ჳ', 'ჴ' => 'Ჴ', 'ჵ' => 'Ჵ', 'ჶ' => 'Ჶ', 'ჷ' => 'Ჷ', 'ჸ' => 'Ჸ', 'ჹ' => 'Ჹ', 'ჺ' => 'Ჺ', 'ჽ' => 'Ჽ', 'ჾ' => 'Ჾ', 'ჿ' => 'Ჿ', 'ᏸ' => 'Ᏸ', 'ᏹ' => 'Ᏹ', 'ᏺ' => 'Ᏺ', 'ᏻ' => 'Ᏻ', 'ᏼ' => 'Ᏼ', 'ᏽ' => 'Ᏽ', 'ᲀ' => 'В', 'ᲁ' => 'Д', 'ᲂ' => 'О', 'ᲃ' => 'С', 'ᲄ' => 'Т', 'ᲅ' => 'Т', 'ᲆ' => 'Ъ', 'ᲇ' => 'Ѣ', 'ᲈ' => 'Ꙋ', 'ᵹ' => 'Ᵹ', 'ᵽ' => 'Ᵽ', 'ᶎ' => 'Ᶎ', 'ḁ' => 'Ḁ', 'ḃ' => 'Ḃ', 'ḅ' => 'Ḅ', 'ḇ' => 'Ḇ', 'ḉ' => 'Ḉ', 'ḋ' => 'Ḋ', 'ḍ' => 'Ḍ', 'ḏ' => 'Ḏ', 'ḑ' => 'Ḑ', 'ḓ' => 'Ḓ', 'ḕ' => 'Ḕ', 'ḗ' => 'Ḗ', 'ḙ' => 'Ḙ', 'ḛ' => 'Ḛ', 'ḝ' => 'Ḝ', 'ḟ' => 'Ḟ', 'ḡ' => 'Ḡ', 'ḣ' => 'Ḣ', 'ḥ' => 'Ḥ', 'ḧ' => 'Ḧ', 'ḩ' => 'Ḩ', 'ḫ' => 'Ḫ', 'ḭ' => 'Ḭ', 'ḯ' => 'Ḯ', 'ḱ' => 'Ḱ', 'ḳ' => 'Ḳ', 'ḵ' => 'Ḵ', 'ḷ' => 'Ḷ', 'ḹ' => 'Ḹ', 'ḻ' => 'Ḻ', 'ḽ' => 'Ḽ', 'ḿ' => 'Ḿ', 'ṁ' => 'Ṁ', 'ṃ' => 'Ṃ', 'ṅ' => 'Ṅ', 'ṇ' => 'Ṇ', 'ṉ' => 'Ṉ', 'ṋ' => 'Ṋ', 'ṍ' => 'Ṍ', 'ṏ' => 'Ṏ', 'ṑ' => 'Ṑ', 'ṓ' => 'Ṓ', 'ṕ' => 'Ṕ', 'ṗ' => 'Ṗ', 'ṙ' => 'Ṙ', 'ṛ' => 'Ṛ', 'ṝ' => 'Ṝ', 'ṟ' => 'Ṟ', 'ṡ' => 'Ṡ', 'ṣ' => 'Ṣ', 'ṥ' => 'Ṥ', 'ṧ' => 'Ṧ', 'ṩ' => 'Ṩ', 'ṫ' => 'Ṫ', 'ṭ' => 'Ṭ', 'ṯ' => 'Ṯ', 'ṱ' => 'Ṱ', 'ṳ' => 'Ṳ', 'ṵ' => 'Ṵ', 'ṷ' => 'Ṷ', 'ṹ' => 'Ṹ', 'ṻ' => 'Ṻ', 'ṽ' => 'Ṽ', 'ṿ' => 'Ṿ', 'ẁ' => 'Ẁ', 'ẃ' => 'Ẃ', 'ẅ' => 'Ẅ', 'ẇ' => 'Ẇ', 'ẉ' => 'Ẉ', 'ẋ' => 'Ẋ', 'ẍ' => 'Ẍ', 'ẏ' => 'Ẏ', 'ẑ' => 'Ẑ', 'ẓ' => 'Ẓ', 'ẕ' => 'Ẕ', 'ẛ' => 'Ṡ', 'ạ' => 'Ạ', 'ả' => 'Ả', 'ấ' => 'Ấ', 'ầ' => 'Ầ', 'ẩ' => 'Ẩ', 'ẫ' => 'Ẫ', 'ậ' => 'Ậ', 'ắ' => 'Ắ', 'ằ' => 'Ằ', 'ẳ' => 'Ẳ', 'ẵ' => 'Ẵ', 'ặ' => 'Ặ', 'ẹ' => 'Ẹ', 'ẻ' => 'Ẻ', 'ẽ' => 'Ẽ', 'ế' => 'Ế', 'ề' => 'Ề', 'ể' => 'Ể', 'ễ' => 'Ễ', 'ệ' => 'Ệ', 'ỉ' => 'Ỉ', 'ị' => 'Ị', 'ọ' => 'Ọ', 'ỏ' => 'Ỏ', 'ố' => 'Ố', 'ồ' => 'Ồ', 'ổ' => 'Ổ', 'ỗ' => 'Ỗ', 'ộ' => 'Ộ', 'ớ' => 'Ớ', 'ờ' => 'Ờ', 'ở' => 'Ở', 'ỡ' => 'Ỡ', 'ợ' => 'Ợ', 'ụ' => 'Ụ', 'ủ' => 'Ủ', 'ứ' => 'Ứ', 'ừ' => 'Ừ', 'ử' => 'Ử', 'ữ' => 'Ữ', 'ự' => 'Ự', 'ỳ' => 'Ỳ', 'ỵ' => 'Ỵ', 'ỷ' => 'Ỷ', 'ỹ' => 'Ỹ', 'ỻ' => 'Ỻ', 'ỽ' => 'Ỽ', 'ỿ' => 'Ỿ', 'ἀ' => 'Ἀ', 'ἁ' => 'Ἁ', 'ἂ' => 'Ἂ', 'ἃ' => 'Ἃ', 'ἄ' => 'Ἄ', 'ἅ' => 'Ἅ', 'ἆ' => 'Ἆ', 'ἇ' => 'Ἇ', 'ἐ' => 'Ἐ', 'ἑ' => 'Ἑ', 'ἒ' => 'Ἒ', 'ἓ' => 'Ἓ', 'ἔ' => 'Ἔ', 'ἕ' => 'Ἕ', 'ἠ' => 'Ἠ', 'ἡ' => 'Ἡ', 'ἢ' => 'Ἢ', 'ἣ' => 'Ἣ', 'ἤ' => 'Ἤ', 'ἥ' => 'Ἥ', 'ἦ' => 'Ἦ', 'ἧ' => 'Ἧ', 'ἰ' => 'Ἰ', 'ἱ' => 'Ἱ', 'ἲ' => 'Ἲ', 'ἳ' => 'Ἳ', 'ἴ' => 'Ἴ', 'ἵ' => 'Ἵ', 'ἶ' => 'Ἶ', 'ἷ' => 'Ἷ', 'ὀ' => 'Ὀ', 'ὁ' => 'Ὁ', 'ὂ' => 'Ὂ', 'ὃ' => 'Ὃ', 'ὄ' => 'Ὄ', 'ὅ' => 'Ὅ', 'ὑ' => 'Ὑ', 'ὓ' => 'Ὓ', 'ὕ' => 'Ὕ', 'ὗ' => 'Ὗ', 'ὠ' => 'Ὠ', 'ὡ' => 'Ὡ', 'ὢ' => 'Ὢ', 'ὣ' => 'Ὣ', 'ὤ' => 'Ὤ', 'ὥ' => 'Ὥ', 'ὦ' => 'Ὦ', 'ὧ' => 'Ὧ', 'ὰ' => 'Ὰ', 'ά' => 'Ά', 'ὲ' => 'Ὲ', 'έ' => 'Έ', 'ὴ' => 'Ὴ', 'ή' => 'Ή', 'ὶ' => 'Ὶ', 'ί' => 'Ί', 'ὸ' => 'Ὸ', 'ό' => 'Ό', 'ὺ' => 'Ὺ', 'ύ' => 'Ύ', 'ὼ' => 'Ὼ', 'ώ' => 'Ώ', 'ᾀ' => 'ἈΙ', 'ᾁ' => 'ἉΙ', 'ᾂ' => 'ἊΙ', 'ᾃ' => 'ἋΙ', 'ᾄ' => 'ἌΙ', 'ᾅ' => 'ἍΙ', 'ᾆ' => 'ἎΙ', 'ᾇ' => 'ἏΙ', 'ᾐ' => 'ἨΙ', 'ᾑ' => 'ἩΙ', 'ᾒ' => 'ἪΙ', 'ᾓ' => 'ἫΙ', 'ᾔ' => 'ἬΙ', 'ᾕ' => 'ἭΙ', 'ᾖ' => 'ἮΙ', 'ᾗ' => 'ἯΙ', 'ᾠ' => 'ὨΙ', 'ᾡ' => 'ὩΙ', 'ᾢ' => 'ὪΙ', 'ᾣ' => 'ὫΙ', 'ᾤ' => 'ὬΙ', 'ᾥ' => 'ὭΙ', 'ᾦ' => 'ὮΙ', 'ᾧ' => 'ὯΙ', 'ᾰ' => 'Ᾰ', 'ᾱ' => 'Ᾱ', 'ᾳ' => 'ΑΙ', 'ι' => 'Ι', 'ῃ' => 'ΗΙ', 'ῐ' => 'Ῐ', 'ῑ' => 'Ῑ', 'ῠ' => 'Ῠ', 'ῡ' => 'Ῡ', 'ῥ' => 'Ῥ', 'ῳ' => 'ΩΙ', 'ⅎ' => 'Ⅎ', 'ⅰ' => 'Ⅰ', 'ⅱ' => 'Ⅱ', 'ⅲ' => 'Ⅲ', 'ⅳ' => 'Ⅳ', 'ⅴ' => 'Ⅴ', 'ⅵ' => 'Ⅵ', 'ⅶ' => 'Ⅶ', 'ⅷ' => 'Ⅷ', 'ⅸ' => 'Ⅸ', 'ⅹ' => 'Ⅹ', 'ⅺ' => 'Ⅺ', 'ⅻ' => 'Ⅻ', 'ⅼ' => 'Ⅼ', 'ⅽ' => 'Ⅽ', 'ⅾ' => 'Ⅾ', 'ⅿ' => 'Ⅿ', 'ↄ' => 'Ↄ', 'ⓐ' => 'Ⓐ', 'ⓑ' => 'Ⓑ', 'ⓒ' => 'Ⓒ', 'ⓓ' => 'Ⓓ', 'ⓔ' => 'Ⓔ', 'ⓕ' => 'Ⓕ', 'ⓖ' => 'Ⓖ', 'ⓗ' => 'Ⓗ', 'ⓘ' => 'Ⓘ', 'ⓙ' => 'Ⓙ', 'ⓚ' => 'Ⓚ', 'ⓛ' => 'Ⓛ', 'ⓜ' => 'Ⓜ', 'ⓝ' => 'Ⓝ', 'ⓞ' => 'Ⓞ', 'ⓟ' => 'Ⓟ', 'ⓠ' => 'Ⓠ', 'ⓡ' => 'Ⓡ', 'ⓢ' => 'Ⓢ', 'ⓣ' => 'Ⓣ', 'ⓤ' => 'Ⓤ', 'ⓥ' => 'Ⓥ', 'ⓦ' => 'Ⓦ', 'ⓧ' => 'Ⓧ', 'ⓨ' => 'Ⓨ', 'ⓩ' => 'Ⓩ', 'ⰰ' => 'Ⰰ', 'ⰱ' => 'Ⰱ', 'ⰲ' => 'Ⰲ', 'ⰳ' => 'Ⰳ', 'ⰴ' => 'Ⰴ', 'ⰵ' => 'Ⰵ', 'ⰶ' => 'Ⰶ', 'ⰷ' => 'Ⰷ', 'ⰸ' => 'Ⰸ', 'ⰹ' => 'Ⰹ', 'ⰺ' => 'Ⰺ', 'ⰻ' => 'Ⰻ', 'ⰼ' => 'Ⰼ', 'ⰽ' => 'Ⰽ', 'ⰾ' => 'Ⰾ', 'ⰿ' => 'Ⰿ', 'ⱀ' => 'Ⱀ', 'ⱁ' => 'Ⱁ', 'ⱂ' => 'Ⱂ', 'ⱃ' => 'Ⱃ', 'ⱄ' => 'Ⱄ', 'ⱅ' => 'Ⱅ', 'ⱆ' => 'Ⱆ', 'ⱇ' => 'Ⱇ', 'ⱈ' => 'Ⱈ', 'ⱉ' => 'Ⱉ', 'ⱊ' => 'Ⱊ', 'ⱋ' => 'Ⱋ', 'ⱌ' => 'Ⱌ', 'ⱍ' => 'Ⱍ', 'ⱎ' => 'Ⱎ', 'ⱏ' => 'Ⱏ', 'ⱐ' => 'Ⱐ', 'ⱑ' => 'Ⱑ', 'ⱒ' => 'Ⱒ', 'ⱓ' => 'Ⱓ', 'ⱔ' => 'Ⱔ', 'ⱕ' => 'Ⱕ', 'ⱖ' => 'Ⱖ', 'ⱗ' => 'Ⱗ', 'ⱘ' => 'Ⱘ', 'ⱙ' => 'Ⱙ', 'ⱚ' => 'Ⱚ', 'ⱛ' => 'Ⱛ', 'ⱜ' => 'Ⱜ', 'ⱝ' => 'Ⱝ', 'ⱞ' => 'Ⱞ', 'ⱡ' => 'Ⱡ', 'ⱥ' => 'Ⱥ', 'ⱦ' => 'Ⱦ', 'ⱨ' => 'Ⱨ', 'ⱪ' => 'Ⱪ', 'ⱬ' => 'Ⱬ', 'ⱳ' => 'Ⱳ', 'ⱶ' => 'Ⱶ', 'ⲁ' => 'Ⲁ', 'ⲃ' => 'Ⲃ', 'ⲅ' => 'Ⲅ', 'ⲇ' => 'Ⲇ', 'ⲉ' => 'Ⲉ', 'ⲋ' => 'Ⲋ', 'ⲍ' => 'Ⲍ', 'ⲏ' => 'Ⲏ', 'ⲑ' => 'Ⲑ', 'ⲓ' => 'Ⲓ', 'ⲕ' => 'Ⲕ', 'ⲗ' => 'Ⲗ', 'ⲙ' => 'Ⲙ', 'ⲛ' => 'Ⲛ', 'ⲝ' => 'Ⲝ', 'ⲟ' => 'Ⲟ', 'ⲡ' => 'Ⲡ', 'ⲣ' => 'Ⲣ', 'ⲥ' => 'Ⲥ', 'ⲧ' => 'Ⲧ', 'ⲩ' => 'Ⲩ', 'ⲫ' => 'Ⲫ', 'ⲭ' => 'Ⲭ', 'ⲯ' => 'Ⲯ', 'ⲱ' => 'Ⲱ', 'ⲳ' => 'Ⲳ', 'ⲵ' => 'Ⲵ', 'ⲷ' => 'Ⲷ', 'ⲹ' => 'Ⲹ', 'ⲻ' => 'Ⲻ', 'ⲽ' => 'Ⲽ', 'ⲿ' => 'Ⲿ', 'ⳁ' => 'Ⳁ', 'ⳃ' => 'Ⳃ', 'ⳅ' => 'Ⳅ', 'ⳇ' => 'Ⳇ', 'ⳉ' => 'Ⳉ', 'ⳋ' => 'Ⳋ', 'ⳍ' => 'Ⳍ', 'ⳏ' => 'Ⳏ', 'ⳑ' => 'Ⳑ', 'ⳓ' => 'Ⳓ', 'ⳕ' => 'Ⳕ', 'ⳗ' => 'Ⳗ', 'ⳙ' => 'Ⳙ', 'ⳛ' => 'Ⳛ', 'ⳝ' => 'Ⳝ', 'ⳟ' => 'Ⳟ', 'ⳡ' => 'Ⳡ', 'ⳣ' => 'Ⳣ', 'ⳬ' => 'Ⳬ', 'ⳮ' => 'Ⳮ', 'ⳳ' => 'Ⳳ', 'ⴀ' => 'Ⴀ', 'ⴁ' => 'Ⴁ', 'ⴂ' => 'Ⴂ', 'ⴃ' => 'Ⴃ', 'ⴄ' => 'Ⴄ', 'ⴅ' => 'Ⴅ', 'ⴆ' => 'Ⴆ', 'ⴇ' => 'Ⴇ', 'ⴈ' => 'Ⴈ', 'ⴉ' => 'Ⴉ', 'ⴊ' => 'Ⴊ', 'ⴋ' => 'Ⴋ', 'ⴌ' => 'Ⴌ', 'ⴍ' => 'Ⴍ', 'ⴎ' => 'Ⴎ', 'ⴏ' => 'Ⴏ', 'ⴐ' => 'Ⴐ', 'ⴑ' => 'Ⴑ', 'ⴒ' => 'Ⴒ', 'ⴓ' => 'Ⴓ', 'ⴔ' => 'Ⴔ', 'ⴕ' => 'Ⴕ', 'ⴖ' => 'Ⴖ', 'ⴗ' => 'Ⴗ', 'ⴘ' => 'Ⴘ', 'ⴙ' => 'Ⴙ', 'ⴚ' => 'Ⴚ', 'ⴛ' => 'Ⴛ', 'ⴜ' => 'Ⴜ', 'ⴝ' => 'Ⴝ', 'ⴞ' => 'Ⴞ', 'ⴟ' => 'Ⴟ', 'ⴠ' => 'Ⴠ', 'ⴡ' => 'Ⴡ', 'ⴢ' => 'Ⴢ', 'ⴣ' => 'Ⴣ', 'ⴤ' => 'Ⴤ', 'ⴥ' => 'Ⴥ', 'ⴧ' => 'Ⴧ', 'ⴭ' => 'Ⴭ', 'ꙁ' => 'Ꙁ', 'ꙃ' => 'Ꙃ', 'ꙅ' => 'Ꙅ', 'ꙇ' => 'Ꙇ', 'ꙉ' => 'Ꙉ', 'ꙋ' => 'Ꙋ', 'ꙍ' => 'Ꙍ', 'ꙏ' => 'Ꙏ', 'ꙑ' => 'Ꙑ', 'ꙓ' => 'Ꙓ', 'ꙕ' => 'Ꙕ', 'ꙗ' => 'Ꙗ', 'ꙙ' => 'Ꙙ', 'ꙛ' => 'Ꙛ', 'ꙝ' => 'Ꙝ', 'ꙟ' => 'Ꙟ', 'ꙡ' => 'Ꙡ', 'ꙣ' => 'Ꙣ', 'ꙥ' => 'Ꙥ', 'ꙧ' => 'Ꙧ', 'ꙩ' => 'Ꙩ', 'ꙫ' => 'Ꙫ', 'ꙭ' => 'Ꙭ', 'ꚁ' => 'Ꚁ', 'ꚃ' => 'Ꚃ', 'ꚅ' => 'Ꚅ', 'ꚇ' => 'Ꚇ', 'ꚉ' => 'Ꚉ', 'ꚋ' => 'Ꚋ', 'ꚍ' => 'Ꚍ', 'ꚏ' => 'Ꚏ', 'ꚑ' => 'Ꚑ', 'ꚓ' => 'Ꚓ', 'ꚕ' => 'Ꚕ', 'ꚗ' => 'Ꚗ', 'ꚙ' => 'Ꚙ', 'ꚛ' => 'Ꚛ', 'ꜣ' => 'Ꜣ', 'ꜥ' => 'Ꜥ', 'ꜧ' => 'Ꜧ', 'ꜩ' => 'Ꜩ', 'ꜫ' => 'Ꜫ', 'ꜭ' => 'Ꜭ', 'ꜯ' => 'Ꜯ', 'ꜳ' => 'Ꜳ', 'ꜵ' => 'Ꜵ', 'ꜷ' => 'Ꜷ', 'ꜹ' => 'Ꜹ', 'ꜻ' => 'Ꜻ', 'ꜽ' => 'Ꜽ', 'ꜿ' => 'Ꜿ', 'ꝁ' => 'Ꝁ', 'ꝃ' => 'Ꝃ', 'ꝅ' => 'Ꝅ', 'ꝇ' => 'Ꝇ', 'ꝉ' => 'Ꝉ', 'ꝋ' => 'Ꝋ', 'ꝍ' => 'Ꝍ', 'ꝏ' => 'Ꝏ', 'ꝑ' => 'Ꝑ', 'ꝓ' => 'Ꝓ', 'ꝕ' => 'Ꝕ', 'ꝗ' => 'Ꝗ', 'ꝙ' => 'Ꝙ', 'ꝛ' => 'Ꝛ', 'ꝝ' => 'Ꝝ', 'ꝟ' => 'Ꝟ', 'ꝡ' => 'Ꝡ', 'ꝣ' => 'Ꝣ', 'ꝥ' => 'Ꝥ', 'ꝧ' => 'Ꝧ', 'ꝩ' => 'Ꝩ', 'ꝫ' => 'Ꝫ', 'ꝭ' => 'Ꝭ', 'ꝯ' => 'Ꝯ', 'ꝺ' => 'Ꝺ', 'ꝼ' => 'Ꝼ', 'ꝿ' => 'Ꝿ', 'ꞁ' => 'Ꞁ', 'ꞃ' => 'Ꞃ', 'ꞅ' => 'Ꞅ', 'ꞇ' => 'Ꞇ', 'ꞌ' => 'Ꞌ', 'ꞑ' => 'Ꞑ', 'ꞓ' => 'Ꞓ', 'ꞔ' => 'Ꞔ', 'ꞗ' => 'Ꞗ', 'ꞙ' => 'Ꞙ', 'ꞛ' => 'Ꞛ', 'ꞝ' => 'Ꞝ', 'ꞟ' => 'Ꞟ', 'ꞡ' => 'Ꞡ', 'ꞣ' => 'Ꞣ', 'ꞥ' => 'Ꞥ', 'ꞧ' => 'Ꞧ', 'ꞩ' => 'Ꞩ', 'ꞵ' => 'Ꞵ', 'ꞷ' => 'Ꞷ', 'ꞹ' => 'Ꞹ', 'ꞻ' => 'Ꞻ', 'ꞽ' => 'Ꞽ', 'ꞿ' => 'Ꞿ', 'ꟃ' => 'Ꟃ', 'ꟈ' => 'Ꟈ', 'ꟊ' => 'Ꟊ', 'ꟶ' => 'Ꟶ', 'ꭓ' => 'Ꭓ', 'ꭰ' => 'Ꭰ', 'ꭱ' => 'Ꭱ', 'ꭲ' => 'Ꭲ', 'ꭳ' => 'Ꭳ', 'ꭴ' => 'Ꭴ', 'ꭵ' => 'Ꭵ', 'ꭶ' => 'Ꭶ', 'ꭷ' => 'Ꭷ', 'ꭸ' => 'Ꭸ', 'ꭹ' => 'Ꭹ', 'ꭺ' => 'Ꭺ', 'ꭻ' => 'Ꭻ', 'ꭼ' => 'Ꭼ', 'ꭽ' => 'Ꭽ', 'ꭾ' => 'Ꭾ', 'ꭿ' => 'Ꭿ', 'ꮀ' => 'Ꮀ', 'ꮁ' => 'Ꮁ', 'ꮂ' => 'Ꮂ', 'ꮃ' => 'Ꮃ', 'ꮄ' => 'Ꮄ', 'ꮅ' => 'Ꮅ', 'ꮆ' => 'Ꮆ', 'ꮇ' => 'Ꮇ', 'ꮈ' => 'Ꮈ', 'ꮉ' => 'Ꮉ', 'ꮊ' => 'Ꮊ', 'ꮋ' => 'Ꮋ', 'ꮌ' => 'Ꮌ', 'ꮍ' => 'Ꮍ', 'ꮎ' => 'Ꮎ', 'ꮏ' => 'Ꮏ', 'ꮐ' => 'Ꮐ', 'ꮑ' => 'Ꮑ', 'ꮒ' => 'Ꮒ', 'ꮓ' => 'Ꮓ', 'ꮔ' => 'Ꮔ', 'ꮕ' => 'Ꮕ', 'ꮖ' => 'Ꮖ', 'ꮗ' => 'Ꮗ', 'ꮘ' => 'Ꮘ', 'ꮙ' => 'Ꮙ', 'ꮚ' => 'Ꮚ', 'ꮛ' => 'Ꮛ', 'ꮜ' => 'Ꮜ', 'ꮝ' => 'Ꮝ', 'ꮞ' => 'Ꮞ', 'ꮟ' => 'Ꮟ', 'ꮠ' => 'Ꮠ', 'ꮡ' => 'Ꮡ', 'ꮢ' => 'Ꮢ', 'ꮣ' => 'Ꮣ', 'ꮤ' => 'Ꮤ', 'ꮥ' => 'Ꮥ', 'ꮦ' => 'Ꮦ', 'ꮧ' => 'Ꮧ', 'ꮨ' => 'Ꮨ', 'ꮩ' => 'Ꮩ', 'ꮪ' => 'Ꮪ', 'ꮫ' => 'Ꮫ', 'ꮬ' => 'Ꮬ', 'ꮭ' => 'Ꮭ', 'ꮮ' => 'Ꮮ', 'ꮯ' => 'Ꮯ', 'ꮰ' => 'Ꮰ', 'ꮱ' => 'Ꮱ', 'ꮲ' => 'Ꮲ', 'ꮳ' => 'Ꮳ', 'ꮴ' => 'Ꮴ', 'ꮵ' => 'Ꮵ', 'ꮶ' => 'Ꮶ', 'ꮷ' => 'Ꮷ', 'ꮸ' => 'Ꮸ', 'ꮹ' => 'Ꮹ', 'ꮺ' => 'Ꮺ', 'ꮻ' => 'Ꮻ', 'ꮼ' => 'Ꮼ', 'ꮽ' => 'Ꮽ', 'ꮾ' => 'Ꮾ', 'ꮿ' => 'Ꮿ', 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D', 'e' => 'E', 'f' => 'F', 'g' => 'G', 'h' => 'H', 'i' => 'I', 'j' => 'J', 'k' => 'K', 'l' => 'L', 'm' => 'M', 'n' => 'N', 'o' => 'O', 'p' => 'P', 'q' => 'Q', 'r' => 'R', 's' => 'S', 't' => 'T', 'u' => 'U', 'v' => 'V', 'w' => 'W', 'x' => 'X', 'y' => 'Y', 'z' => 'Z', '𐐨' => '𐐀', '𐐩' => '𐐁', '𐐪' => '𐐂', '𐐫' => '𐐃', '𐐬' => '𐐄', '𐐭' => '𐐅', '𐐮' => '𐐆', '𐐯' => '𐐇', '𐐰' => '𐐈', '𐐱' => '𐐉', '𐐲' => '𐐊', '𐐳' => '𐐋', '𐐴' => '𐐌', '𐐵' => '𐐍', '𐐶' => '𐐎', '𐐷' => '𐐏', '𐐸' => '𐐐', '𐐹' => '𐐑', '𐐺' => '𐐒', '𐐻' => '𐐓', '𐐼' => '𐐔', '𐐽' => '𐐕', '𐐾' => '𐐖', '𐐿' => '𐐗', '𐑀' => '𐐘', '𐑁' => '𐐙', '𐑂' => '𐐚', '𐑃' => '𐐛', '𐑄' => '𐐜', '𐑅' => '𐐝', '𐑆' => '𐐞', '𐑇' => '𐐟', '𐑈' => '𐐠', '𐑉' => '𐐡', '𐑊' => '𐐢', '𐑋' => '𐐣', '𐑌' => '𐐤', '𐑍' => '𐐥', '𐑎' => '𐐦', '𐑏' => '𐐧', '𐓘' => '𐒰', '𐓙' => '𐒱', '𐓚' => '𐒲', '𐓛' => '𐒳', '𐓜' => '𐒴', '𐓝' => '𐒵', '𐓞' => '𐒶', '𐓟' => '𐒷', '𐓠' => '𐒸', '𐓡' => '𐒹', '𐓢' => '𐒺', '𐓣' => '𐒻', '𐓤' => '𐒼', '𐓥' => '𐒽', '𐓦' => '𐒾', '𐓧' => '𐒿', '𐓨' => '𐓀', '𐓩' => '𐓁', '𐓪' => '𐓂', '𐓫' => '𐓃', '𐓬' => '𐓄', '𐓭' => '𐓅', '𐓮' => '𐓆', '𐓯' => '𐓇', '𐓰' => '𐓈', '𐓱' => '𐓉', '𐓲' => '𐓊', '𐓳' => '𐓋', '𐓴' => '𐓌', '𐓵' => '𐓍', '𐓶' => '𐓎', '𐓷' => '𐓏', '𐓸' => '𐓐', '𐓹' => '𐓑', '𐓺' => '𐓒', '𐓻' => '𐓓', '𐳀' => '𐲀', '𐳁' => '𐲁', '𐳂' => '𐲂', '𐳃' => '𐲃', '𐳄' => '𐲄', '𐳅' => '𐲅', '𐳆' => '𐲆', '𐳇' => '𐲇', '𐳈' => '𐲈', '𐳉' => '𐲉', '𐳊' => '𐲊', '𐳋' => '𐲋', '𐳌' => '𐲌', '𐳍' => '𐲍', '𐳎' => '𐲎', '𐳏' => '𐲏', '𐳐' => '𐲐', '𐳑' => '𐲑', '𐳒' => '𐲒', '𐳓' => '𐲓', '𐳔' => '𐲔', '𐳕' => '𐲕', '𐳖' => '𐲖', '𐳗' => '𐲗', '𐳘' => '𐲘', '𐳙' => '𐲙', '𐳚' => '𐲚', '𐳛' => '𐲛', '𐳜' => '𐲜', '𐳝' => '𐲝', '𐳞' => '𐲞', '𐳟' => '𐲟', '𐳠' => '𐲠', '𐳡' => '𐲡', '𐳢' => '𐲢', '𐳣' => '𐲣', '𐳤' => '𐲤', '𐳥' => '𐲥', '𐳦' => '𐲦', '𐳧' => '𐲧', '𐳨' => '𐲨', '𐳩' => '𐲩', '𐳪' => '𐲪', '𐳫' => '𐲫', '𐳬' => '𐲬', '𐳭' => '𐲭', '𐳮' => '𐲮', '𐳯' => '𐲯', '𐳰' => '𐲰', '𐳱' => '𐲱', '𐳲' => '𐲲', '𑣀' => '𑢠', '𑣁' => '𑢡', '𑣂' => '𑢢', '𑣃' => '𑢣', '𑣄' => '𑢤', '𑣅' => '𑢥', '𑣆' => '𑢦', '𑣇' => '𑢧', '𑣈' => '𑢨', '𑣉' => '𑢩', '𑣊' => '𑢪', '𑣋' => '𑢫', '𑣌' => '𑢬', '𑣍' => '𑢭', '𑣎' => '𑢮', '𑣏' => '𑢯', '𑣐' => '𑢰', '𑣑' => '𑢱', '𑣒' => '𑢲', '𑣓' => '𑢳', '𑣔' => '𑢴', '𑣕' => '𑢵', '𑣖' => '𑢶', '𑣗' => '𑢷', '𑣘' => '𑢸', '𑣙' => '𑢹', '𑣚' => '𑢺', '𑣛' => '𑢻', '𑣜' => '𑢼', '𑣝' => '𑢽', '𑣞' => '𑢾', '𑣟' => '𑢿', '𖹠' => '𖹀', '𖹡' => '𖹁', '𖹢' => '𖹂', '𖹣' => '𖹃', '𖹤' => '𖹄', '𖹥' => '𖹅', '𖹦' => '𖹆', '𖹧' => '𖹇', '𖹨' => '𖹈', '𖹩' => '𖹉', '𖹪' => '𖹊', '𖹫' => '𖹋', '𖹬' => '𖹌', '𖹭' => '𖹍', '𖹮' => '𖹎', '𖹯' => '𖹏', '𖹰' => '𖹐', '𖹱' => '𖹑', '𖹲' => '𖹒', '𖹳' => '𖹓', '𖹴' => '𖹔', '𖹵' => '𖹕', '𖹶' => '𖹖', '𖹷' => '𖹗', '𖹸' => '𖹘', '𖹹' => '𖹙', '𖹺' => '𖹚', '𖹻' => '𖹛', '𖹼' => '𖹜', '𖹽' => '𖹝', '𖹾' => '𖹞', '𖹿' => '𖹟', '𞤢' => '𞤀', '𞤣' => '𞤁', '𞤤' => '𞤂', '𞤥' => '𞤃', '𞤦' => '𞤄', '𞤧' => '𞤅', '𞤨' => '𞤆', '𞤩' => '𞤇', '𞤪' => '𞤈', '𞤫' => '𞤉', '𞤬' => '𞤊', '𞤭' => '𞤋', '𞤮' => '𞤌', '𞤯' => '𞤍', '𞤰' => '𞤎', '𞤱' => '𞤏', '𞤲' => '𞤐', '𞤳' => '𞤑', '𞤴' => '𞤒', '𞤵' => '𞤓', '𞤶' => '𞤔', '𞤷' => '𞤕', '𞤸' => '𞤖', '𞤹' => '𞤗', '𞤺' => '𞤘', '𞤻' => '𞤙', '𞤼' => '𞤚', '𞤽' => '𞤛', '𞤾' => '𞤜', '𞤿' => '𞤝', '𞥀' => '𞤞', '𞥁' => '𞤟', '𞥂' => '𞤠', '𞥃' => '𞤡', 'ß' => 'SS', 'ff' => 'FF', 'fi' => 'FI', 'fl' => 'FL', 'ffi' => 'FFI', 'ffl' => 'FFL', 'ſt' => 'ST', 'st' => 'ST', 'և' => 'ԵՒ', 'ﬓ' => 'ՄՆ', 'ﬔ' => 'ՄԵ', 'ﬕ' => 'ՄԻ', 'ﬖ' => 'ՎՆ', 'ﬗ' => 'ՄԽ', 'ʼn' => 'ʼN', 'ΐ' => 'Ϊ́', 'ΰ' => 'Ϋ́', 'ǰ' => 'J̌', 'ẖ' => 'H̱', 'ẗ' => 'T̈', 'ẘ' => 'W̊', 'ẙ' => 'Y̊', 'ẚ' => 'Aʾ', 'ὐ' => 'Υ̓', 'ὒ' => 'Υ̓̀', 'ὔ' => 'Υ̓́', 'ὖ' => 'Υ̓͂', 'ᾶ' => 'Α͂', 'ῆ' => 'Η͂', 'ῒ' => 'Ϊ̀', 'ΐ' => 'Ϊ́', 'ῖ' => 'Ι͂', 'ῗ' => 'Ϊ͂', 'ῢ' => 'Ϋ̀', 'ΰ' => 'Ϋ́', 'ῤ' => 'Ρ̓', 'ῦ' => 'Υ͂', 'ῧ' => 'Ϋ͂', 'ῶ' => 'Ω͂', 'ᾈ' => 'ἈΙ', 'ᾉ' => 'ἉΙ', 'ᾊ' => 'ἊΙ', 'ᾋ' => 'ἋΙ', 'ᾌ' => 'ἌΙ', 'ᾍ' => 'ἍΙ', 'ᾎ' => 'ἎΙ', 'ᾏ' => 'ἏΙ', 'ᾘ' => 'ἨΙ', 'ᾙ' => 'ἩΙ', 'ᾚ' => 'ἪΙ', 'ᾛ' => 'ἫΙ', 'ᾜ' => 'ἬΙ', 'ᾝ' => 'ἭΙ', 'ᾞ' => 'ἮΙ', 'ᾟ' => 'ἯΙ', 'ᾨ' => 'ὨΙ', 'ᾩ' => 'ὩΙ', 'ᾪ' => 'ὪΙ', 'ᾫ' => 'ὫΙ', 'ᾬ' => 'ὬΙ', 'ᾭ' => 'ὭΙ', 'ᾮ' => 'ὮΙ', 'ᾯ' => 'ὯΙ', 'ᾼ' => 'ΑΙ', 'ῌ' => 'ΗΙ', 'ῼ' => 'ΩΙ', 'ᾲ' => 'ᾺΙ', 'ᾴ' => 'ΆΙ', 'ῂ' => 'ῊΙ', 'ῄ' => 'ΉΙ', 'ῲ' => 'ῺΙ', 'ῴ' => 'ΏΙ', 'ᾷ' => 'Α͂Ι', 'ῇ' => 'Η͂Ι', 'ῷ' => 'Ω͂Ι', ); polyfill-php80/bootstrap.php 0000644 00000002774 15025017654 0012100 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Php80 as p; if (\PHP_VERSION_ID >= 80000) { return; } if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) { define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN); } if (!function_exists('fdiv')) { function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); } } if (!function_exists('preg_last_error_msg')) { function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); } } if (!function_exists('str_contains')) { function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); } } if (!function_exists('str_starts_with')) { function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); } } if (!function_exists('str_ends_with')) { function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); } } if (!function_exists('get_debug_type')) { function get_debug_type($value): string { return p\Php80::get_debug_type($value); } } if (!function_exists('get_resource_id')) { function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); } } polyfill-php80/PhpToken.php 0000644 00000004211 15025017654 0011577 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Php80; /** * @author Fedonyuk Anton <info@ensostudio.ru> * * @internal */ class PhpToken implements \Stringable { /** * @var int */ public $id; /** * @var string */ public $text; /** * @var int */ public $line; /** * @var int */ public $pos; public function __construct(int $id, string $text, int $line = -1, int $position = -1) { $this->id = $id; $this->text = $text; $this->line = $line; $this->pos = $position; } public function getTokenName(): ?string { if ('UNKNOWN' === $name = token_name($this->id)) { $name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text; } return $name; } /** * @param int|string|array $kind */ public function is($kind): bool { foreach ((array) $kind as $value) { if (\in_array($value, [$this->id, $this->text], true)) { return true; } } return false; } public function isIgnorable(): bool { return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true); } public function __toString(): string { return (string) $this->text; } /** * @return static[] */ public static function tokenize(string $code, int $flags = 0): array { $line = 1; $position = 0; $tokens = token_get_all($code, $flags); foreach ($tokens as $index => $token) { if (\is_string($token)) { $id = \ord($token); $text = $token; } else { [$id, $text, $line] = $token; } $tokens[$index] = new static($id, $text, $line, $position); $position += \strlen($text); } return $tokens; } } polyfill-php80/composer.json 0000644 00000002075 15025017654 0012066 0 ustar 00 { "name": "symfony/polyfill-php80", "type": "library", "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "keywords": ["polyfill", "shim", "compatibility", "portable"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Ion Bazan", "email": "ion.bazan@gmail.com" }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.1" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Php80\\": "" }, "files": [ "bootstrap.php" ], "classmap": [ "Resources/stubs" ] }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } polyfill-php80/Php80.php 0000644 00000006771 15025017654 0010763 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Php80; /** * @author Ion Bazan <ion.bazan@gmail.com> * @author Nico Oelgart <nicoswd@gmail.com> * @author Nicolas Grekas <p@tchwork.com> * * @internal */ final class Php80 { public static function fdiv(float $dividend, float $divisor): float { return @($dividend / $divisor); } public static function get_debug_type($value): string { switch (true) { case null === $value: return 'null'; case \is_bool($value): return 'bool'; case \is_string($value): return 'string'; case \is_array($value): return 'array'; case \is_int($value): return 'int'; case \is_float($value): return 'float'; case \is_object($value): break; case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class'; default: if (null === $type = @get_resource_type($value)) { return 'unknown'; } if ('Unknown' === $type) { $type = 'closed'; } return "resource ($type)"; } $class = \get_class($value); if (false === strpos($class, '@')) { return $class; } return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous'; } public static function get_resource_id($res): int { if (!\is_resource($res) && null === @get_resource_type($res)) { throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res))); } return (int) $res; } public static function preg_last_error_msg(): string { switch (preg_last_error()) { case \PREG_INTERNAL_ERROR: return 'Internal error'; case \PREG_BAD_UTF8_ERROR: return 'Malformed UTF-8 characters, possibly incorrectly encoded'; case \PREG_BAD_UTF8_OFFSET_ERROR: return 'The offset did not correspond to the beginning of a valid UTF-8 code point'; case \PREG_BACKTRACK_LIMIT_ERROR: return 'Backtrack limit exhausted'; case \PREG_RECURSION_LIMIT_ERROR: return 'Recursion limit exhausted'; case \PREG_JIT_STACKLIMIT_ERROR: return 'JIT stack limit exhausted'; case \PREG_NO_ERROR: return 'No error'; default: return 'Unknown error'; } } public static function str_contains(string $haystack, string $needle): bool { return '' === $needle || false !== strpos($haystack, $needle); } public static function str_starts_with(string $haystack, string $needle): bool { return 0 === strncmp($haystack, $needle, \strlen($needle)); } public static function str_ends_with(string $haystack, string $needle): bool { if ('' === $needle || $needle === $haystack) { return true; } if ('' === $haystack) { return false; } $needleLength = \strlen($needle); return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength); } } polyfill-php80/README.md 0000644 00000001627 15025017654 0010625 0 ustar 00 Symfony Polyfill / Php80 ======================== This component provides features added to PHP 8.0 core: - [`Stringable`](https://php.net/stringable) interface - [`fdiv`](https://php.net/fdiv) - [`ValueError`](https://php.net/valueerror) class - [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class - `FILTER_VALIDATE_BOOL` constant - [`get_debug_type`](https://php.net/get_debug_type) - [`PhpToken`](https://php.net/phptoken) class - [`preg_last_error_msg`](https://php.net/preg_last_error_msg) - [`str_contains`](https://php.net/str_contains) - [`str_starts_with`](https://php.net/str_starts_with) - [`str_ends_with`](https://php.net/str_ends_with) - [`get_resource_id`](https://php.net/get_resource_id) More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). polyfill-php80/LICENSE 0000644 00000002044 15025017654 0010345 0 ustar 00 Copyright (c) 2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. polyfill-php80/Resources/stubs/Attribute.php 0000644 00000001360 15025017654 0015126 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #[Attribute(Attribute::TARGET_CLASS)] final class Attribute { public const TARGET_CLASS = 1; public const TARGET_FUNCTION = 2; public const TARGET_METHOD = 4; public const TARGET_PROPERTY = 8; public const TARGET_CLASS_CONSTANT = 16; public const TARGET_PARAMETER = 32; public const TARGET_ALL = 63; public const IS_REPEATABLE = 64; /** @var int */ public $flags; public function __construct(int $flags = self::TARGET_ALL) { $this->flags = $flags; } } polyfill-php80/Resources/stubs/Stringable.php 0000644 00000000614 15025017654 0015256 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000) { interface Stringable { /** * @return string */ public function __toString(); } } polyfill-php80/Resources/stubs/PhpToken.php 0000644 00000000567 15025017654 0014723 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000 && extension_loaded('tokenizer')) { class PhpToken extends Symfony\Polyfill\Php80\PhpToken { } } polyfill-php80/Resources/stubs/UnhandledMatchError.php 0000644 00000000507 15025017654 0017056 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000) { class UnhandledMatchError extends Error { } } polyfill-php80/Resources/stubs/ValueError.php 0000644 00000000476 15025017654 0015260 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000) { class ValueError extends Error { } } uid/BinaryUtil.php 0000644 00000013624 15025017654 0010133 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid; /** * @internal * * @author Nicolas Grekas <p@tchwork.com> */ class BinaryUtil { public const BASE10 = [ '' => '0123456789', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ]; public const BASE58 = [ '' => '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', 1 => 0, 1, 2, 3, 4, 5, 6, 7, 8, 'A' => 9, 'B' => 10, 'C' => 11, 'D' => 12, 'E' => 13, 'F' => 14, 'G' => 15, 'H' => 16, 'J' => 17, 'K' => 18, 'L' => 19, 'M' => 20, 'N' => 21, 'P' => 22, 'Q' => 23, 'R' => 24, 'S' => 25, 'T' => 26, 'U' => 27, 'V' => 28, 'W' => 29, 'X' => 30, 'Y' => 31, 'Z' => 32, 'a' => 33, 'b' => 34, 'c' => 35, 'd' => 36, 'e' => 37, 'f' => 38, 'g' => 39, 'h' => 40, 'i' => 41, 'j' => 42, 'k' => 43, 'm' => 44, 'n' => 45, 'o' => 46, 'p' => 47, 'q' => 48, 'r' => 49, 's' => 50, 't' => 51, 'u' => 52, 'v' => 53, 'w' => 54, 'x' => 55, 'y' => 56, 'z' => 57, ]; // https://tools.ietf.org/html/rfc4122#section-4.1.4 // 0x01b21dd213814000 is the number of 100-ns intervals between the // UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00. private const TIME_OFFSET_INT = 0x01B21DD213814000; private const TIME_OFFSET_BIN = "\x01\xb2\x1d\xd2\x13\x81\x40\x00"; private const TIME_OFFSET_COM1 = "\xfe\x4d\xe2\x2d\xec\x7e\xbf\xff"; private const TIME_OFFSET_COM2 = "\xfe\x4d\xe2\x2d\xec\x7e\xc0\x00"; public static function toBase(string $bytes, array $map): string { $base = \strlen($alphabet = $map['']); $bytes = array_values(unpack(\PHP_INT_SIZE >= 8 ? 'n*' : 'C*', $bytes)); $digits = ''; while ($count = \count($bytes)) { $quotient = []; $remainder = 0; for ($i = 0; $i !== $count; ++$i) { $carry = $bytes[$i] + ($remainder << (\PHP_INT_SIZE >= 8 ? 16 : 8)); $digit = intdiv($carry, $base); $remainder = $carry % $base; if ($digit || $quotient) { $quotient[] = $digit; } } $digits = $alphabet[$remainder].$digits; $bytes = $quotient; } return $digits; } public static function fromBase(string $digits, array $map): string { $base = \strlen($map['']); $count = \strlen($digits); $bytes = []; while ($count) { $quotient = []; $remainder = 0; for ($i = 0; $i !== $count; ++$i) { $carry = ($bytes ? $digits[$i] : $map[$digits[$i]]) + $remainder * $base; if (\PHP_INT_SIZE >= 8) { $digit = $carry >> 16; $remainder = $carry & 0xFFFF; } else { $digit = $carry >> 8; $remainder = $carry & 0xFF; } if ($digit || $quotient) { $quotient[] = $digit; } } $bytes[] = $remainder; $count = \count($digits = $quotient); } return pack(\PHP_INT_SIZE >= 8 ? 'n*' : 'C*', ...array_reverse($bytes)); } public static function add(string $a, string $b): string { $carry = 0; for ($i = 7; 0 <= $i; --$i) { $carry += \ord($a[$i]) + \ord($b[$i]); $a[$i] = \chr($carry & 0xFF); $carry >>= 8; } return $a; } /** * @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal */ public static function hexToDateTime(string $time): \DateTimeImmutable { if (\PHP_INT_SIZE >= 8) { $time = (string) (hexdec($time) - self::TIME_OFFSET_INT); } else { $time = str_pad(hex2bin($time), 8, "\0", \STR_PAD_LEFT); if (self::TIME_OFFSET_BIN <= $time) { $time = self::add($time, self::TIME_OFFSET_COM2); $time[0] = $time[0] & "\x7F"; $time = self::toBase($time, self::BASE10); } else { $time = self::add($time, self::TIME_OFFSET_COM1); $time = '-'.self::toBase($time ^ "\xff\xff\xff\xff\xff\xff\xff\xff", self::BASE10); } } if (9 > \strlen($time)) { $time = '-' === $time[0] ? '-'.str_pad(substr($time, 1), 8, '0', \STR_PAD_LEFT) : str_pad($time, 8, '0', \STR_PAD_LEFT); } return \DateTimeImmutable::createFromFormat('U.u?', substr_replace($time, '.', -7, 0)); } /** * @return string Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal */ public static function dateTimeToHex(\DateTimeInterface $time): string { if (\PHP_INT_SIZE >= 8) { if (-self::TIME_OFFSET_INT > $time = (int) $time->format('Uu0')) { throw new \InvalidArgumentException('The given UUID date cannot be earlier than 1582-10-15.'); } return str_pad(dechex(self::TIME_OFFSET_INT + $time), 16, '0', \STR_PAD_LEFT); } $time = $time->format('Uu0'); $negative = '-' === $time[0]; if ($negative && self::TIME_OFFSET_INT < $time = substr($time, 1)) { throw new \InvalidArgumentException('The given UUID date cannot be earlier than 1582-10-15.'); } $time = self::fromBase($time, self::BASE10); $time = str_pad($time, 8, "\0", \STR_PAD_LEFT); if ($negative) { $time = self::add($time, self::TIME_OFFSET_COM1) ^ "\xff\xff\xff\xff\xff\xff\xff\xff"; } else { $time = self::add($time, self::TIME_OFFSET_BIN); } return bin2hex($time); } } uid/composer.json 0000644 00000001627 15025017654 0010062 0 ustar 00 { "name": "symfony/uid", "type": "library", "description": "Provides an object-oriented API to generate and represent UIDs", "keywords": ["uid", "uuid", "ulid"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Grégoire Pineau", "email": "lyrixx@lyrixx.info" }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.0.2", "symfony/polyfill-uuid": "^1.15" }, "require-dev": { "symfony/console": "^5.4|^6.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Uid\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } uid/UuidV6.php 0000644 00000003764 15025017654 0007177 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid; /** * A v6 UUID is lexicographically sortable and contains a 60-bit timestamp and 62 extra unique bits. * * Unlike UUIDv1, this implementation of UUIDv6 doesn't leak the MAC address of the host. * * @author Nicolas Grekas <p@tchwork.com> */ class UuidV6 extends Uuid { protected const TYPE = 6; private static string $node; public function __construct(string $uuid = null) { if (null === $uuid) { $this->uid = static::generate(); } else { parent::__construct($uuid, true); } } public function getDateTime(): \DateTimeImmutable { return BinaryUtil::hexToDateTime('0'.substr($this->uid, 0, 8).substr($this->uid, 9, 4).substr($this->uid, 15, 3)); } public function getNode(): string { return substr($this->uid, 24); } public static function generate(\DateTimeInterface $time = null, Uuid $node = null): string { $uuidV1 = UuidV1::generate($time, $node); $uuid = substr($uuidV1, 15, 3).substr($uuidV1, 9, 4).$uuidV1[0].'-'.substr($uuidV1, 1, 4).'-6'.substr($uuidV1, 5, 3).substr($uuidV1, 18, 6); if ($node) { return $uuid.substr($uuidV1, 24); } // uuid_create() returns a stable "node" that can leak the MAC of the host, but // UUIDv6 prefers a truly random number here, let's XOR both to preserve the entropy if (!isset(self::$node)) { $seed = [random_int(0, 0xFFFFFF), random_int(0, 0xFFFFFF)]; $node = unpack('N2', hex2bin('00'.substr($uuidV1, 24, 6)).hex2bin('00'.substr($uuidV1, 30))); self::$node = sprintf('%06x%06x', ($seed[0] ^ $node[1]) | 0x010000, $seed[1] ^ $node[2]); } return $uuid.self::$node; } } uid/CHANGELOG.md 0000644 00000001463 15025017654 0007147 0 ustar 00 CHANGELOG ========= 5.4 --- * Add `NilUlid` 5.3 --- * The component is not marked as `@experimental` anymore * Add `AbstractUid::fromBinary()`, `AbstractUid::fromBase58()`, `AbstractUid::fromBase32()` and `AbstractUid::fromRfc4122()` * [BC BREAK] Replace `UuidV1::getTime()`, `UuidV6::getTime()` and `Ulid::getTime()` by `UuidV1::getDateTime()`, `UuidV6::getDateTime()` and `Ulid::getDateTime()` * Add `Uuid::NAMESPACE_*` constants from RFC4122 * Add `UlidFactory`, `UuidFactory`, `RandomBasedUuidFactory`, `TimeBasedUuidFactory` and `NameBasedUuidFactory` * Add commands to generate and inspect UUIDs and ULIDs 5.2.0 ----- * made UUIDv6 always return truly random node fields to prevent leaking the MAC of the host 5.1.0 ----- * added support for UUID * added support for ULID * added the component uid/Ulid.php 0000644 00000014661 15025017654 0006750 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid; /** * A ULID is lexicographically sortable and contains a 48-bit timestamp and 80-bit of crypto-random entropy. * * @see https://github.com/ulid/spec * * @author Nicolas Grekas <p@tchwork.com> */ class Ulid extends AbstractUid { protected const NIL = '00000000000000000000000000'; private static string $time = ''; private static array $rand = []; public function __construct(string $ulid = null) { if (null === $ulid) { $this->uid = static::generate(); return; } if (self::NIL === $ulid) { $this->uid = $ulid; return; } if (!self::isValid($ulid)) { throw new \InvalidArgumentException(sprintf('Invalid ULID: "%s".', $ulid)); } $this->uid = strtoupper($ulid); } public static function isValid(string $ulid): bool { if (26 !== \strlen($ulid)) { return false; } if (26 !== strspn($ulid, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz')) { return false; } return $ulid[0] <= '7'; } /** * {@inheritdoc} */ public static function fromString(string $ulid): static { if (36 === \strlen($ulid) && preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $ulid)) { $ulid = uuid_parse($ulid); } elseif (22 === \strlen($ulid) && 22 === strspn($ulid, BinaryUtil::BASE58[''])) { $ulid = str_pad(BinaryUtil::fromBase($ulid, BinaryUtil::BASE58), 16, "\0", \STR_PAD_LEFT); } if (16 !== \strlen($ulid)) { if (self::NIL === $ulid) { return new NilUlid(); } return new static($ulid); } $ulid = bin2hex($ulid); $ulid = sprintf('%02s%04s%04s%04s%04s%04s%04s', base_convert(substr($ulid, 0, 2), 16, 32), base_convert(substr($ulid, 2, 5), 16, 32), base_convert(substr($ulid, 7, 5), 16, 32), base_convert(substr($ulid, 12, 5), 16, 32), base_convert(substr($ulid, 17, 5), 16, 32), base_convert(substr($ulid, 22, 5), 16, 32), base_convert(substr($ulid, 27, 5), 16, 32) ); if (self::NIL === $ulid) { return new NilUlid(); } $u = new static(self::NIL); $u->uid = strtr($ulid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ'); return $u; } public function toBinary(): string { $ulid = strtr($this->uid, 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv'); $ulid = sprintf('%02s%05s%05s%05s%05s%05s%05s', base_convert(substr($ulid, 0, 2), 32, 16), base_convert(substr($ulid, 2, 4), 32, 16), base_convert(substr($ulid, 6, 4), 32, 16), base_convert(substr($ulid, 10, 4), 32, 16), base_convert(substr($ulid, 14, 4), 32, 16), base_convert(substr($ulid, 18, 4), 32, 16), base_convert(substr($ulid, 22, 4), 32, 16) ); return hex2bin($ulid); } public function toBase32(): string { return $this->uid; } public function getDateTime(): \DateTimeImmutable { $time = strtr(substr($this->uid, 0, 10), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv'); if (\PHP_INT_SIZE >= 8) { $time = (string) hexdec(base_convert($time, 32, 16)); } else { $time = sprintf('%02s%05s%05s', base_convert(substr($time, 0, 2), 32, 16), base_convert(substr($time, 2, 4), 32, 16), base_convert(substr($time, 6, 4), 32, 16) ); $time = BinaryUtil::toBase(hex2bin($time), BinaryUtil::BASE10); } if (4 > \strlen($time)) { $time = '000'.$time; } return \DateTimeImmutable::createFromFormat('U.u', substr_replace($time, '.', -3, 0)); } public static function generate(\DateTimeInterface $time = null): string { if (null === $mtime = $time) { $time = microtime(false); $time = substr($time, 11).substr($time, 2, 3); } elseif (0 > $time = $time->format('Uv')) { throw new \InvalidArgumentException('The timestamp must be positive.'); } if ($time > self::$time || (null !== $mtime && $time !== self::$time)) { randomize: $r = unpack('nr1/nr2/nr3/nr4/nr', random_bytes(10)); $r['r1'] |= ($r['r'] <<= 4) & 0xF0000; $r['r2'] |= ($r['r'] <<= 4) & 0xF0000; $r['r3'] |= ($r['r'] <<= 4) & 0xF0000; $r['r4'] |= ($r['r'] <<= 4) & 0xF0000; unset($r['r']); self::$rand = array_values($r); self::$time = $time; } elseif ([0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) { if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time = self::$time)) { $time = (string) (1 + $time); } elseif ('999999999' === $mtime = substr($time, -9)) { $time = (1 + substr($time, 0, -9)).'000000000'; } else { $time = substr_replace($time, str_pad(++$mtime, 9, '0', \STR_PAD_LEFT), -9); } goto randomize; } else { for ($i = 3; $i >= 0 && 0xFFFFF === self::$rand[$i]; --$i) { self::$rand[$i] = 0; } ++self::$rand[$i]; $time = self::$time; } if (\PHP_INT_SIZE >= 8) { $time = base_convert($time, 10, 32); } else { $time = str_pad(bin2hex(BinaryUtil::fromBase($time, BinaryUtil::BASE10)), 12, '0', \STR_PAD_LEFT); $time = sprintf('%s%04s%04s', base_convert(substr($time, 0, 2), 16, 32), base_convert(substr($time, 2, 5), 16, 32), base_convert(substr($time, 7, 5), 16, 32) ); } return strtr(sprintf('%010s%04s%04s%04s%04s', $time, base_convert(self::$rand[0], 10, 32), base_convert(self::$rand[1], 10, 32), base_convert(self::$rand[2], 10, 32), base_convert(self::$rand[3], 10, 32) ), 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ'); } } uid/UuidV1.php 0000644 00000003764 15025017654 0007172 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid; /** * A v1 UUID contains a 60-bit timestamp and 62 extra unique bits. * * @author Grégoire Pineau <lyrixx@lyrixx.info> */ class UuidV1 extends Uuid { protected const TYPE = 1; private static ?string $clockSeq = null; public function __construct(string $uuid = null) { if (null === $uuid) { $this->uid = uuid_create(static::TYPE); } else { parent::__construct($uuid, true); } } public function getDateTime(): \DateTimeImmutable { return BinaryUtil::hexToDateTime('0'.substr($this->uid, 15, 3).substr($this->uid, 9, 4).substr($this->uid, 0, 8)); } public function getNode(): string { return uuid_mac($this->uid); } public static function generate(\DateTimeInterface $time = null, Uuid $node = null): string { $uuid = !$time || !$node ? uuid_create(static::TYPE) : parent::NIL; if ($time) { if ($node) { // use clock_seq from the node $seq = substr($node->uid, 19, 4); } else { // generate a static random clock_seq to prevent any collisions with the real one $seq = substr($uuid, 19, 4); while (null === self::$clockSeq || $seq === self::$clockSeq) { self::$clockSeq = sprintf('%04x', random_int(0, 0x3FFF) | 0x8000); } $seq = self::$clockSeq; } $time = BinaryUtil::dateTimeToHex($time); $uuid = substr($time, 8).'-'.substr($time, 4, 4).'-1'.substr($time, 1, 3).'-'.$seq.substr($uuid, 23); } if ($node) { $uuid = substr($uuid, 0, 24).substr($node->uid, 24); } return $uuid; } } uid/UuidV3.php 0000644 00000001120 15025017654 0007154 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid; /** * A v3 UUID contains an MD5 hash of another UUID and a name. * * Use Uuid::v3() to compute one. * * @author Grégoire Pineau <lyrixx@lyrixx.info> */ class UuidV3 extends Uuid { protected const TYPE = 3; public function __construct(string $uuid) { parent::__construct($uuid, true); } } uid/Command/InspectUlidCommand.php 0000644 00000004242 15025017654 0013145 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Uid\Ulid; #[AsCommand(name: 'ulid:inspect', description: 'Inspect a ULID')] class InspectUlidCommand extends Command { /** * {@inheritdoc} */ protected function configure(): void { $this ->setDefinition([ new InputArgument('ulid', InputArgument::REQUIRED, 'The ULID to inspect'), ]) ->setHelp(<<<'EOF' The <info>%command.name%</info> displays information about a ULID. <info>php %command.full_name% 01EWAKBCMWQ2C94EXNN60ZBS0Q</info> <info>php %command.full_name% 1BVdfLn3ERmbjYBLCdaaLW</info> <info>php %command.full_name% 01771535-b29c-b898-923b-b5a981f5e417</info> EOF ) ; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); try { $ulid = Ulid::fromString($input->getArgument('ulid')); } catch (\InvalidArgumentException $e) { $io->error($e->getMessage()); return 1; } $io->table(['Label', 'Value'], [ ['toBase32 (canonical)', (string) $ulid], ['toBase58', $ulid->toBase58()], ['toRfc4122', $ulid->toRfc4122()], new TableSeparator(), ['Time', $ulid->getDateTime()->format('Y-m-d H:i:s.v \U\T\C')], ]); return 0; } } uid/Command/GenerateUuidCommand.php 0000644 00000016543 15025017654 0013312 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Uid\Factory\UuidFactory; use Symfony\Component\Uid\Uuid; #[AsCommand(name: 'uuid:generate', description: 'Generate a UUID')] class GenerateUuidCommand extends Command { private $factory; public function __construct(UuidFactory $factory = null) { $this->factory = $factory ?? new UuidFactory(); parent::__construct(); } /** * {@inheritdoc} */ protected function configure(): void { $this ->setDefinition([ new InputOption('time-based', null, InputOption::VALUE_REQUIRED, 'The timestamp, to generate a time-based UUID: a parsable date/time string'), new InputOption('node', null, InputOption::VALUE_REQUIRED, 'The UUID whose node part should be used as the node of the generated UUID'), new InputOption('name-based', null, InputOption::VALUE_REQUIRED, 'The name, to generate a name-based UUID'), new InputOption('namespace', null, InputOption::VALUE_REQUIRED, 'The UUID to use at the namespace for named-based UUIDs, predefined namespaces keywords "dns", "url", "oid" and "x500" are accepted'), new InputOption('random-based', null, InputOption::VALUE_NONE, 'To generate a random-based UUID'), new InputOption('count', 'c', InputOption::VALUE_REQUIRED, 'The number of UUID to generate', 1), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'The UUID output format: rfc4122, base58 or base32', 'rfc4122'), ]) ->setHelp(<<<'EOF' The <info>%command.name%</info> generates a UUID. <info>php %command.full_name%</info> To generate a time-based UUID: <info>php %command.full_name% --time-based=now</info> To specify a time-based UUID's node: <info>php %command.full_name% --time-based=@1613480254 --node=fb3502dc-137e-4849-8886-ac90d07f64a7</info> To generate a name-based UUID: <info>php %command.full_name% --name-based=foo</info> To specify a name-based UUID's namespace: <info>php %command.full_name% --name-based=bar --namespace=fb3502dc-137e-4849-8886-ac90d07f64a7</info> To generate a random-based UUID: <info>php %command.full_name% --random-based</info> To generate several UUIDs: <info>php %command.full_name% --count=10</info> To output a specific format: <info>php %command.full_name% --format=base58</info> EOF ) ; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); $time = $input->getOption('time-based'); $node = $input->getOption('node'); $name = $input->getOption('name-based'); $namespace = $input->getOption('namespace'); $random = $input->getOption('random-based'); if (false !== ($time ?? $name ?? $random) && 1 < ((null !== $time) + (null !== $name) + $random)) { $io->error('Only one of "--time-based", "--name-based" or "--random-based" can be provided at a time.'); return 1; } if (null === $time && null !== $node) { $io->error('Option "--node" can only be used with "--time-based".'); return 1; } if (null === $name && null !== $namespace) { $io->error('Option "--namespace" can only be used with "--name-based".'); return 1; } switch (true) { case null !== $time: if (null !== $node) { try { $node = Uuid::fromString($node); } catch (\InvalidArgumentException $e) { $io->error(sprintf('Invalid node "%s": %s', $node, $e->getMessage())); return 1; } } try { new \DateTimeImmutable($time); } catch (\Exception $e) { $io->error(sprintf('Invalid timestamp "%s": %s', $time, str_replace('DateTimeImmutable::__construct(): ', '', $e->getMessage()))); return 1; } $create = function () use ($node, $time): Uuid { return $this->factory->timeBased($node)->create(new \DateTimeImmutable($time)); }; break; case null !== $name: if ($namespace && !\in_array($namespace, ['dns', 'url', 'oid', 'x500'], true)) { try { $namespace = Uuid::fromString($namespace); } catch (\InvalidArgumentException $e) { $io->error(sprintf('Invalid namespace "%s": %s', $namespace, $e->getMessage())); return 1; } } $create = function () use ($namespace, $name): Uuid { try { $factory = $this->factory->nameBased($namespace); } catch (\LogicException $e) { throw new \InvalidArgumentException('Missing namespace: use the "--namespace" option or configure a default namespace in the underlying factory.'); } return $factory->create($name); }; break; case $random: $create = [$this->factory->randomBased(), 'create']; break; default: $create = [$this->factory, 'create']; break; } $formatOption = $input->getOption('format'); if (\in_array($formatOption, $this->getAvailableFormatOptions())) { $format = 'to'.ucfirst($formatOption); } else { $io->error(sprintf('Invalid format "%s", did you mean "base32", "base58" or "rfc4122"?', $formatOption)); return 1; } $count = (int) $input->getOption('count'); try { for ($i = 0; $i < $count; ++$i) { $output->writeln($create()->$format()); } } catch (\Exception $e) { $io->error($e->getMessage()); return 1; } return 0; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues($this->getAvailableFormatOptions()); } } private function getAvailableFormatOptions(): array { return [ 'base32', 'base58', 'rfc4122', ]; } } uid/Command/InspectUuidCommand.php 0000644 00000005147 15025017654 0013163 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\UuidV1; use Symfony\Component\Uid\UuidV6; #[AsCommand(name: 'uuid:inspect', description: 'Inspect a UUID')] class InspectUuidCommand extends Command { /** * {@inheritdoc} */ protected function configure(): void { $this ->setDefinition([ new InputArgument('uuid', InputArgument::REQUIRED, 'The UUID to inspect'), ]) ->setHelp(<<<'EOF' The <info>%command.name%</info> displays information about a UUID. <info>php %command.full_name% a7613e0a-5986-11eb-a861-2bf05af69e52</info> <info>php %command.full_name% MfnmaUvvQ1h8B14vTwt6dX</info> <info>php %command.full_name% 57C4Z0MPC627NTGR9BY1DFD7JJ</info> EOF ) ; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); try { /** @var Uuid $uuid */ $uuid = Uuid::fromString($input->getArgument('uuid')); } catch (\InvalidArgumentException $e) { $io->error($e->getMessage()); return 1; } if (-1 === $version = uuid_type($uuid)) { $version = 'nil'; } elseif (0 === $version || 2 === $version || 6 < $version) { $version = 'unknown'; } $rows = [ ['Version', $version], ['toRfc4122 (canonical)', (string) $uuid], ['toBase58', $uuid->toBase58()], ['toBase32', $uuid->toBase32()], ]; if ($uuid instanceof UuidV1 || $uuid instanceof UuidV6) { $rows[] = new TableSeparator(); $rows[] = ['Time', $uuid->getDateTime()->format('Y-m-d H:i:s.u \U\T\C')]; } $io->table(['Label', 'Value'], $rows); return 0; } } uid/Command/GenerateUlidCommand.php 0000644 00000007201 15025017654 0013270 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Uid\Factory\UlidFactory; #[AsCommand(name: 'ulid:generate', description: 'Generate a ULID')] class GenerateUlidCommand extends Command { private const FORMAT_OPTIONS = [ 'base32', 'base58', 'rfc4122', ]; private $factory; public function __construct(UlidFactory $factory = null) { $this->factory = $factory ?? new UlidFactory(); parent::__construct(); } /** * {@inheritdoc} */ protected function configure(): void { $this ->setDefinition([ new InputOption('time', null, InputOption::VALUE_REQUIRED, 'The ULID timestamp: a parsable date/time string'), new InputOption('count', 'c', InputOption::VALUE_REQUIRED, 'The number of ULID to generate', 1), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'The ULID output format: base32, base58 or rfc4122', 'base32'), ]) ->setHelp(<<<'EOF' The <info>%command.name%</info> command generates a ULID. <info>php %command.full_name%</info> To specify the timestamp: <info>php %command.full_name% --time="2021-02-16 14:09:08"</info> To generate several ULIDs: <info>php %command.full_name% --count=10</info> To output a specific format: <info>php %command.full_name% --format=rfc4122</info> EOF ) ; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); if (null !== $time = $input->getOption('time')) { try { $time = new \DateTimeImmutable($time); } catch (\Exception $e) { $io->error(sprintf('Invalid timestamp "%s": %s', $time, str_replace('DateTimeImmutable::__construct(): ', '', $e->getMessage()))); return 1; } } $formatOption = $input->getOption('format'); if (\in_array($formatOption, self::FORMAT_OPTIONS)) { $format = 'to'.ucfirst($formatOption); } else { $io->error(sprintf('Invalid format "%s", did you mean "base32", "base58" or "rfc4122"?', $input->getOption('format'))); return 1; } $count = (int) $input->getOption('count'); try { for ($i = 0; $i < $count; ++$i) { $output->writeln($this->factory->create($time)->$format()); } } catch (\Exception $e) { $io->error($e->getMessage()); return 1; } return 0; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues(self::FORMAT_OPTIONS); } } } uid/UuidV5.php 0000644 00000001120 15025017654 0007156 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid; /** * A v5 UUID contains a SHA1 hash of another UUID and a name. * * Use Uuid::v5() to compute one. * * @author Grégoire Pineau <lyrixx@lyrixx.info> */ class UuidV5 extends Uuid { protected const TYPE = 5; public function __construct(string $uuid) { parent::__construct($uuid, true); } } uid/README.md 0000644 00000000744 15025017654 0006616 0 ustar 00 Uid Component ============= The UID component provides an object-oriented API to generate and represent UIDs. Resources --------- * [Documentation](https://symfony.com/doc/current/components/uid.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) uid/LICENSE 0000644 00000002051 15025017654 0006335 0 ustar 00 Copyright (c) 2020-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. uid/AbstractUid.php 0000644 00000010402 15025017654 0010245 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid; /** * @author Nicolas Grekas <p@tchwork.com> */ abstract class AbstractUid implements \JsonSerializable { /** * The identifier in its canonic representation. */ protected $uid; /** * Whether the passed value is valid for the constructor of the current class. */ abstract public static function isValid(string $uid): bool; /** * Creates an AbstractUid from an identifier represented in any of the supported formats. * * @throws \InvalidArgumentException When the passed value is not valid */ abstract public static function fromString(string $uid): static; /** * @throws \InvalidArgumentException When the passed value is not valid */ public static function fromBinary(string $uid): static { if (16 !== \strlen($uid)) { throw new \InvalidArgumentException('Invalid binary uid provided.'); } return static::fromString($uid); } /** * @throws \InvalidArgumentException When the passed value is not valid */ public static function fromBase58(string $uid): static { if (22 !== \strlen($uid)) { throw new \InvalidArgumentException('Invalid base-58 uid provided.'); } return static::fromString($uid); } /** * @throws \InvalidArgumentException When the passed value is not valid */ public static function fromBase32(string $uid): static { if (26 !== \strlen($uid)) { throw new \InvalidArgumentException('Invalid base-32 uid provided.'); } return static::fromString($uid); } /** * @throws \InvalidArgumentException When the passed value is not valid */ public static function fromRfc4122(string $uid): static { if (36 !== \strlen($uid)) { throw new \InvalidArgumentException('Invalid RFC4122 uid provided.'); } return static::fromString($uid); } /** * Returns the identifier as a raw binary string. */ abstract public function toBinary(): string; /** * Returns the identifier as a base58 case sensitive string. */ public function toBase58(): string { return strtr(sprintf('%022s', BinaryUtil::toBase($this->toBinary(), BinaryUtil::BASE58)), '0', '1'); } /** * Returns the identifier as a base32 case insensitive string. */ public function toBase32(): string { $uid = bin2hex($this->toBinary()); $uid = sprintf('%02s%04s%04s%04s%04s%04s%04s', base_convert(substr($uid, 0, 2), 16, 32), base_convert(substr($uid, 2, 5), 16, 32), base_convert(substr($uid, 7, 5), 16, 32), base_convert(substr($uid, 12, 5), 16, 32), base_convert(substr($uid, 17, 5), 16, 32), base_convert(substr($uid, 22, 5), 16, 32), base_convert(substr($uid, 27, 5), 16, 32) ); return strtr($uid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ'); } /** * Returns the identifier as a RFC4122 case insensitive string. */ public function toRfc4122(): string { // don't use uuid_unparse(), it's slower $uuid = bin2hex($this->toBinary()); $uuid = substr_replace($uuid, '-', 8, 0); $uuid = substr_replace($uuid, '-', 13, 0); $uuid = substr_replace($uuid, '-', 18, 0); return substr_replace($uuid, '-', 23, 0); } /** * Returns whether the argument is an AbstractUid and contains the same value as the current instance. */ public function equals(mixed $other): bool { if (!$other instanceof self) { return false; } return $this->uid === $other->uid; } public function compare(self $other): int { return (\strlen($this->uid) - \strlen($other->uid)) ?: ($this->uid <=> $other->uid); } public function __toString(): string { return $this->uid; } public function jsonSerialize(): string { return $this->uid; } } uid/Uuid.php 0000644 00000012053 15025017654 0006752 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid; /** * @author Grégoire Pineau <lyrixx@lyrixx.info> * * @see https://tools.ietf.org/html/rfc4122#appendix-C for details about namespaces */ class Uuid extends AbstractUid { public const NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; public const NAMESPACE_URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; public const NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8'; public const NAMESPACE_X500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8'; protected const TYPE = 0; protected const NIL = '00000000-0000-0000-0000-000000000000'; public function __construct(string $uuid, bool $checkVariant = false) { $type = preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $uuid) ? (int) $uuid[14] : false; if (false === $type || (static::TYPE ?: $type) !== $type) { throw new \InvalidArgumentException(sprintf('Invalid UUID%s: "%s".', static::TYPE ? 'v'.static::TYPE : '', $uuid)); } $this->uid = strtolower($uuid); if ($checkVariant && !\in_array($this->uid[19], ['8', '9', 'a', 'b'], true)) { throw new \InvalidArgumentException(sprintf('Invalid UUID%s: "%s".', static::TYPE ? 'v'.static::TYPE : '', $uuid)); } } public static function fromString(string $uuid): static { if (22 === \strlen($uuid) && 22 === strspn($uuid, BinaryUtil::BASE58[''])) { $uuid = str_pad(BinaryUtil::fromBase($uuid, BinaryUtil::BASE58), 16, "\0", \STR_PAD_LEFT); } if (16 === \strlen($uuid)) { // don't use uuid_unparse(), it's slower $uuid = bin2hex($uuid); $uuid = substr_replace($uuid, '-', 8, 0); $uuid = substr_replace($uuid, '-', 13, 0); $uuid = substr_replace($uuid, '-', 18, 0); $uuid = substr_replace($uuid, '-', 23, 0); } elseif (26 === \strlen($uuid) && Ulid::isValid($uuid)) { $ulid = new NilUlid(); $ulid->uid = strtoupper($uuid); $uuid = $ulid->toRfc4122(); } if (__CLASS__ !== static::class || 36 !== \strlen($uuid)) { return new static($uuid); } if (self::NIL === $uuid) { return new NilUuid(); } if (\in_array($uuid[19], ['8', '9', 'a', 'b', 'A', 'B'], true)) { switch ($uuid[14]) { case UuidV1::TYPE: return new UuidV1($uuid); case UuidV3::TYPE: return new UuidV3($uuid); case UuidV4::TYPE: return new UuidV4($uuid); case UuidV5::TYPE: return new UuidV5($uuid); case UuidV6::TYPE: return new UuidV6($uuid); } } return new self($uuid); } final public static function v1(): UuidV1 { return new UuidV1(); } final public static function v3(self $namespace, string $name): UuidV3 { // don't use uuid_generate_md5(), some versions are buggy $uuid = md5(hex2bin(str_replace('-', '', $namespace->uid)).$name, true); return new UuidV3(self::format($uuid, '-3')); } final public static function v4(): UuidV4 { return new UuidV4(); } final public static function v5(self $namespace, string $name): UuidV5 { // don't use uuid_generate_sha1(), some versions are buggy $uuid = substr(sha1(hex2bin(str_replace('-', '', $namespace->uid)).$name, true), 0, 16); return new UuidV5(self::format($uuid, '-5')); } final public static function v6(): UuidV6 { return new UuidV6(); } public static function isValid(string $uuid): bool { if (self::NIL === $uuid && \in_array(static::class, [__CLASS__, NilUuid::class], true)) { return true; } if (__CLASS__ === static::class && 'ffffffff-ffff-ffff-ffff-ffffffffffff' === strtr($uuid, 'F', 'f')) { return true; } if (!preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){2}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$}Di', $uuid)) { return false; } return __CLASS__ === static::class || static::TYPE === (int) $uuid[14]; } public function toBinary(): string { return uuid_parse($this->uid); } public function toRfc4122(): string { return $this->uid; } public function compare(AbstractUid $other): int { if (false !== $cmp = uuid_compare($this->uid, $other->uid)) { return $cmp; } return parent::compare($other); } private static function format(string $uuid, string $version): string { $uuid[8] = $uuid[8] & "\x3F" | "\x80"; $uuid = substr_replace(bin2hex($uuid), '-', 8, 0); $uuid = substr_replace($uuid, $version, 13, 1); $uuid = substr_replace($uuid, '-', 18, 0); return substr_replace($uuid, '-', 23, 0); } } uid/UuidV4.php 0000644 00000001643 15025017654 0007167 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid; /** * A v4 UUID contains a 122-bit random number. * * @author Grégoire Pineau <lyrixx@lyrixx.info> */ class UuidV4 extends Uuid { protected const TYPE = 4; public function __construct(string $uuid = null) { if (null === $uuid) { $uuid = random_bytes(16); $uuid[6] = $uuid[6] & "\x0F" | "\x40"; $uuid[8] = $uuid[8] & "\x3F" | "\x80"; $uuid = bin2hex($uuid); $this->uid = substr($uuid, 0, 8).'-'.substr($uuid, 8, 4).'-'.substr($uuid, 12, 4).'-'.substr($uuid, 16, 4).'-'.substr($uuid, 20, 12); } else { parent::__construct($uuid, true); } } } uid/NilUuid.php 0000644 00000000730 15025017654 0007414 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid; /** * @author Grégoire Pineau <lyrixx@lyrixx.info> */ class NilUuid extends Uuid { protected const TYPE = -1; public function __construct() { $this->uid = parent::NIL; } } uid/NilUlid.php 0000644 00000000577 15025017654 0007414 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid; class NilUlid extends Ulid { public function __construct() { $this->uid = parent::NIL; } } uid/Factory/NameBasedUuidFactory.php 0000644 00000002120 15025017654 0013443 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Factory; use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\UuidV3; use Symfony\Component\Uid\UuidV5; class NameBasedUuidFactory { private string $class; private $namespace; public function __construct(string $class, Uuid $namespace) { $this->class = $class; $this->namespace = $namespace; } public function create(string $name): UuidV5|UuidV3 { switch ($class = $this->class) { case UuidV5::class: return Uuid::v5($this->namespace, $name); case UuidV3::class: return Uuid::v3($this->namespace, $name); } if (is_subclass_of($class, UuidV5::class)) { $uuid = Uuid::v5($this->namespace, $name); } else { $uuid = Uuid::v3($this->namespace, $name); } return new $class($uuid); } } uid/Factory/UuidFactory.php 0000644 00000006403 15025017654 0011713 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Factory; use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\UuidV1; use Symfony\Component\Uid\UuidV4; use Symfony\Component\Uid\UuidV5; use Symfony\Component\Uid\UuidV6; class UuidFactory { private string $defaultClass; private string $timeBasedClass; private string $nameBasedClass; private string $randomBasedClass; private $timeBasedNode; private $nameBasedNamespace; public function __construct(string|int $defaultClass = UuidV6::class, string|int $timeBasedClass = UuidV6::class, string|int $nameBasedClass = UuidV5::class, string|int $randomBasedClass = UuidV4::class, Uuid|string $timeBasedNode = null, Uuid|string $nameBasedNamespace = null) { if (null !== $timeBasedNode && !$timeBasedNode instanceof Uuid) { $timeBasedNode = Uuid::fromString($timeBasedNode); } if (null !== $nameBasedNamespace) { $nameBasedNamespace = $this->getNamespace($nameBasedNamespace); } $this->defaultClass = is_numeric($defaultClass) ? Uuid::class.'V'.$defaultClass : $defaultClass; $this->timeBasedClass = is_numeric($timeBasedClass) ? Uuid::class.'V'.$timeBasedClass : $timeBasedClass; $this->nameBasedClass = is_numeric($nameBasedClass) ? Uuid::class.'V'.$nameBasedClass : $nameBasedClass; $this->randomBasedClass = is_numeric($randomBasedClass) ? Uuid::class.'V'.$randomBasedClass : $randomBasedClass; $this->timeBasedNode = $timeBasedNode; $this->nameBasedNamespace = $nameBasedNamespace; } public function create(): UuidV6|UuidV4|UuidV1 { $class = $this->defaultClass; return new $class(); } public function randomBased(): RandomBasedUuidFactory { return new RandomBasedUuidFactory($this->randomBasedClass); } public function timeBased(Uuid|string $node = null): TimeBasedUuidFactory { $node ?? $node = $this->timeBasedNode; if (null !== $node && !$node instanceof Uuid) { $node = Uuid::fromString($node); } return new TimeBasedUuidFactory($this->timeBasedClass, $node); } public function nameBased(Uuid|string $namespace = null): NameBasedUuidFactory { $namespace ?? $namespace = $this->nameBasedNamespace; if (null === $namespace) { throw new \LogicException(sprintf('A namespace should be defined when using "%s()".', __METHOD__)); } return new NameBasedUuidFactory($this->nameBasedClass, $this->getNamespace($namespace)); } private function getNamespace(Uuid|string $namespace): Uuid { if ($namespace instanceof Uuid) { return $namespace; } switch ($namespace) { case 'dns': return new UuidV1(Uuid::NAMESPACE_DNS); case 'url': return new UuidV1(Uuid::NAMESPACE_URL); case 'oid': return new UuidV1(Uuid::NAMESPACE_OID); case 'x500': return new UuidV1(Uuid::NAMESPACE_X500); default: return Uuid::fromString($namespace); } } } uid/Factory/RandomBasedUuidFactory.php 0000644 00000001101 15025017654 0014001 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Factory; use Symfony\Component\Uid\UuidV4; class RandomBasedUuidFactory { private string $class; public function __construct(string $class) { $this->class = $class; } public function create(): UuidV4 { $class = $this->class; return new $class(); } } uid/Factory/TimeBasedUuidFactory.php 0000644 00000001560 15025017654 0013470 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Factory; use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\UuidV1; use Symfony\Component\Uid\UuidV6; class TimeBasedUuidFactory { private string $class; private $node; public function __construct(string $class, Uuid $node = null) { $this->class = $class; $this->node = $node; } public function create(\DateTimeInterface $time = null): UuidV6|UuidV1 { $class = $this->class; if (null === $time && null === $this->node) { return new $class(); } return new $class($class::generate($time, $this->node)); } } uid/Factory/UlidFactory.php 0000644 00000000745 15025017654 0011705 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Factory; use Symfony\Component\Uid\Ulid; class UlidFactory { public function create(\DateTimeInterface $time = null): Ulid { return new Ulid(null === $time ? null : Ulid::generate($time)); } } mailer/Mailer.php 0000644 00000005151 15025017654 0007746 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Psr\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Messenger\SendEmailMessage; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Messenger\Exception\HandlerFailedException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ final class Mailer implements MailerInterface { private $transport; private $bus; private $dispatcher; public function __construct(TransportInterface $transport, MessageBusInterface $bus = null, EventDispatcherInterface $dispatcher = null) { $this->transport = $transport; $this->bus = $bus; $this->dispatcher = $dispatcher; } public function send(RawMessage $message, Envelope $envelope = null): void { if (null === $this->bus) { $this->transport->send($message, $envelope); return; } if (null !== $this->dispatcher) { // The dispatched event here has `queued` set to `true`; the goal is NOT to render the message, but to let // listeners do something before a message is sent to the queue. // We are using a cloned message as we still want to dispatch the **original** message, not the one modified by listeners. // That's because the listeners will run again when the email is sent via Messenger by the transport (see `AbstractTransport`). // Listeners should act depending on the `$queued` argument of the `MessageEvent` instance. $clonedMessage = clone $message; $clonedEnvelope = null !== $envelope ? clone $envelope : Envelope::create($clonedMessage); $event = new MessageEvent($clonedMessage, $clonedEnvelope, (string) $this->transport, true); $this->dispatcher->dispatch($event); } try { $this->bus->dispatch(new SendEmailMessage($message, $envelope)); } catch (HandlerFailedException $e) { foreach ($e->getNestedExceptions() as $nested) { if ($nested instanceof TransportExceptionInterface) { throw $nested; } } throw $e; } } } mailer/Transport/AbstractApiTransport.php 0000644 00000003061 15025017654 0014641 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\RuntimeException; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; use Symfony\Component\Mime\MessageConverter; use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Fabien Potencier <fabien@symfony.com> */ abstract class AbstractApiTransport extends AbstractHttpTransport { abstract protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface; protected function doSendHttp(SentMessage $message): ResponseInterface { try { $email = MessageConverter::toEmail($message->getOriginalMessage()); } catch (\Exception $e) { throw new RuntimeException(sprintf('Unable to send message with the "%s" transport: ', __CLASS__).$e->getMessage(), 0, $e); } return $this->doSendApi($message, $email, $message->getEnvelope()); } protected function getRecipients(Email $email, Envelope $envelope): array { return array_filter($envelope->getRecipients(), function (Address $address) use ($email) { return false === \in_array($address, array_merge($email->getCc(), $email->getBcc()), true); }); } } mailer/Transport/AbstractTransportFactory.php 0000644 00000003137 15025017654 0015543 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Exception\IncompleteDsnException; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ abstract class AbstractTransportFactory implements TransportFactoryInterface { protected $dispatcher; protected $client; protected $logger; public function __construct(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null) { $this->dispatcher = $dispatcher; $this->client = $client; $this->logger = $logger; } public function supports(Dsn $dsn): bool { return \in_array($dsn->getScheme(), $this->getSupportedSchemes()); } abstract protected function getSupportedSchemes(): array; protected function getUser(Dsn $dsn): string { $user = $dsn->getUser(); if (null === $user) { throw new IncompleteDsnException('User is not set.'); } return $user; } protected function getPassword(Dsn $dsn): string { $password = $dsn->getPassword(); if (null === $password) { throw new IncompleteDsnException('Password is not set.'); } return $password; } } mailer/Transport/NullTransport.php 0000644 00000001216 15025017654 0013356 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\SentMessage; /** * Pretends messages have been sent, but just ignores them. * * @author Fabien Potencier <fabien@symfony.com> */ final class NullTransport extends AbstractTransport { protected function doSend(SentMessage $message): void { } public function __toString(): string { return 'null://'; } } mailer/Transport/NullTransportFactory.php 0000644 00000001535 15025017654 0014712 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ final class NullTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { if ('null' === $dsn->getScheme()) { return new NullTransport($this->dispatcher, $this->logger); } throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes()); } protected function getSupportedSchemes(): array { return ['null']; } } mailer/Transport/SendmailTransport.php 0000644 00000010260 15025017654 0014177 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; use Symfony\Component\Mailer\Transport\Smtp\Stream\ProcessStream; use Symfony\Component\Mime\RawMessage; /** * SendmailTransport for sending mail through a Sendmail/Postfix (etc..) binary. * * Transport can be instantiated through SendmailTransportFactory or NativeTransportFactory: * * - SendmailTransportFactory to use most common sendmail path and recommended options * - NativeTransportFactory when configuration is set via php.ini * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn */ class SendmailTransport extends AbstractTransport { private string $command = '/usr/sbin/sendmail -bs'; private $stream; private $transport = null; /** * Constructor. * * Supported modes are -bs and -t, with any additional flags desired. * * The recommended mode is "-bs" since it is interactive and failure notifications are hence possible. * Note that the -t mode does not support error reporting and does not support Bcc properly (the Bcc headers are not removed). * * If using -t mode, you are strongly advised to include -oi or -i in the flags (like /usr/sbin/sendmail -oi -t) * * -f<sender> flag will be appended automatically if one is not present. */ public function __construct(string $command = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { parent::__construct($dispatcher, $logger); if (null !== $command) { if (!str_contains($command, ' -bs') && !str_contains($command, ' -t')) { throw new \InvalidArgumentException(sprintf('Unsupported sendmail command flags "%s"; must be one of "-bs" or "-t" but can include additional flags.', $command)); } $this->command = $command; } $this->stream = new ProcessStream(); if (str_contains($this->command, ' -bs')) { $this->stream->setCommand($this->command); $this->transport = new SmtpTransport($this->stream, $dispatcher, $logger); } } public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { if ($this->transport) { return $this->transport->send($message, $envelope); } return parent::send($message, $envelope); } public function __toString(): string { if ($this->transport) { return (string) $this->transport; } return 'smtp://sendmail'; } protected function doSend(SentMessage $message): void { $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__)); $command = $this->command; if ($recipients = $message->getEnvelope()->getRecipients()) { $command = str_replace(' -t', '', $command); } if (!str_contains($command, ' -f')) { $command .= ' -f'.escapeshellarg($message->getEnvelope()->getSender()->getEncodedAddress()); } $chunks = AbstractStream::replace("\r\n", "\n", $message->toIterable()); if (!str_contains($command, ' -i') && !str_contains($command, ' -oi')) { $chunks = AbstractStream::replace("\n.", "\n..", $chunks); } foreach ($recipients as $recipient) { $command .= ' '.escapeshellarg($recipient->getEncodedAddress()); } $this->stream->setCommand($command); $this->stream->initialize(); foreach ($chunks as $chunk) { $this->stream->write($chunk); } $this->stream->flush(); $this->stream->terminate(); $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); } } mailer/Transport/Transports.php 0000644 00000004407 15025017654 0012713 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\Message; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ final class Transports implements TransportInterface { /** * @var array<string, TransportInterface> */ private array $transports = []; private $default; /** * @param iterable<string, TransportInterface> $transports */ public function __construct(iterable $transports) { foreach ($transports as $name => $transport) { $this->default ??= $transport; $this->transports[$name] = $transport; } if (!$this->transports) { throw new LogicException(sprintf('"%s" must have at least one transport configured.', __CLASS__)); } } public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { /** @var Message $message */ if (RawMessage::class === \get_class($message) || !$message->getHeaders()->has('X-Transport')) { return $this->default->send($message, $envelope); } $headers = $message->getHeaders(); $transport = $headers->get('X-Transport')->getBody(); $headers->remove('X-Transport'); if (!isset($this->transports[$transport])) { throw new InvalidArgumentException(sprintf('The "%s" transport does not exist (available transports: "%s").', $transport, implode('", "', array_keys($this->transports)))); } try { return $this->transports[$transport]->send($message, $envelope); } catch (\Throwable $e) { $headers->addTextHeader('X-Transport', $transport); throw $e; } } public function __toString(): string { return '['.implode(',', array_keys($this->transports)).']'; } } mailer/Transport/RoundRobinTransport.php 0000644 00000007157 15025017654 0014537 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\RawMessage; /** * Uses several Transports using a round robin algorithm. * * @author Fabien Potencier <fabien@symfony.com> */ class RoundRobinTransport implements TransportInterface { /** * @var \SplObjectStorage<TransportInterface, float> */ private \SplObjectStorage $deadTransports; private array $transports = []; private int $retryPeriod; private int $cursor = -1; /** * @param TransportInterface[] $transports */ public function __construct(array $transports, int $retryPeriod = 60) { if (!$transports) { throw new TransportException(sprintf('"%s" must have at least one transport configured.', static::class)); } $this->transports = $transports; $this->deadTransports = new \SplObjectStorage(); $this->retryPeriod = $retryPeriod; } public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { $exception = null; while ($transport = $this->getNextTransport()) { try { return $transport->send($message, $envelope); } catch (TransportExceptionInterface $e) { $exception ??= new TransportException('All transports failed.'); $exception->appendDebug(sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug())); $this->deadTransports[$transport] = microtime(true); } } throw $exception ?? new TransportException('No transports found.'); } public function __toString(): string { return $this->getNameSymbol().'('.implode(' ', array_map('strval', $this->transports)).')'; } /** * Rotates the transport list around and returns the first instance. */ protected function getNextTransport(): ?TransportInterface { if (-1 === $this->cursor) { $this->cursor = $this->getInitialCursor(); } $cursor = $this->cursor; while (true) { $transport = $this->transports[$cursor]; if (!$this->isTransportDead($transport)) { break; } if ((microtime(true) - $this->deadTransports[$transport]) > $this->retryPeriod) { $this->deadTransports->detach($transport); break; } if ($this->cursor === $cursor = $this->moveCursor($cursor)) { return null; } } $this->cursor = $this->moveCursor($cursor); return $transport; } protected function isTransportDead(TransportInterface $transport): bool { return $this->deadTransports->contains($transport); } protected function getInitialCursor(): int { // the cursor initial value is randomized so that // when are not in a daemon, we are still rotating the transports return mt_rand(0, \count($this->transports) - 1); } protected function getNameSymbol(): string { return 'roundrobin'; } private function moveCursor(int $cursor): int { return ++$cursor >= \count($this->transports) ? 0 : $cursor; } } mailer/Transport/AbstractTransport.php 0000644 00000005465 15025017654 0014221 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ abstract class AbstractTransport implements TransportInterface { private $dispatcher; private $logger; private float $rate = 0; private float $lastSent = 0; public function __construct(EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { $this->dispatcher = $dispatcher; $this->logger = $logger ?? new NullLogger(); } /** * Sets the maximum number of messages to send per second (0 to disable). * * @return $this */ public function setMaxPerSecond(float $rate): static { if (0 >= $rate) { $rate = 0; } $this->rate = $rate; $this->lastSent = 0; return $this; } public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { $message = clone $message; $envelope = null !== $envelope ? clone $envelope : Envelope::create($message); if (null !== $this->dispatcher) { $event = new MessageEvent($message, $envelope, (string) $this); $this->dispatcher->dispatch($event); $envelope = $event->getEnvelope(); $message = $event->getMessage(); } $message = new SentMessage($message, $envelope); $this->doSend($message); $this->checkThrottling(); return $message; } abstract protected function doSend(SentMessage $message): void; /** * @param Address[] $addresses * * @return string[] */ protected function stringifyAddresses(array $addresses): array { return array_map(function (Address $a) { return $a->toString(); }, $addresses); } protected function getLogger(): LoggerInterface { return $this->logger; } private function checkThrottling() { if (0 == $this->rate) { return; } $sleep = (1 / $this->rate) - (microtime(true) - $this->lastSent); if (0 < $sleep) { $this->logger->debug(sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep)); usleep($sleep * 1000000); } $this->lastSent = microtime(true); } } mailer/Transport/AbstractHttpTransport.php 0000644 00000004177 15025017654 0015060 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\Mailer\Exception\HttpTransportException; use Symfony\Component\Mailer\SentMessage; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Victor Bocharsky <victor@symfonycasts.com> */ abstract class AbstractHttpTransport extends AbstractTransport { protected $host; protected $port; protected $client; public function __construct(HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { $this->client = $client; if (null === $client) { if (!class_exists(HttpClient::class)) { throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__)); } $this->client = HttpClient::create(); } parent::__construct($dispatcher, $logger); } /** * @return $this */ public function setHost(?string $host): static { $this->host = $host; return $this; } /** * @return $this */ public function setPort(?int $port): static { $this->port = $port; return $this; } abstract protected function doSendHttp(SentMessage $message): ResponseInterface; protected function doSend(SentMessage $message): void { $response = null; try { $response = $this->doSendHttp($message); $message->appendDebug($response->getInfo('debug') ?? ''); } catch (HttpTransportException $e) { $e->appendDebug($e->getResponse()->getInfo('debug') ?? ''); throw $e; } } } mailer/Transport/Dsn.php 0000644 00000004662 15025017654 0011263 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Exception\InvalidArgumentException; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ final class Dsn { private string $scheme; private string $host; private ?string $user; private ?string $password; private ?int $port; private array $options; public function __construct(string $scheme, string $host, string $user = null, string $password = null, int $port = null, array $options = []) { $this->scheme = $scheme; $this->host = $host; $this->user = $user; $this->password = $password; $this->port = $port; $this->options = $options; } public static function fromString(string $dsn): self { if (false === $parsedDsn = parse_url($dsn)) { throw new InvalidArgumentException(sprintf('The "%s" mailer DSN is invalid.', $dsn)); } if (!isset($parsedDsn['scheme'])) { throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a scheme.', $dsn)); } if (!isset($parsedDsn['host'])) { throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a host (use "default" by default).', $dsn)); } $user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null; $password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null; $port = $parsedDsn['port'] ?? null; parse_str($parsedDsn['query'] ?? '', $query); return new self($parsedDsn['scheme'], $parsedDsn['host'], $user, $password, $port, $query); } public function getScheme(): string { return $this->scheme; } public function getHost(): string { return $this->host; } public function getUser(): ?string { return $this->user; } public function getPassword(): ?string { return $this->password; } public function getPort(int $default = null): ?int { return $this->port ?? $default; } public function getOption(string $key, mixed $default = null) { return $this->options[$key] ?? $default; } } mailer/Transport/NativeTransportFactory.php 0000644 00000004003 15025017654 0015217 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; /** * Factory that configures a transport (sendmail or SMTP) based on php.ini settings. * * @author Laurent VOULLEMIER <laurent.voullemier@gmail.com> */ final class NativeTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { if (!\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) { throw new UnsupportedSchemeException($dsn, 'native', $this->getSupportedSchemes()); } if ($sendMailPath = ini_get('sendmail_path')) { return new SendmailTransport($sendMailPath, $this->dispatcher, $this->logger); } if ('\\' !== \DIRECTORY_SEPARATOR) { throw new TransportException('sendmail_path is not configured in php.ini.'); } // Only for windows hosts; at this point non-windows // host have already thrown an exception or returned a transport $host = ini_get('SMTP'); $port = (int) ini_get('smtp_port'); if (!$host || !$port) { throw new TransportException('smtp or smtp_port is not configured in php.ini.'); } $socketStream = new SocketStream(); $socketStream->setHost($host); $socketStream->setPort($port); if (465 !== $port) { $socketStream->disableTls(); } return new SmtpTransport($socketStream, $this->dispatcher, $this->logger); } protected function getSupportedSchemes(): array { return ['native']; } } mailer/Transport/SendmailTransportFactory.php 0000644 00000001707 15025017654 0015535 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ final class SendmailTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { if ('sendmail+smtp' === $dsn->getScheme() || 'sendmail' === $dsn->getScheme()) { return new SendmailTransport($dsn->getOption('command'), $this->dispatcher, $this->logger); } throw new UnsupportedSchemeException($dsn, 'sendmail', $this->getSupportedSchemes()); } protected function getSupportedSchemes(): array { return ['sendmail', 'sendmail+smtp']; } } mailer/Transport/FailoverTransport.php 0000644 00000001667 15025017654 0014225 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; /** * Uses several Transports using a failover algorithm. * * @author Fabien Potencier <fabien@symfony.com> */ class FailoverTransport extends RoundRobinTransport { private $currentTransport = null; protected function getNextTransport(): ?TransportInterface { if (null === $this->currentTransport || $this->isTransportDead($this->currentTransport)) { $this->currentTransport = parent::getNextTransport(); } return $this->currentTransport; } protected function getInitialCursor(): int { return 0; } protected function getNameSymbol(): string { return 'failover'; } } mailer/Transport/TransportFactoryInterface.php 0000644 00000001317 15025017654 0015676 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Exception\IncompleteDsnException; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ interface TransportFactoryInterface { /** * @throws UnsupportedSchemeException * @throws IncompleteDsnException */ public function create(Dsn $dsn): TransportInterface; public function supports(Dsn $dsn): bool; } mailer/Transport/TransportInterface.php 0000644 00000001621 15025017654 0014344 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\RawMessage; /** * Interface for all mailer transports. * * When sending emails, you should prefer MailerInterface implementations * as they allow asynchronous sending. * * @author Fabien Potencier <fabien@symfony.com> */ interface TransportInterface { /** * @throws TransportExceptionInterface */ public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage; public function __toString(): string; } mailer/Transport/Smtp/EsmtpTransportFactory.php 0000644 00000004316 15025017654 0016013 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp; use Symfony\Component\Mailer\Transport\AbstractTransportFactory; use Symfony\Component\Mailer\Transport\Dsn; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; use Symfony\Component\Mailer\Transport\TransportInterface; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ final class EsmtpTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { $tls = 'smtps' === $dsn->getScheme() ? true : null; $port = $dsn->getPort(0); $host = $dsn->getHost(); $transport = new EsmtpTransport($host, $port, $tls, $this->dispatcher, $this->logger); if ('' !== $dsn->getOption('verify_peer') && !filter_var($dsn->getOption('verify_peer', true), \FILTER_VALIDATE_BOOLEAN)) { /** @var SocketStream $stream */ $stream = $transport->getStream(); $streamOptions = $stream->getStreamOptions(); $streamOptions['ssl']['verify_peer'] = false; $streamOptions['ssl']['verify_peer_name'] = false; $stream->setStreamOptions($streamOptions); } if ($user = $dsn->getUser()) { $transport->setUsername($user); } if ($password = $dsn->getPassword()) { $transport->setPassword($password); } if (null !== ($localDomain = $dsn->getOption('local_domain'))) { $transport->setLocalDomain($localDomain); } if (null !== ($restartThreshold = $dsn->getOption('restart_threshold'))) { $transport->setRestartThreshold((int) $restartThreshold, (int) $dsn->getOption('restart_threshold_sleep', 0)); } if (null !== ($pingThreshold = $dsn->getOption('ping_threshold'))) { $transport->setPingThreshold((int) $pingThreshold); } return $transport; } protected function getSupportedSchemes(): array { return ['smtp', 'smtps']; } } mailer/Transport/Smtp/Stream/SocketStream.php 0000644 00000010765 15025017654 0015322 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Stream; use Symfony\Component\Mailer\Exception\TransportException; /** * A stream supporting remote sockets. * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn * * @internal */ final class SocketStream extends AbstractStream { private string $url; private string $host = 'localhost'; private int $port = 465; private float $timeout; private bool $tls = true; private ?string $sourceIp = null; private array $streamContextOptions = []; /** * @return $this */ public function setTimeout(float $timeout): static { $this->timeout = $timeout; return $this; } public function getTimeout(): float { return $this->timeout ?? (float) \ini_get('default_socket_timeout'); } /** * Literal IPv6 addresses should be wrapped in square brackets. * * @return $this */ public function setHost(string $host): static { $this->host = $host; return $this; } public function getHost(): string { return $this->host; } /** * @return $this */ public function setPort(int $port): static { $this->port = $port; return $this; } public function getPort(): int { return $this->port; } /** * Sets the TLS/SSL on the socket (disables STARTTLS). * * @return $this */ public function disableTls(): static { $this->tls = false; return $this; } public function isTLS(): bool { return $this->tls; } /** * @return $this */ public function setStreamOptions(array $options): static { $this->streamContextOptions = $options; return $this; } public function getStreamOptions(): array { return $this->streamContextOptions; } /** * Sets the source IP. * * IPv6 addresses should be wrapped in square brackets. * * @return $this */ public function setSourceIp(string $ip): static { $this->sourceIp = $ip; return $this; } /** * Returns the IP used to connect to the destination. */ public function getSourceIp(): ?string { return $this->sourceIp; } public function initialize(): void { $this->url = $this->host.':'.$this->port; if ($this->tls) { $this->url = 'ssl://'.$this->url; } $options = []; if ($this->sourceIp) { $options['socket']['bindto'] = $this->sourceIp.':0'; } if ($this->streamContextOptions) { $options = array_merge($options, $this->streamContextOptions); } // do it unconditionally as it will be used by STARTTLS as well if supported $options['ssl']['crypto_method'] = $options['ssl']['crypto_method'] ?? \STREAM_CRYPTO_METHOD_TLS_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; $streamContext = stream_context_create($options); $timeout = $this->getTimeout(); set_error_handler(function ($type, $msg) { throw new TransportException(sprintf('Connection could not be established with host "%s": ', $this->url).$msg); }); try { $this->stream = stream_socket_client($this->url, $errno, $errstr, $timeout, \STREAM_CLIENT_CONNECT, $streamContext); } finally { restore_error_handler(); } stream_set_blocking($this->stream, true); stream_set_timeout($this->stream, $timeout); $this->in = &$this->stream; $this->out = &$this->stream; } public function startTLS(): bool { set_error_handler(function ($type, $msg) { throw new TransportException('Unable to connect with STARTTLS: '.$msg); }); try { return stream_socket_enable_crypto($this->stream, true); } finally { restore_error_handler(); } } public function terminate(): void { if (null !== $this->stream) { fclose($this->stream); } parent::terminate(); } protected function getReadConnectionDescription(): string { return $this->url; } } mailer/Transport/Smtp/Stream/AbstractStream.php 0000644 00000006760 15025017654 0015635 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Stream; use Symfony\Component\Mailer\Exception\TransportException; /** * A stream supporting remote sockets and local processes. * * @author Fabien Potencier <fabien@symfony.com> * @author Nicolas Grekas <p@tchwork.com> * @author Chris Corbyn * * @internal */ abstract class AbstractStream { protected $stream; protected $in; protected $out; private string $debug = ''; public function write(string $bytes, bool $debug = true): void { if ($debug) { foreach (explode("\n", trim($bytes)) as $line) { $this->debug .= sprintf("> %s\n", $line); } } $bytesToWrite = \strlen($bytes); $totalBytesWritten = 0; while ($totalBytesWritten < $bytesToWrite) { $bytesWritten = @fwrite($this->in, substr($bytes, $totalBytesWritten)); if (false === $bytesWritten || 0 === $bytesWritten) { throw new TransportException('Unable to write bytes on the wire.'); } $totalBytesWritten += $bytesWritten; } } /** * Flushes the contents of the stream (empty it) and set the internal pointer to the beginning. */ public function flush(): void { fflush($this->in); } /** * Performs any initialization needed. */ abstract public function initialize(): void; public function terminate(): void { $this->stream = $this->out = $this->in = null; } public function readLine(): string { if (feof($this->out)) { return ''; } $line = fgets($this->out); if ('' === $line || false === $line) { $metas = stream_get_meta_data($this->out); if ($metas['timed_out']) { throw new TransportException(sprintf('Connection to "%s" timed out.', $this->getReadConnectionDescription())); } if ($metas['eof']) { throw new TransportException(sprintf('Connection to "%s" has been closed unexpectedly.', $this->getReadConnectionDescription())); } } $this->debug .= sprintf('< %s', $line); return $line; } public function getDebug(): string { $debug = $this->debug; $this->debug = ''; return $debug; } public static function replace(string $from, string $to, iterable $chunks): \Generator { if ('' === $from) { yield from $chunks; return; } $carry = ''; $fromLen = \strlen($from); foreach ($chunks as $chunk) { if ('' === $chunk = $carry.$chunk) { continue; } if (str_contains($chunk, $from)) { $chunk = explode($from, $chunk); $carry = array_pop($chunk); yield implode($to, $chunk).$to; } else { $carry = $chunk; } if (\strlen($carry) > $fromLen) { yield substr($carry, 0, -$fromLen); $carry = substr($carry, -$fromLen); } } if ('' !== $carry) { yield $carry; } } abstract protected function getReadConnectionDescription(): string; } mailer/Transport/Smtp/Stream/ProcessStream.php 0000644 00000003033 15025017654 0015476 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Stream; use Symfony\Component\Mailer\Exception\TransportException; /** * A stream supporting local processes. * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn * * @internal */ final class ProcessStream extends AbstractStream { private string $command; public function setCommand(string $command) { $this->command = $command; } public function initialize(): void { $descriptorSpec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $pipes = []; $this->stream = proc_open($this->command, $descriptorSpec, $pipes); stream_set_blocking($pipes[2], false); if ($err = stream_get_contents($pipes[2])) { throw new TransportException('Process could not be started: '.$err); } $this->in = &$pipes[0]; $this->out = &$pipes[1]; } public function terminate(): void { if (null !== $this->stream) { fclose($this->in); fclose($this->out); proc_close($this->stream); } parent::terminate(); } protected function getReadConnectionDescription(): string { return 'process '.$this->command; } } mailer/Transport/Smtp/EsmtpTransport.php 0000644 00000014127 15025017654 0014464 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; /** * Sends Emails over SMTP with ESMTP support. * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn */ class EsmtpTransport extends SmtpTransport { private array $authenticators = []; private string $username = ''; private string $password = ''; public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { parent::__construct(null, $dispatcher, $logger); // order is important here (roughly most secure and popular first) $this->authenticators = [ new Auth\CramMd5Authenticator(), new Auth\LoginAuthenticator(), new Auth\PlainAuthenticator(), new Auth\XOAuth2Authenticator(), ]; /** @var SocketStream $stream */ $stream = $this->getStream(); if (null === $tls) { if (465 === $port) { $tls = true; } else { $tls = \defined('OPENSSL_VERSION_NUMBER') && 0 === $port && 'localhost' !== $host; } } if (!$tls) { $stream->disableTls(); } if (0 === $port) { $port = $tls ? 465 : 25; } $stream->setHost($host); $stream->setPort($port); } /** * @return $this */ public function setUsername(string $username): static { $this->username = $username; return $this; } public function getUsername(): string { return $this->username; } /** * @return $this */ public function setPassword(string $password): static { $this->password = $password; return $this; } public function getPassword(): string { return $this->password; } public function addAuthenticator(AuthenticatorInterface $authenticator): void { $this->authenticators[] = $authenticator; } protected function doHeloCommand(): void { if (!$capabilities = $this->callHeloCommand()) { return; } /** @var SocketStream $stream */ $stream = $this->getStream(); // WARNING: !$stream->isTLS() is right, 100% sure :) // if you think that the ! should be removed, read the code again // if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $capabilities)) { $this->executeCommand("STARTTLS\r\n", [220]); if (!$stream->startTLS()) { throw new TransportException('Unable to connect with STARTTLS.'); } $capabilities = $this->callHeloCommand(); } if (\array_key_exists('AUTH', $capabilities)) { $this->handleAuth($capabilities['AUTH']); } } private function callHeloCommand(): array { try { $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); } catch (TransportExceptionInterface $e) { try { parent::doHeloCommand(); return []; } catch (TransportExceptionInterface $ex) { if (!$ex->getCode()) { throw $e; } throw $ex; } } $capabilities = []; $lines = explode("\r\n", trim($response)); array_shift($lines); foreach ($lines as $line) { if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) { $value = strtoupper(ltrim($matches[2], ' =')); $capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : []; } } return $capabilities; } private function handleAuth(array $modes): void { if (!$this->username) { return; } $authNames = []; $errors = []; $modes = array_map('strtolower', $modes); foreach ($this->authenticators as $authenticator) { if (!\in_array(strtolower($authenticator->getAuthKeyword()), $modes, true)) { continue; } $authNames[] = $authenticator->getAuthKeyword(); try { $authenticator->authenticate($this); return; } catch (TransportExceptionInterface $e) { try { $this->executeCommand("RSET\r\n", [250]); } catch (TransportExceptionInterface $_) { // ignore this exception as it probably means that the server error was final } // keep the error message, but tries the other authenticators $errors[$authenticator->getAuthKeyword()] = $e->getMessage(); } } if (!$authNames) { throw new TransportException(sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes))); } $message = sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames)); foreach ($errors as $name => $error) { $message .= sprintf(' Authenticator "%s" returned "%s".', $name, $error); } throw new TransportException($message); } } mailer/Transport/Smtp/SmtpTransport.php 0000644 00000025661 15025017654 0014324 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\AbstractTransport; use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; use Symfony\Component\Mime\RawMessage; /** * Sends emails over SMTP. * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn */ class SmtpTransport extends AbstractTransport { private bool $started = false; private int $restartThreshold = 100; private int $restartThresholdSleep = 0; private int $restartCounter = 0; private int $pingThreshold = 100; private float $lastMessageTime = 0; private $stream; private string $domain = '[127.0.0.1]'; public function __construct(AbstractStream $stream = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { parent::__construct($dispatcher, $logger); $this->stream = $stream ?? new SocketStream(); } public function getStream(): AbstractStream { return $this->stream; } /** * Sets the maximum number of messages to send before re-starting the transport. * * By default, the threshold is set to 100 (and no sleep at restart). * * @param int $threshold The maximum number of messages (0 to disable) * @param int $sleep The number of seconds to sleep between stopping and re-starting the transport * * @return $this */ public function setRestartThreshold(int $threshold, int $sleep = 0): static { $this->restartThreshold = $threshold; $this->restartThresholdSleep = $sleep; return $this; } /** * Sets the minimum number of seconds required between two messages, before the server is pinged. * If the transport wants to send a message and the time since the last message exceeds the specified threshold, * the transport will ping the server first (NOOP command) to check if the connection is still alive. * Otherwise the message will be sent without pinging the server first. * * Do not set the threshold too low, as the SMTP server may drop the connection if there are too many * non-mail commands (like pinging the server with NOOP). * * By default, the threshold is set to 100 seconds. * * @param int $seconds The minimum number of seconds between two messages required to ping the server * * @return $this */ public function setPingThreshold(int $seconds): static { $this->pingThreshold = $seconds; return $this; } /** * Sets the name of the local domain that will be used in HELO. * * This should be a fully-qualified domain name and should be truly the domain * you're using. * * If your server does not have a domain name, use the IP address. This will * automatically be wrapped in square brackets as described in RFC 5321, * section 4.1.3. * * @return $this */ public function setLocalDomain(string $domain): static { if ('' !== $domain && '[' !== $domain[0]) { if (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { $domain = '['.$domain.']'; } elseif (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { $domain = '[IPv6:'.$domain.']'; } } $this->domain = $domain; return $this; } /** * Gets the name of the domain that will be used in HELO. * * If an IP address was specified, this will be returned wrapped in square * brackets as described in RFC 5321, section 4.1.3. */ public function getLocalDomain(): string { return $this->domain; } public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { try { $message = parent::send($message, $envelope); } catch (TransportExceptionInterface $e) { if ($this->started) { try { $this->executeCommand("RSET\r\n", [250]); } catch (TransportExceptionInterface $_) { // ignore this exception as it probably means that the server error was final } } throw $e; } $this->checkRestartThreshold(); return $message; } public function __toString(): string { if ($this->stream instanceof SocketStream) { $name = sprintf('smtp%s://%s', ($tls = $this->stream->isTLS()) ? 's' : '', $this->stream->getHost()); $port = $this->stream->getPort(); if (!(25 === $port || ($tls && 465 === $port))) { $name .= ':'.$port; } return $name; } return 'smtp://sendmail'; } /** * Runs a command against the stream, expecting the given response codes. * * @param int[] $codes * * @throws TransportException when an invalid response if received * * @internal */ public function executeCommand(string $command, array $codes): string { $this->stream->write($command); $response = $this->getFullResponse(); $this->assertResponseCode($response, $codes); return $response; } protected function doSend(SentMessage $message): void { if (microtime(true) - $this->lastMessageTime > $this->pingThreshold) { $this->ping(); } if (!$this->started) { $this->start(); } try { $envelope = $message->getEnvelope(); $this->doMailFromCommand($envelope->getSender()->getEncodedAddress()); foreach ($envelope->getRecipients() as $recipient) { $this->doRcptToCommand($recipient->getEncodedAddress()); } $this->executeCommand("DATA\r\n", [354]); try { foreach (AbstractStream::replace("\r\n.", "\r\n..", $message->toIterable()) as $chunk) { $this->stream->write($chunk, false); } $this->stream->flush(); } catch (TransportExceptionInterface $e) { throw $e; } catch (\Exception $e) { $this->stream->terminate(); $this->started = false; $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); throw $e; } $this->executeCommand("\r\n.\r\n", [250]); $message->appendDebug($this->stream->getDebug()); $this->lastMessageTime = microtime(true); } catch (TransportExceptionInterface $e) { $e->appendDebug($this->stream->getDebug()); $this->lastMessageTime = 0; throw $e; } } protected function doHeloCommand(): void { $this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]); } private function doMailFromCommand(string $address): void { $this->executeCommand(sprintf("MAIL FROM:<%s>\r\n", $address), [250]); } private function doRcptToCommand(string $address): void { $this->executeCommand(sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]); } private function start(): void { if ($this->started) { return; } $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__)); $this->stream->initialize(); $this->assertResponseCode($this->getFullResponse(), [220]); $this->doHeloCommand(); $this->started = true; $this->lastMessageTime = 0; $this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__)); } private function stop(): void { if (!$this->started) { return; } $this->getLogger()->debug(sprintf('Email transport "%s" stopping', __CLASS__)); try { $this->executeCommand("QUIT\r\n", [221]); } catch (TransportExceptionInterface $e) { } finally { $this->stream->terminate(); $this->started = false; $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); } } private function ping(): void { if (!$this->started) { return; } try { $this->executeCommand("NOOP\r\n", [250]); } catch (TransportExceptionInterface $e) { $this->stop(); } } /** * @throws TransportException if a response code is incorrect */ private function assertResponseCode(string $response, array $codes): void { if (!$codes) { throw new LogicException('You must set the expected response code.'); } [$code] = sscanf($response, '%3d'); $valid = \in_array($code, $codes); if (!$valid || !$response) { $codeStr = $code ? sprintf('code "%s"', $code) : 'empty code'; $responseStr = $response ? sprintf(', with message "%s"', trim($response)) : ''; throw new TransportException(sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0); } } private function getFullResponse(): string { $response = ''; do { $line = $this->stream->readLine(); $response .= $line; } while ($line && isset($line[3]) && ' ' !== $line[3]); return $response; } private function checkRestartThreshold(): void { // when using sendmail via non-interactive mode, the transport is never "started" if (!$this->started) { return; } ++$this->restartCounter; if ($this->restartCounter < $this->restartThreshold) { return; } $this->stop(); if (0 < $sleep = $this->restartThresholdSleep) { $this->getLogger()->debug(sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep)); sleep($sleep); } $this->start(); $this->restartCounter = 0; } public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } public function __wakeup() { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } public function __destruct() { $this->stop(); } } mailer/Transport/Smtp/Auth/LoginAuthenticator.php 0000644 00000001737 15025017654 0016166 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles LOGIN authentication. * * @author Chris Corbyn */ class LoginAuthenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'LOGIN'; } /** * {@inheritdoc} * * @see https://www.ietf.org/rfc/rfc4954.txt */ public function authenticate(EsmtpTransport $client): void { $client->executeCommand("AUTH LOGIN\r\n", [334]); $client->executeCommand(sprintf("%s\r\n", base64_encode($client->getUsername())), [334]); $client->executeCommand(sprintf("%s\r\n", base64_encode($client->getPassword())), [235]); } } mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php 0000644 00000002041 15025017654 0016335 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles XOAUTH2 authentication. * * @author xu.li<AthenaLightenedMyPath@gmail.com> * * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol */ class XOAuth2Authenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'XOAUTH2'; } /** * {@inheritdoc} * * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism */ public function authenticate(EsmtpTransport $client): void { $client->executeCommand('AUTH XOAUTH2 '.base64_encode('user='.$client->getUsername()."\1auth=Bearer ".$client->getPassword()."\1\1")."\r\n", [235]); } } mailer/Transport/Smtp/Auth/CramMd5Authenticator.php 0000644 00000003227 15025017654 0016342 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles CRAM-MD5 authentication. * * @author Chris Corbyn */ class CramMd5Authenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'CRAM-MD5'; } /** * {@inheritdoc} * * @see https://www.ietf.org/rfc/rfc4954.txt */ public function authenticate(EsmtpTransport $client): void { $challenge = $client->executeCommand("AUTH CRAM-MD5\r\n", [334]); $challenge = base64_decode(substr($challenge, 4)); $message = base64_encode($client->getUsername().' '.$this->getResponse($client->getPassword(), $challenge)); $client->executeCommand(sprintf("%s\r\n", $message), [235]); } /** * Generates a CRAM-MD5 response from a server challenge. */ private function getResponse(string $secret, string $challenge): string { if (\strlen($secret) > 64) { $secret = pack('H32', md5($secret)); } if (\strlen($secret) < 64) { $secret = str_pad($secret, 64, \chr(0)); } $kipad = substr($secret, 0, 64) ^ str_repeat(\chr(0x36), 64); $kopad = substr($secret, 0, 64) ^ str_repeat(\chr(0x5C), 64); $inner = pack('H32', md5($kipad.$challenge)); $digest = md5($kopad.$inner); return $digest; } } mailer/Transport/Smtp/Auth/PlainAuthenticator.php 0000644 00000001614 15025017654 0016153 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles PLAIN authentication. * * @author Chris Corbyn */ class PlainAuthenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'PLAIN'; } /** * {@inheritdoc} * * @see https://www.ietf.org/rfc/rfc4954.txt */ public function authenticate(EsmtpTransport $client): void { $client->executeCommand(sprintf("AUTH PLAIN %s\r\n", base64_encode($client->getUsername().\chr(0).$client->getUsername().\chr(0).$client->getPassword())), [235]); } } mailer/Transport/Smtp/Auth/AuthenticatorInterface.php 0000644 00000001477 15025017654 0017017 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Auth; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * An Authentication mechanism. * * @author Chris Corbyn */ interface AuthenticatorInterface { /** * Tries to authenticate the user. * * @throws TransportExceptionInterface */ public function authenticate(EsmtpTransport $client): void; /** * Gets the name of the AUTH mechanism this Authenticator handles. */ public function getAuthKeyword(): string; } mailer/DataCollector/MessageDataCollector.php 0000644 00000002672 15025017654 0015307 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\Mailer\Event\MessageEvents; use Symfony\Component\Mailer\EventListener\MessageLoggerListener; /** * @author Fabien Potencier <fabien@symfony.com> */ final class MessageDataCollector extends DataCollector { private $events; public function __construct(MessageLoggerListener $logger) { $this->events = $logger->getEvents(); } /** * {@inheritdoc} */ public function collect(Request $request, Response $response, \Throwable $exception = null) { $this->data['events'] = $this->events; } public function getEvents(): MessageEvents { return $this->data['events']; } /** * @internal */ public function base64Encode(string $data): string { return base64_encode($data); } /** * {@inheritdoc} */ public function reset() { $this->data = []; } /** * {@inheritdoc} */ public function getName(): string { return 'mailer'; } } mailer/EventListener/EnvelopeListener.php 0000644 00000003653 15025017654 0014614 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Message; /** * Manipulates the Envelope of a Message. * * @author Fabien Potencier <fabien@symfony.com> */ class EnvelopeListener implements EventSubscriberInterface { private $sender = null; /** * @var Address[]|null */ private ?array $recipients = null; /** * @param array<Address|string> $recipients */ public function __construct(Address|string $sender = null, array $recipients = null) { if (null !== $sender) { $this->sender = Address::create($sender); } if (null !== $recipients) { $this->recipients = Address::createArray($recipients); } } public function onMessage(MessageEvent $event): void { if ($this->sender) { $event->getEnvelope()->setSender($this->sender); $message = $event->getMessage(); if ($message instanceof Message) { if (!$message->getHeaders()->has('Sender') && !$message->getHeaders()->has('From')) { $message->getHeaders()->addMailboxHeader('Sender', $this->sender); } } } if ($this->recipients) { $event->getEnvelope()->setRecipients($this->recipients); } } public static function getSubscribedEvents(): array { return [ // should be the last one to allow header changes by other listeners first MessageEvent::class => ['onMessage', -255], ]; } } mailer/EventListener/MessageListener.php 0000644 00000007514 15025017654 0014423 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\RuntimeException; use Symfony\Component\Mime\BodyRendererInterface; use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\Header\MailboxListHeader; use Symfony\Component\Mime\Message; /** * Manipulates the headers and the body of a Message. * * @author Fabien Potencier <fabien@symfony.com> */ class MessageListener implements EventSubscriberInterface { public const HEADER_SET_IF_EMPTY = 1; public const HEADER_ADD = 2; public const HEADER_REPLACE = 3; public const DEFAULT_RULES = [ 'from' => self::HEADER_SET_IF_EMPTY, 'return-path' => self::HEADER_SET_IF_EMPTY, 'reply-to' => self::HEADER_ADD, 'to' => self::HEADER_SET_IF_EMPTY, 'cc' => self::HEADER_ADD, 'bcc' => self::HEADER_ADD, ]; private $headers; private array $headerRules = []; private $renderer; public function __construct(Headers $headers = null, BodyRendererInterface $renderer = null, array $headerRules = self::DEFAULT_RULES) { $this->headers = $headers; $this->renderer = $renderer; foreach ($headerRules as $headerName => $rule) { $this->addHeaderRule($headerName, $rule); } } public function addHeaderRule(string $headerName, int $rule): void { if ($rule < 1 || $rule > 3) { throw new InvalidArgumentException(sprintf('The "%d" rule is not supported.', $rule)); } $this->headerRules[strtolower($headerName)] = $rule; } public function onMessage(MessageEvent $event): void { $message = $event->getMessage(); if (!$message instanceof Message) { return; } $this->setHeaders($message); $this->renderMessage($message); } private function setHeaders(Message $message): void { if (!$this->headers) { return; } $headers = $message->getHeaders(); foreach ($this->headers->all() as $name => $header) { if (!$headers->has($name)) { $headers->add($header); continue; } switch ($this->headerRules[$name] ?? self::HEADER_SET_IF_EMPTY) { case self::HEADER_SET_IF_EMPTY: break; case self::HEADER_REPLACE: $headers->remove($name); $headers->add($header); break; case self::HEADER_ADD: if (!Headers::isUniqueHeader($name)) { $headers->add($header); break; } $h = $headers->get($name); if (!$h instanceof MailboxListHeader) { throw new RuntimeException(sprintf('Unable to set header "%s".', $name)); } Headers::checkHeaderClass($header); foreach ($header->getAddresses() as $address) { $h->addAddress($address); } } } } private function renderMessage(Message $message): void { if (!$this->renderer) { return; } $this->renderer->render($message); } public static function getSubscribedEvents(): array { return [ MessageEvent::class => 'onMessage', ]; } } mailer/EventListener/MessageLoggerListener.php 0000644 00000002336 15025017654 0015560 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\Event\MessageEvents; use Symfony\Contracts\Service\ResetInterface; /** * Logs Messages. * * @author Fabien Potencier <fabien@symfony.com> */ class MessageLoggerListener implements EventSubscriberInterface, ResetInterface { private $events; public function __construct() { $this->events = new MessageEvents(); } /** * {@inheritdoc} */ public function reset() { $this->events = new MessageEvents(); } public function onMessage(MessageEvent $event): void { $this->events->add($event); } public function getEvents(): MessageEvents { return $this->events; } public static function getSubscribedEvents(): array { return [ MessageEvent::class => ['onMessage', -255], ]; } } mailer/composer.json 0000644 00000002116 15025017654 0010544 0 ustar 00 { "name": "symfony/mailer", "type": "library", "description": "Helps sending emails", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.0.2", "egulias/email-validator": "^2.1.10|^3|^4", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", "symfony/event-dispatcher": "^5.4|^6.0", "symfony/mime": "^5.4|^6.0", "symfony/service-contracts": "^1.1|^2|^3" }, "require-dev": { "symfony/http-client-contracts": "^1.1|^2|^3", "symfony/messenger": "^5.4|^6.0" }, "conflict": { "symfony/http-kernel": "<5.4" }, "autoload": { "psr-4": { "Symfony\\Component\\Mailer\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } mailer/CHANGELOG.md 0000644 00000004633 15025017654 0007641 0 ustar 00 CHANGELOG ========= 6.0 --- * The `HttpTransportException` class takes a string at first argument 5.4 --- * Enable the mailer to operate on any PSR-14-compatible event dispatcher 5.3 --- * added the `mailer` monolog channel and set it on all transport definitions 5.2.0 ----- * added `NativeTransportFactory` to configure a transport based on php.ini settings * added `local_domain`, `restart_threshold`, `restart_threshold_sleep` and `ping_threshold` options for `smtp` * added `command` option for `sendmail` 4.4.0 ----- * [BC BREAK] changed the `NullTransport` DSN from `smtp://null` to `null://null` * [BC BREAK] renamed `SmtpEnvelope` to `Envelope`, renamed `DelayedSmtpEnvelope` to `DelayedEnvelope` * [BC BREAK] changed the syntax for failover and roundrobin DSNs Before: dummy://a || dummy://b (for failover) dummy://a && dummy://b (for roundrobin) After: failover(dummy://a dummy://b) roundrobin(dummy://a dummy://b) * added support for multiple transports on a `Mailer` instance * [BC BREAK] removed the `auth_mode` DSN option (it is now always determined automatically) * STARTTLS cannot be enabled anymore (it is used automatically if TLS is disabled and the server supports STARTTLS) * [BC BREAK] Removed the `encryption` DSN option (use `smtps` instead) * Added support for the `smtps` protocol (does the same as using `smtp` and port `465`) * Added PHPUnit constraints * Added `MessageDataCollector` * Added `MessageEvents` and `MessageLoggerListener` to allow collecting sent emails * [BC BREAK] `TransportInterface` has a new `__toString()` method * [BC BREAK] Classes `AbstractApiTransport` and `AbstractHttpTransport` moved under `Transport` sub-namespace. * [BC BREAK] Transports depend on `Symfony\Contracts\EventDispatcher\EventDispatcherInterface` instead of `Symfony\Component\EventDispatcher\EventDispatcherInterface`. * Added possibility to register custom transport for dsn by implementing `Symfony\Component\Mailer\Transport\TransportFactoryInterface` and tagging with `mailer.transport_factory` tag in DI. * Added `Symfony\Component\Mailer\Test\TransportFactoryTestCase` to ease testing custom transport factories. * Added `SentMessage::getDebug()` and `TransportExceptionInterface::getDebug` to help debugging * Made `MessageEvent` final * add DSN parameter `verify_peer` to disable TLS peer verification for SMTP transport 4.3.0 ----- * Added the component. mailer/Exception/TransportException.php 0000644 00000001206 15025017654 0014343 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Fabien Potencier <fabien@symfony.com> */ class TransportException extends RuntimeException implements TransportExceptionInterface { private string $debug = ''; public function getDebug(): string { return $this->debug; } public function appendDebug(string $debug): void { $this->debug .= $debug; } } mailer/Exception/LogicException.php 0000644 00000000645 15025017654 0013412 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Fabien Potencier <fabien@symfony.com> */ class LogicException extends \LogicException implements ExceptionInterface { } mailer/Exception/UnsupportedSchemeException.php 0000644 00000005550 15025017654 0016032 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; use Symfony\Component\Mailer\Bridge; use Symfony\Component\Mailer\Transport\Dsn; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ class UnsupportedSchemeException extends LogicException { private const SCHEME_TO_PACKAGE_MAP = [ 'gmail' => [ 'class' => Bridge\Google\Transport\GmailTransportFactory::class, 'package' => 'symfony/google-mailer', ], 'mailgun' => [ 'class' => Bridge\Mailgun\Transport\MailgunTransportFactory::class, 'package' => 'symfony/mailgun-mailer', ], 'mailjet' => [ 'class' => Bridge\Mailjet\Transport\MailjetTransportFactory::class, 'package' => 'symfony/mailjet-mailer', ], 'mandrill' => [ 'class' => Bridge\Mailchimp\Transport\MandrillTransportFactory::class, 'package' => 'symfony/mailchimp-mailer', ], 'ohmysmtp' => [ 'class' => Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory::class, 'package' => 'symfony/oh-my-smtp-mailer', ], 'postmark' => [ 'class' => Bridge\Postmark\Transport\PostmarkTransportFactory::class, 'package' => 'symfony/postmark-mailer', ], 'sendgrid' => [ 'class' => Bridge\Sendgrid\Transport\SendgridTransportFactory::class, 'package' => 'symfony/sendgrid-mailer', ], 'sendinblue' => [ 'class' => Bridge\Sendinblue\Transport\SendinblueTransportFactory::class, 'package' => 'symfony/sendinblue-mailer', ], 'ses' => [ 'class' => Bridge\Amazon\Transport\SesTransportFactory::class, 'package' => 'symfony/amazon-mailer', ], ]; public function __construct(Dsn $dsn, string $name = null, array $supported = []) { $provider = $dsn->getScheme(); if (false !== $pos = strpos($provider, '+')) { $provider = substr($provider, 0, $pos); } $package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null; if ($package && !class_exists($package['class'])) { parent::__construct(sprintf('Unable to send emails via "%s" as the bridge is not installed; try running "composer require %s".', $provider, $package['package'])); return; } $message = sprintf('The "%s" scheme is not supported', $dsn->getScheme()); if ($name && $supported) { $message .= sprintf('; supported schemes for mailer "%s" are: "%s"', $name, implode('", "', $supported)); } parent::__construct($message.'.'); } } mailer/Exception/TransportExceptionInterface.php 0000644 00000000772 15025017654 0016173 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Fabien Potencier <fabien@symfony.com> */ interface TransportExceptionInterface extends ExceptionInterface { public function getDebug(): string; public function appendDebug(string $debug): void; } mailer/Exception/InvalidArgumentException.php 0000644 00000000671 15025017654 0015445 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Fabien Potencier <fabien@symfony.com> */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } mailer/Exception/ExceptionInterface.php 0000644 00000000720 15025017654 0014247 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * Exception interface for all exceptions thrown by the component. * * @author Fabien Potencier <fabien@symfony.com> */ interface ExceptionInterface extends \Throwable { } mailer/Exception/IncompleteDsnException.php 0000644 00000000635 15025017654 0015120 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ class IncompleteDsnException extends InvalidArgumentException { } mailer/Exception/HttpTransportException.php 0000644 00000001446 15025017654 0015211 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Fabien Potencier <fabien@symfony.com> */ class HttpTransportException extends TransportException { private $response; public function __construct(string $message, ResponseInterface $response, int $code = 0, \Throwable $previous = null) { parent::__construct($message, $code, $previous); $this->response = $response; } public function getResponse(): ResponseInterface { return $this->response; } } mailer/Exception/RuntimeException.php 0000644 00000000651 15025017654 0013775 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Fabien Potencier <fabien@symfony.com> */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } mailer/Messenger/SendEmailMessage.php 0000644 00000001505 15025017654 0013632 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Messenger; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ class SendEmailMessage { private $message; private $envelope; public function __construct(RawMessage $message, Envelope $envelope = null) { $this->message = $message; $this->envelope = $envelope; } public function getMessage(): RawMessage { return $this->message; } public function getEnvelope(): ?Envelope { return $this->envelope; } } mailer/Messenger/MessageHandler.php 0000644 00000001413 15025017654 0013344 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Messenger; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\TransportInterface; /** * @author Fabien Potencier <fabien@symfony.com> */ class MessageHandler { private $transport; public function __construct(TransportInterface $transport) { $this->transport = $transport; } public function __invoke(SendEmailMessage $message): ?SentMessage { return $this->transport->send($message->getMessage(), $message->getEnvelope()); } } mailer/DelayedEnvelope.php 0000644 00000004626 15025017654 0011610 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\Message; /** * @author Fabien Potencier <fabien@symfony.com> * * @internal */ final class DelayedEnvelope extends Envelope { private bool $senderSet = false; private bool $recipientsSet = false; private $message; public function __construct(Message $message) { $this->message = $message; } public function setSender(Address $sender): void { parent::setSender($sender); $this->senderSet = true; } public function getSender(): Address { if (!$this->senderSet) { parent::setSender(self::getSenderFromHeaders($this->message->getHeaders())); } return parent::getSender(); } public function setRecipients(array $recipients): void { parent::setRecipients($recipients); $this->recipientsSet = (bool) parent::getRecipients(); } /** * @return Address[] */ public function getRecipients(): array { if ($this->recipientsSet) { return parent::getRecipients(); } return self::getRecipientsFromHeaders($this->message->getHeaders()); } private static function getRecipientsFromHeaders(Headers $headers): array { $recipients = []; foreach (['to', 'cc', 'bcc'] as $name) { foreach ($headers->all($name) as $header) { foreach ($header->getAddresses() as $address) { $recipients[] = $address; } } } return $recipients; } private static function getSenderFromHeaders(Headers $headers): Address { if ($sender = $headers->get('Sender')) { return $sender->getAddress(); } if ($return = $headers->get('Return-Path')) { return $return->getAddress(); } if ($from = $headers->get('From')) { return $from->getAddresses()[0]; } throw new LogicException('Unable to determine the sender of the message.'); } } mailer/MailerInterface.php 0000644 00000001373 15025017654 0011571 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mime\RawMessage; /** * Interface for mailers able to send emails synchronous and/or asynchronous. * * Implementations must support synchronous and asynchronous sending. * * @author Fabien Potencier <fabien@symfony.com> */ interface MailerInterface { /** * @throws TransportExceptionInterface */ public function send(RawMessage $message, Envelope $envelope = null): void; } mailer/README.md 0000644 00000004115 15025017654 0007302 0 ustar 00 Mailer Component ================ The Mailer component helps sending emails. Getting Started --------------- ``` $ composer require symfony/mailer ``` ```php use Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mime\Email; $transport = Transport::fromDsn('smtp://localhost'); $mailer = new Mailer($transport); $email = (new Email()) ->from('hello@example.com') ->to('you@example.com') //->cc('cc@example.com') //->bcc('bcc@example.com') //->replyTo('fabien@example.com') //->priority(Email::PRIORITY_HIGH) ->subject('Time for Symfony Mailer!') ->text('Sending emails is fun again!') ->html('<p>See Twig integration for better HTML integration!</p>'); $mailer->send($email); ``` To enable the Twig integration of the Mailer, require `symfony/twig-bridge` and set up the `BodyRenderer`: ```php use Symfony\Bridge\Twig\Mime\BodyRenderer; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Mailer\EventListener\MessageListener; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mailer\Transport; use Twig\Environment as TwigEnvironment; $twig = new TwigEnvironment(...); $messageListener = new MessageListener(null, new BodyRenderer($twig)); $eventDispatcher = new EventDispatcher(); $eventDispatcher->addSubscriber($messageListener); $transport = Transport::fromDsn('smtp://localhost', $eventDispatcher); $mailer = new Mailer($transport, null, $eventDispatcher); $email = (new TemplatedEmail()) // ... ->htmlTemplate('emails/signup.html.twig') ->context([ 'expiration_date' => new \DateTime('+7 days'), 'username' => 'foo', ]) ; $mailer->send($email); ``` Resources --------- * [Documentation](https://symfony.com/doc/current/mailer.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) mailer/Test/Constraint/EmailCount.php 0000644 00000003537 15025017654 0013646 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\Mailer\Event\MessageEvents; final class EmailCount extends Constraint { private int $expectedValue; private ?string $transport; private bool $queued; public function __construct(int $expectedValue, string $transport = null, bool $queued = false) { $this->expectedValue = $expectedValue; $this->transport = $transport; $this->queued = $queued; } /** * {@inheritdoc} */ public function toString(): string { return sprintf('%shas %s "%d" emails', $this->transport ? $this->transport.' ' : '', $this->queued ? 'queued' : 'sent', $this->expectedValue); } /** * @param MessageEvents $events * * {@inheritdoc} */ protected function matches($events): bool { return $this->expectedValue === $this->countEmails($events); } /** * @param MessageEvents $events * * {@inheritdoc} */ protected function failureDescription($events): string { return sprintf('the Transport %s (%d %s)', $this->toString(), $this->countEmails($events), $this->queued ? 'queued' : 'sent'); } private function countEmails(MessageEvents $events): int { $count = 0; foreach ($events->getEvents($this->transport) as $event) { if ( ($this->queued && $event->isQueued()) || (!$this->queued && !$event->isQueued()) ) { ++$count; } } return $count; } } mailer/Test/Constraint/EmailIsQueued.php 0000644 00000001626 15025017654 0014277 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\Mailer\Event\MessageEvent; final class EmailIsQueued extends Constraint { /** * {@inheritdoc} */ public function toString(): string { return 'is queued'; } /** * @param MessageEvent $event * * {@inheritdoc} */ protected function matches($event): bool { return $event->isQueued(); } /** * @param MessageEvent $event * * {@inheritdoc} */ protected function failureDescription($event): string { return 'the Email '.$this->toString(); } } mailer/Test/TransportFactoryTestCase.php 0000644 00000006307 15025017654 0014440 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Test; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Exception\IncompleteDsnException; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; use Symfony\Component\Mailer\Transport\Dsn; use Symfony\Component\Mailer\Transport\TransportFactoryInterface; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * A test case to ease testing Transport Factory. * * @author Konstantin Myakshin <molodchick@gmail.com> */ abstract class TransportFactoryTestCase extends TestCase { protected const USER = 'u$er'; protected const PASSWORD = 'pa$s'; protected $dispatcher; protected $client; protected $logger; abstract public function getFactory(): TransportFactoryInterface; abstract public function supportsProvider(): iterable; abstract public function createProvider(): iterable; public function unsupportedSchemeProvider(): iterable { return []; } public function incompleteDsnProvider(): iterable { return []; } /** * @dataProvider supportsProvider */ public function testSupports(Dsn $dsn, bool $supports) { $factory = $this->getFactory(); $this->assertSame($supports, $factory->supports($dsn)); } /** * @dataProvider createProvider */ public function testCreate(Dsn $dsn, TransportInterface $transport) { $factory = $this->getFactory(); $this->assertEquals($transport, $factory->create($dsn)); if (str_contains('smtp', $dsn->getScheme())) { $this->assertStringMatchesFormat($dsn->getScheme().'://%S'.$dsn->getHost().'%S', (string) $transport); } } /** * @dataProvider unsupportedSchemeProvider */ public function testUnsupportedSchemeException(Dsn $dsn, string $message = null) { $factory = $this->getFactory(); $this->expectException(UnsupportedSchemeException::class); if (null !== $message) { $this->expectExceptionMessage($message); } $factory->create($dsn); } /** * @dataProvider incompleteDsnProvider */ public function testIncompleteDsnException(Dsn $dsn) { $factory = $this->getFactory(); $this->expectException(IncompleteDsnException::class); $factory->create($dsn); } protected function getDispatcher(): EventDispatcherInterface { return $this->dispatcher ?? $this->dispatcher = $this->createMock(EventDispatcherInterface::class); } protected function getClient(): HttpClientInterface { return $this->client ?? $this->client = $this->createMock(HttpClientInterface::class); } protected function getLogger(): LoggerInterface { return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class); } } mailer/LICENSE 0000644 00000002051 15025017654 0007025 0 ustar 00 Copyright (c) 2019-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mailer/Transport.php 0000644 00000015104 15025017654 0010530 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; use Symfony\Component\Mailer\Transport\Dsn; use Symfony\Component\Mailer\Transport\FailoverTransport; use Symfony\Component\Mailer\Transport\NativeTransportFactory; use Symfony\Component\Mailer\Transport\NullTransportFactory; use Symfony\Component\Mailer\Transport\RoundRobinTransport; use Symfony\Component\Mailer\Transport\SendmailTransportFactory; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory; use Symfony\Component\Mailer\Transport\TransportFactoryInterface; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mailer\Transport\Transports; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Fabien Potencier <fabien@symfony.com> * @author Konstantin Myakshin <molodchick@gmail.com> */ final class Transport { private const FACTORY_CLASSES = [ GmailTransportFactory::class, MailgunTransportFactory::class, MailjetTransportFactory::class, MandrillTransportFactory::class, OhMySmtpTransportFactory::class, PostmarkTransportFactory::class, SendgridTransportFactory::class, SendinblueTransportFactory::class, SesTransportFactory::class, ]; private iterable $factories; public static function fromDsn(string $dsn, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface { $factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger))); return $factory->fromString($dsn); } public static function fromDsns(array $dsns, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface { $factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger))); return $factory->fromStrings($dsns); } /** * @param TransportFactoryInterface[] $factories */ public function __construct(iterable $factories) { $this->factories = $factories; } public function fromStrings(array $dsns): Transports { $transports = []; foreach ($dsns as $name => $dsn) { $transports[$name] = $this->fromString($dsn); } return new Transports($transports); } public function fromString(string $dsn): TransportInterface { [$transport, $offset] = $this->parseDsn($dsn); if ($offset !== \strlen($dsn)) { throw new InvalidArgumentException(sprintf('The DSN has some garbage at the end: "%s".', substr($dsn, $offset))); } return $transport; } private function parseDsn(string $dsn, int $offset = 0): array { static $keywords = [ 'failover' => FailoverTransport::class, 'roundrobin' => RoundRobinTransport::class, ]; while (true) { foreach ($keywords as $name => $class) { $name .= '('; if ($name === substr($dsn, $offset, \strlen($name))) { $offset += \strlen($name) - 1; preg_match('{\(([^()]|(?R))*\)}A', $dsn, $matches, 0, $offset); if (!isset($matches[0])) { continue; } ++$offset; $args = []; while (true) { [$arg, $offset] = $this->parseDsn($dsn, $offset); $args[] = $arg; if (\strlen($dsn) === $offset) { break; } ++$offset; if (')' === $dsn[$offset - 1]) { break; } } return [new $class($args), $offset]; } } if (preg_match('{(\w+)\(}A', $dsn, $matches, 0, $offset)) { throw new InvalidArgumentException(sprintf('The "%s" keyword is not valid (valid ones are "%s"), ', $matches[1], implode('", "', array_keys($keywords)))); } if ($pos = strcspn($dsn, ' )', $offset)) { return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset, $pos))), $offset + $pos]; } return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset))), \strlen($dsn)]; } } public function fromDsnObject(Dsn $dsn): TransportInterface { foreach ($this->factories as $factory) { if ($factory->supports($dsn)) { return $factory->create($dsn); } } throw new UnsupportedSchemeException($dsn); } /** * @return \Traversable<int, TransportFactoryInterface> */ public static function getDefaultFactories(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): \Traversable { foreach (self::FACTORY_CLASSES as $factoryClass) { if (class_exists($factoryClass)) { yield new $factoryClass($dispatcher, $client, $logger); } } yield new NullTransportFactory($dispatcher, $client, $logger); yield new SendmailTransportFactory($dispatcher, $client, $logger); yield new EsmtpTransportFactory($dispatcher, $client, $logger); yield new NativeTransportFactory($dispatcher, $client, $logger); } } mailer/Event/MessageEvent.php 0000644 00000002767 15025017654 0012216 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Event; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mime\RawMessage; use Symfony\Contracts\EventDispatcher\Event; /** * Allows the transformation of a Message and the Envelope before the email is sent. * * @author Fabien Potencier <fabien@symfony.com> */ final class MessageEvent extends Event { private $message; private $envelope; private string $transport; private bool $queued; public function __construct(RawMessage $message, Envelope $envelope, string $transport, bool $queued = false) { $this->message = $message; $this->envelope = $envelope; $this->transport = $transport; $this->queued = $queued; } public function getMessage(): RawMessage { return $this->message; } public function setMessage(RawMessage $message): void { $this->message = $message; } public function getEnvelope(): Envelope { return $this->envelope; } public function setEnvelope(Envelope $envelope): void { $this->envelope = $envelope; } public function getTransport(): string { return $this->transport; } public function isQueued(): bool { return $this->queued; } } mailer/Event/MessageEvents.php 0000644 00000002762 15025017654 0012374 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Event; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ class MessageEvents { /** * @var MessageEvent[] */ private array $events = []; /** * @var array<string, bool> */ private array $transports = []; public function add(MessageEvent $event): void { $this->events[] = $event; $this->transports[$event->getTransport()] = true; } public function getTransports(): array { return array_keys($this->transports); } /** * @return MessageEvent[] */ public function getEvents(string $name = null): array { if (null === $name) { return $this->events; } $events = []; foreach ($this->events as $event) { if ($name === $event->getTransport()) { $events[] = $event; } } return $events; } /** * @return RawMessage[] */ public function getMessages(string $name = null): array { $events = $this->getEvents($name); $messages = []; foreach ($events as $event) { $messages[] = $event->getMessage(); } return $messages; } } mailer/Header/TagHeader.php 0000644 00000001043 15025017654 0011545 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Header; use Symfony\Component\Mime\Header\UnstructuredHeader; /** * @author Kevin Bond <kevinbond@gmail.com> */ final class TagHeader extends UnstructuredHeader { public function __construct(string $value) { parent::__construct('X-Tag', $value); } } mailer/Header/MetadataHeader.php 0000644 00000001303 15025017654 0012551 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Header; use Symfony\Component\Mime\Header\UnstructuredHeader; /** * @author Kevin Bond <kevinbond@gmail.com> */ final class MetadataHeader extends UnstructuredHeader { private string $key; public function __construct(string $key, string $value) { $this->key = $key; parent::__construct('X-Metadata-'.$key, $value); } public function getKey(): string { return $this->key; } } mailer/SentMessage.php 0000644 00000004042 15025017654 0010751 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Symfony\Component\Mime\Message; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ class SentMessage { private $original; private $raw; private $envelope; private string $messageId; private string $debug = ''; /** * @internal */ public function __construct(RawMessage $message, Envelope $envelope) { $message->ensureValidity(); $this->original = $message; $this->envelope = $envelope; if ($message instanceof Message) { $message = clone $message; $headers = $message->getHeaders(); if (!$headers->has('Message-ID')) { $headers->addIdHeader('Message-ID', $message->generateMessageId()); } $this->messageId = $headers->get('Message-ID')->getId(); $this->raw = new RawMessage($message->toIterable()); } else { $this->raw = $message; } } public function getMessage(): RawMessage { return $this->raw; } public function getOriginalMessage(): RawMessage { return $this->original; } public function getEnvelope(): Envelope { return $this->envelope; } public function setMessageId(string $id): void { $this->messageId = $id; } public function getMessageId(): string { return $this->messageId; } public function getDebug(): string { return $this->debug; } public function appendDebug(string $debug): void { $this->debug .= $debug; } public function toString(): string { return $this->raw->toString(); } public function toIterable(): iterable { return $this->raw->toIterable(); } } mailer/Envelope.php 0000644 00000005074 15025017654 0010316 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ class Envelope { private $sender; private array $recipients = []; /** * @param Address[] $recipients */ public function __construct(Address $sender, array $recipients) { $this->setSender($sender); $this->setRecipients($recipients); } public static function create(RawMessage $message): self { if (RawMessage::class === \get_class($message)) { throw new LogicException('Cannot send a RawMessage instance without an explicit Envelope.'); } return new DelayedEnvelope($message); } public function setSender(Address $sender): void { // to ensure deliverability of bounce emails independent of UTF-8 capabilities of SMTP servers if (!preg_match('/^[^@\x80-\xFF]++@/', $sender->getAddress())) { throw new InvalidArgumentException(sprintf('Invalid sender "%s": non-ASCII characters not supported in local-part of email.', $sender->getAddress())); } $this->sender = $sender; } /** * @return Address Returns a "mailbox" as specified by RFC 2822 * Must be converted to an "addr-spec" when used as a "MAIL FROM" value in SMTP (use getAddress()) */ public function getSender(): Address { return $this->sender; } /** * @param Address[] $recipients */ public function setRecipients(array $recipients): void { if (!$recipients) { throw new InvalidArgumentException('An envelope must have at least one recipient.'); } $this->recipients = []; foreach ($recipients as $recipient) { if (!$recipient instanceof Address) { throw new InvalidArgumentException(sprintf('A recipient must be an instance of "%s" (got "%s").', Address::class, get_debug_type($recipient))); } $this->recipients[] = new Address($recipient->getAddress()); } } /** * @return Address[] */ public function getRecipients(): array { return $this->recipients; } } deprecation-contracts/.gitignore 0000644 00000000042 15025017654 0013030 0 ustar 00 vendor/ composer.lock phpunit.xml deprecation-contracts/composer.json 0000644 00000001513 15025017654 0013566 0 ustar 00 { "name": "symfony/deprecation-contracts", "type": "library", "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.0.2" }, "autoload": { "files": [ "function.php" ] }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "3.0-dev" }, "thanks": { "name": "symfony/contracts", "url": "https://github.com/symfony/contracts" } } } deprecation-contracts/CHANGELOG.md 0000644 00000000235 15025017654 0012655 0 ustar 00 CHANGELOG ========= The changelog is maintained for all Symfony contracts at the following URL: https://github.com/symfony/contracts/blob/main/CHANGELOG.md deprecation-contracts/README.md 0000644 00000002264 15025017654 0012327 0 ustar 00 Symfony Deprecation Contracts ============================= A generic function and convention to trigger deprecation notices. This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices. By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component, the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments. The function requires at least 3 arguments: - the name of the Composer package that is triggering the deprecation - the version of the package that introduced the deprecation - the message of the deprecation - more arguments can be provided: they will be inserted in the message using `printf()` formatting Example: ```php trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin'); ``` This will generate the following message: `Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.` While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty `function trigger_deprecation() {}` in your application. deprecation-contracts/LICENSE 0000644 00000002051 15025017654 0012047 0 ustar 00 Copyright (c) 2020-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. deprecation-contracts/function.php 0000644 00000001766 15025017654 0013414 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (!function_exists('trigger_deprecation')) { /** * Triggers a silenced deprecation notice. * * @param string $package The name of the Composer package that is triggering the deprecation * @param string $version The version of the package that introduced the deprecation * @param string $message The message of the deprecation * @param mixed ...$args Values to insert in the message using printf() formatting * * @author Nicolas Grekas <p@tchwork.com> */ function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void { @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); } } polyfill-php81/Php81.php 0000644 00000001306 15025017654 0010752 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Php81; /** * @author Nicolas Grekas <p@tchwork.com> * * @internal */ final class Php81 { public static function array_is_list(array $array): bool { if ([] === $array || $array === array_values($array)) { return true; } $nextKey = -1; foreach ($array as $k => $v) { if ($k !== ++$nextKey) { return false; } } return true; } } polyfill-php81/bootstrap.php 0000644 00000001342 15025017654 0012067 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Php81 as p; if (\PHP_VERSION_ID >= 80100) { return; } if (defined('MYSQLI_REFRESH_SLAVE') && !defined('MYSQLI_REFRESH_REPLICA')) { define('MYSQLI_REFRESH_REPLICA', 64); } if (!function_exists('array_is_list')) { function array_is_list(array $array): bool { return p\Php81::array_is_list($array); } } if (!function_exists('enum_exists')) { function enum_exists(string $enum, bool $autoload = true): bool { return $autoload && class_exists($enum) && false; } } polyfill-php81/composer.json 0000644 00000001734 15025017654 0012070 0 ustar 00 { "name": "symfony/polyfill-php81", "type": "library", "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", "keywords": ["polyfill", "shim", "compatibility", "portable"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.1" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Php81\\": "" }, "files": [ "bootstrap.php" ], "classmap": [ "Resources/stubs" ] }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } polyfill-php81/README.md 0000644 00000001113 15025017654 0010614 0 ustar 00 Symfony Polyfill / Php81 ======================== This component provides features added to PHP 8.1 core: - [`array_is_list`](https://php.net/array_is_list) - [`enum_exists`](https://php.net/enum-exists) - [`MYSQLI_REFRESH_REPLICA`](https://php.net/mysqli.constants#constantmysqli-refresh-replica) constant - [`ReturnTypeWillChange`](https://wiki.php.net/rfc/internal_method_return_types) More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). polyfill-php81/LICENSE 0000644 00000002044 15025017654 0010346 0 ustar 00 Copyright (c) 2021 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. polyfill-php81/Resources/stubs/ReturnTypeWillChange.php 0000644 00000000645 15025017654 0017250 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80100) { #[Attribute(Attribute::TARGET_METHOD)] final class ReturnTypeWillChange { public function __construct() { } } } translation/TranslatableMessage.php 0000644 00000003116 15025017654 0013542 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Nate Wiebe <nate@northern.co> */ class TranslatableMessage implements TranslatableInterface { private string $message; private array $parameters; private ?string $domain; public function __construct(string $message, array $parameters = [], string $domain = null) { $this->message = $message; $this->parameters = $parameters; $this->domain = $domain; } public function __toString(): string { return $this->getMessage(); } public function getMessage(): string { return $this->message; } public function getParameters(): array { return $this->parameters; } public function getDomain(): ?string { return $this->domain; } public function trans(TranslatorInterface $translator, string $locale = null): string { return $translator->trans($this->getMessage(), array_map( static function ($parameter) use ($translator, $locale) { return $parameter instanceof TranslatableInterface ? $parameter->trans($translator, $locale) : $parameter; }, $this->getParameters() ), $this->getDomain(), $locale); } } translation/Writer/TranslationWriterInterface.php 0000644 00000001655 15025017654 0016417 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Writer; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationWriter writes translation messages. * * @author Michel Salib <michelsalib@hotmail.com> */ interface TranslationWriterInterface { /** * Writes translation from the catalogue according to the selected format. * * @param string $format The format to use to dump the messages * @param array $options Options that are passed to the dumper * * @throws InvalidArgumentException */ public function write(MessageCatalogue $catalogue, string $format, array $options = []); } translation/Writer/TranslationWriter.php 0000644 00000004104 15025017654 0014566 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Writer; use Symfony\Component\Translation\Dumper\DumperInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationWriter writes translation messages. * * @author Michel Salib <michelsalib@hotmail.com> */ class TranslationWriter implements TranslationWriterInterface { /** * @var array<string, DumperInterface> */ private array $dumpers = []; /** * Adds a dumper to the writer. */ public function addDumper(string $format, DumperInterface $dumper) { $this->dumpers[$format] = $dumper; } /** * Obtains the list of supported formats. */ public function getFormats(): array { return array_keys($this->dumpers); } /** * Writes translation from the catalogue according to the selected format. * * @param string $format The format to use to dump the messages * @param array $options Options that are passed to the dumper * * @throws InvalidArgumentException */ public function write(MessageCatalogue $catalogue, string $format, array $options = []) { if (!isset($this->dumpers[$format])) { throw new InvalidArgumentException(sprintf('There is no dumper associated with format "%s".', $format)); } // get the right dumper $dumper = $this->dumpers[$format]; if (isset($options['path']) && !is_dir($options['path']) && !@mkdir($options['path'], 0777, true) && !is_dir($options['path'])) { throw new RuntimeException(sprintf('Translation Writer was not able to create directory "%s".', $options['path'])); } // save $dumper->dump($catalogue, $options); } } translation/Extractor/PhpExtractor.php 0000644 00000022145 15025017654 0014222 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use Symfony\Component\Finder\Finder; use Symfony\Component\Translation\MessageCatalogue; /** * PhpExtractor extracts translation messages from a PHP template. * * @author Michel Salib <michelsalib@hotmail.com> */ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface { public const MESSAGE_TOKEN = 300; public const METHOD_ARGUMENTS_TOKEN = 1000; public const DOMAIN_TOKEN = 1001; /** * Prefix for new found message. */ private string $prefix = ''; /** * The sequence that captures translation messages. */ protected $sequences = [ [ '->', 'trans', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ '->', 'trans', '(', self::MESSAGE_TOKEN, ], [ 'new', 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ 'new', 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ], [ 'new', '\\', 'Symfony', '\\', 'Component', '\\', 'Translation', '\\', 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ 'new', '\Symfony\Component\Translation\TranslatableMessage', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ 'new', '\\', 'Symfony', '\\', 'Component', '\\', 'Translation', '\\', 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ], [ 'new', '\Symfony\Component\Translation\TranslatableMessage', '(', self::MESSAGE_TOKEN, ], [ 't', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ 't', '(', self::MESSAGE_TOKEN, ], ]; /** * {@inheritdoc} */ public function extract(string|iterable $resource, MessageCatalogue $catalog) { $files = $this->extractFiles($resource); foreach ($files as $file) { $this->parseTokens(token_get_all(file_get_contents($file)), $catalog, $file); gc_mem_caches(); } } /** * {@inheritdoc} */ public function setPrefix(string $prefix) { $this->prefix = $prefix; } /** * Normalizes a token. */ protected function normalizeToken(mixed $token): ?string { if (isset($token[1]) && 'b"' !== $token) { return $token[1]; } return $token; } /** * Seeks to a non-whitespace token. */ private function seekToNextRelevantToken(\Iterator $tokenIterator) { for (; $tokenIterator->valid(); $tokenIterator->next()) { $t = $tokenIterator->current(); if (\T_WHITESPACE !== $t[0]) { break; } } } private function skipMethodArgument(\Iterator $tokenIterator) { $openBraces = 0; for (; $tokenIterator->valid(); $tokenIterator->next()) { $t = $tokenIterator->current(); if ('[' === $t[0] || '(' === $t[0]) { ++$openBraces; } if (']' === $t[0] || ')' === $t[0]) { --$openBraces; } if ((0 === $openBraces && ',' === $t[0]) || (-1 === $openBraces && ')' === $t[0])) { break; } } } /** * Extracts the message from the iterator while the tokens * match allowed message tokens. */ private function getValue(\Iterator $tokenIterator) { $message = ''; $docToken = ''; $docPart = ''; for (; $tokenIterator->valid(); $tokenIterator->next()) { $t = $tokenIterator->current(); if ('.' === $t) { // Concatenate with next token continue; } if (!isset($t[1])) { break; } switch ($t[0]) { case \T_START_HEREDOC: $docToken = $t[1]; break; case \T_ENCAPSED_AND_WHITESPACE: case \T_CONSTANT_ENCAPSED_STRING: if ('' === $docToken) { $message .= PhpStringTokenParser::parse($t[1]); } else { $docPart = $t[1]; } break; case \T_END_HEREDOC: if ($indentation = strspn($t[1], ' ')) { $docPartWithLineBreaks = $docPart; $docPart = ''; foreach (preg_split('~(\r\n|\n|\r)~', $docPartWithLineBreaks, -1, \PREG_SPLIT_DELIM_CAPTURE) as $str) { if (\in_array($str, ["\r\n", "\n", "\r"], true)) { $docPart .= $str; } else { $docPart .= substr($str, $indentation); } } } $message .= PhpStringTokenParser::parseDocString($docToken, $docPart); $docToken = ''; $docPart = ''; break; case \T_WHITESPACE: break; default: break 2; } } return $message; } /** * Extracts trans message from PHP tokens. */ protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename) { $tokenIterator = new \ArrayIterator($tokens); for ($key = 0; $key < $tokenIterator->count(); ++$key) { foreach ($this->sequences as $sequence) { $message = ''; $domain = 'messages'; $tokenIterator->seek($key); foreach ($sequence as $sequenceKey => $item) { $this->seekToNextRelevantToken($tokenIterator); if ($this->normalizeToken($tokenIterator->current()) === $item) { $tokenIterator->next(); continue; } elseif (self::MESSAGE_TOKEN === $item) { $message = $this->getValue($tokenIterator); if (\count($sequence) === ($sequenceKey + 1)) { break; } } elseif (self::METHOD_ARGUMENTS_TOKEN === $item) { $this->skipMethodArgument($tokenIterator); } elseif (self::DOMAIN_TOKEN === $item) { $domainToken = $this->getValue($tokenIterator); if ('' !== $domainToken) { $domain = $domainToken; } break; } else { break; } } if ($message) { $catalog->set($message, $this->prefix.$message, $domain); $metadata = $catalog->getMetadata($message, $domain) ?? []; $normalizedFilename = preg_replace('{[\\\\/]+}', '/', $filename); $metadata['sources'][] = $normalizedFilename.':'.$tokens[$key][2]; $catalog->setMetadata($message, $metadata, $domain); break; } } } } /** * @throws \InvalidArgumentException */ protected function canBeExtracted(string $file): bool { return $this->isFile($file) && 'php' === pathinfo($file, \PATHINFO_EXTENSION); } /** * {@inheritdoc} */ protected function extractFromDirectory(string|array $directory): iterable { if (!class_exists(Finder::class)) { throw new \LogicException(sprintf('You cannot use "%s" as the "symfony/finder" package is not installed. Try running "composer require symfony/finder".', static::class)); } $finder = new Finder(); return $finder->files()->name('*.php')->in($directory); } } translation/Extractor/ExtractorInterface.php 0000644 00000001735 15025017654 0015375 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use Symfony\Component\Translation\MessageCatalogue; /** * Extracts translation messages from a directory or files to the catalogue. * New found messages are injected to the catalogue using the prefix. * * @author Michel Salib <michelsalib@hotmail.com> */ interface ExtractorInterface { /** * Extracts translation messages from files, a file or a directory to the catalogue. * * @param string|iterable<string> $resource Files, a file or a directory */ public function extract(string|iterable $resource, MessageCatalogue $catalogue); /** * Sets the prefix that should be used for new found messages. */ public function setPrefix(string $prefix); } translation/Extractor/AbstractFileExtractor.php 0000644 00000003316 15025017654 0016035 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use Symfony\Component\Translation\Exception\InvalidArgumentException; /** * Base class used by classes that extract translation messages from files. * * @author Marcos D. Sánchez <marcosdsanchez@gmail.com> */ abstract class AbstractFileExtractor { protected function extractFiles(string|iterable $resource): iterable { if (is_iterable($resource)) { $files = []; foreach ($resource as $file) { if ($this->canBeExtracted($file)) { $files[] = $this->toSplFileInfo($file); } } } elseif (is_file($resource)) { $files = $this->canBeExtracted($resource) ? [$this->toSplFileInfo($resource)] : []; } else { $files = $this->extractFromDirectory($resource); } return $files; } private function toSplFileInfo(string $file): \SplFileInfo { return new \SplFileInfo($file); } /** * @throws InvalidArgumentException */ protected function isFile(string $file): bool { if (!is_file($file)) { throw new InvalidArgumentException(sprintf('The "%s" file does not exist.', $file)); } return true; } /** * @return bool */ abstract protected function canBeExtracted(string $file); /** * @return iterable */ abstract protected function extractFromDirectory(string|array $resource); } translation/Extractor/ChainExtractor.php 0000644 00000002434 15025017654 0014514 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use Symfony\Component\Translation\MessageCatalogue; /** * ChainExtractor extracts translation messages from template files. * * @author Michel Salib <michelsalib@hotmail.com> */ class ChainExtractor implements ExtractorInterface { /** * The extractors. * * @var ExtractorInterface[] */ private array $extractors = []; /** * Adds a loader to the translation extractor. */ public function addExtractor(string $format, ExtractorInterface $extractor) { $this->extractors[$format] = $extractor; } /** * {@inheritdoc} */ public function setPrefix(string $prefix) { foreach ($this->extractors as $extractor) { $extractor->setPrefix($prefix); } } /** * {@inheritdoc} */ public function extract(string|iterable $directory, MessageCatalogue $catalogue) { foreach ($this->extractors as $extractor) { $extractor->extract($directory, $catalogue); } } } translation/Extractor/PhpStringTokenParser.php 0000644 00000010325 15025017654 0015670 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; /* * The following is derived from code at http://github.com/nikic/PHP-Parser * * Copyright (c) 2011 by Nikita Popov * * Some rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * * The names of the contributors may not be used to endorse or * promote products derived from this software without specific * prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class PhpStringTokenParser { protected static $replacements = [ '\\' => '\\', '$' => '$', 'n' => "\n", 'r' => "\r", 't' => "\t", 'f' => "\f", 'v' => "\v", 'e' => "\x1B", ]; /** * Parses a string token. * * @param string $str String token content */ public static function parse(string $str): string { $bLength = 0; if ('b' === $str[0]) { $bLength = 1; } if ('\'' === $str[$bLength]) { return str_replace( ['\\\\', '\\\''], ['\\', '\''], substr($str, $bLength + 1, -1) ); } else { return self::parseEscapeSequences(substr($str, $bLength + 1, -1), '"'); } } /** * Parses escape sequences in strings (all string types apart from single quoted). * * @param string $str String without quotes * @param string|null $quote Quote type */ public static function parseEscapeSequences(string $str, string $quote = null): string { if (null !== $quote) { $str = str_replace('\\'.$quote, $quote, $str); } return preg_replace_callback( '~\\\\([\\\\$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3})~', [__CLASS__, 'parseCallback'], $str ); } private static function parseCallback(array $matches): string { $str = $matches[1]; if (isset(self::$replacements[$str])) { return self::$replacements[$str]; } elseif ('x' === $str[0] || 'X' === $str[0]) { return \chr(hexdec($str)); } else { return \chr(octdec($str)); } } /** * Parses a constant doc string. * * @param string $startToken Doc string start token content (<<<SMTHG) * @param string $str String token content */ public static function parseDocString(string $startToken, string $str): string { // strip last newline (thanks tokenizer for sticking it into the string!) $str = preg_replace('~(\r\n|\n|\r)$~', '', $str); // nowdoc string if (str_contains($startToken, '\'')) { return $str; } return self::parseEscapeSequences($str, null); } } translation/MessageCatalogue.php 0000644 00000020771 15025017654 0013040 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\Translation\Exception\LogicException; /** * @author Fabien Potencier <fabien@symfony.com> */ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterface { private array $messages = []; private array $metadata = []; private array $resources = []; private string $locale; private $fallbackCatalogue = null; private ?self $parent = null; /** * @param array $messages An array of messages classified by domain */ public function __construct(string $locale, array $messages = []) { $this->locale = $locale; $this->messages = $messages; } /** * {@inheritdoc} */ public function getLocale(): string { return $this->locale; } /** * {@inheritdoc} */ public function getDomains(): array { $domains = []; foreach ($this->messages as $domain => $messages) { if (str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) { $domain = substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)); } $domains[$domain] = $domain; } return array_values($domains); } /** * {@inheritdoc} */ public function all(string $domain = null): array { if (null !== $domain) { // skip messages merge if intl-icu requested explicitly if (str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) { return $this->messages[$domain] ?? []; } return ($this->messages[$domain.self::INTL_DOMAIN_SUFFIX] ?? []) + ($this->messages[$domain] ?? []); } $allMessages = []; foreach ($this->messages as $domain => $messages) { if (str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) { $domain = substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)); $allMessages[$domain] = $messages + ($allMessages[$domain] ?? []); } else { $allMessages[$domain] = ($allMessages[$domain] ?? []) + $messages; } } return $allMessages; } /** * {@inheritdoc} */ public function set(string $id, string $translation, string $domain = 'messages') { $this->add([$id => $translation], $domain); } /** * {@inheritdoc} */ public function has(string $id, string $domain = 'messages'): bool { if (isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { return true; } if (null !== $this->fallbackCatalogue) { return $this->fallbackCatalogue->has($id, $domain); } return false; } /** * {@inheritdoc} */ public function defines(string $id, string $domain = 'messages'): bool { return isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]); } /** * {@inheritdoc} */ public function get(string $id, string $domain = 'messages'): string { if (isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { return $this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]; } if (isset($this->messages[$domain][$id])) { return $this->messages[$domain][$id]; } if (null !== $this->fallbackCatalogue) { return $this->fallbackCatalogue->get($id, $domain); } return $id; } /** * {@inheritdoc} */ public function replace(array $messages, string $domain = 'messages') { unset($this->messages[$domain], $this->messages[$domain.self::INTL_DOMAIN_SUFFIX]); $this->add($messages, $domain); } /** * {@inheritdoc} */ public function add(array $messages, string $domain = 'messages') { $altDomain = str_ends_with($domain, self::INTL_DOMAIN_SUFFIX) ? substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)) : $domain.self::INTL_DOMAIN_SUFFIX; foreach ($messages as $id => $message) { unset($this->messages[$altDomain][$id]); $this->messages[$domain][$id] = $message; } if ([] === ($this->messages[$altDomain] ?? null)) { unset($this->messages[$altDomain]); } } /** * {@inheritdoc} */ public function addCatalogue(MessageCatalogueInterface $catalogue) { if ($catalogue->getLocale() !== $this->locale) { throw new LogicException(sprintf('Cannot add a catalogue for locale "%s" as the current locale for this catalogue is "%s".', $catalogue->getLocale(), $this->locale)); } foreach ($catalogue->all() as $domain => $messages) { if ($intlMessages = $catalogue->all($domain.self::INTL_DOMAIN_SUFFIX)) { $this->add($intlMessages, $domain.self::INTL_DOMAIN_SUFFIX); $messages = array_diff_key($messages, $intlMessages); } $this->add($messages, $domain); } foreach ($catalogue->getResources() as $resource) { $this->addResource($resource); } if ($catalogue instanceof MetadataAwareInterface) { $metadata = $catalogue->getMetadata('', ''); $this->addMetadata($metadata); } } /** * {@inheritdoc} */ public function addFallbackCatalogue(MessageCatalogueInterface $catalogue) { // detect circular references $c = $catalogue; while ($c = $c->getFallbackCatalogue()) { if ($c->getLocale() === $this->getLocale()) { throw new LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale())); } } $c = $this; do { if ($c->getLocale() === $catalogue->getLocale()) { throw new LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale())); } foreach ($catalogue->getResources() as $resource) { $c->addResource($resource); } } while ($c = $c->parent); $catalogue->parent = $this; $this->fallbackCatalogue = $catalogue; foreach ($catalogue->getResources() as $resource) { $this->addResource($resource); } } /** * {@inheritdoc} */ public function getFallbackCatalogue(): ?MessageCatalogueInterface { return $this->fallbackCatalogue; } /** * {@inheritdoc} */ public function getResources(): array { return array_values($this->resources); } /** * {@inheritdoc} */ public function addResource(ResourceInterface $resource) { $this->resources[$resource->__toString()] = $resource; } /** * {@inheritdoc} */ public function getMetadata(string $key = '', string $domain = 'messages'): mixed { if ('' == $domain) { return $this->metadata; } if (isset($this->metadata[$domain])) { if ('' == $key) { return $this->metadata[$domain]; } if (isset($this->metadata[$domain][$key])) { return $this->metadata[$domain][$key]; } } return null; } /** * {@inheritdoc} */ public function setMetadata(string $key, mixed $value, string $domain = 'messages') { $this->metadata[$domain][$key] = $value; } /** * {@inheritdoc} */ public function deleteMetadata(string $key = '', string $domain = 'messages') { if ('' == $domain) { $this->metadata = []; } elseif ('' == $key) { unset($this->metadata[$domain]); } else { unset($this->metadata[$domain][$key]); } } /** * Adds current values with the new values. * * @param array $values Values to add */ private function addMetadata(array $values) { foreach ($values as $domain => $keys) { foreach ($keys as $key => $value) { $this->setMetadata($key, $value, $domain); } } } } translation/DependencyInjection/TranslatorPass.php 0000644 00000005325 15025017654 0016526 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; class TranslatorPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { if (!$container->hasDefinition('translator.default')) { return; } $loaders = []; $loaderRefs = []; foreach ($container->findTaggedServiceIds('translation.loader', true) as $id => $attributes) { $loaderRefs[$id] = new Reference($id); $loaders[$id][] = $attributes[0]['alias']; if (isset($attributes[0]['legacy-alias'])) { $loaders[$id][] = $attributes[0]['legacy-alias']; } } if ($container->hasDefinition('translation.reader')) { $definition = $container->getDefinition('translation.reader'); foreach ($loaders as $id => $formats) { foreach ($formats as $format) { $definition->addMethodCall('addLoader', [$format, $loaderRefs[$id]]); } } } $container ->findDefinition('translator.default') ->replaceArgument(0, ServiceLocatorTagPass::register($container, $loaderRefs)) ->replaceArgument(3, $loaders) ; if (!$container->hasParameter('twig.default_path')) { return; } $paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1)); if ($container->hasDefinition('console.command.translation_debug')) { $definition = $container->getDefinition('console.command.translation_debug'); $definition->replaceArgument(4, $container->getParameter('twig.default_path')); if (\count($definition->getArguments()) > 6) { $definition->replaceArgument(6, $paths); } } if ($container->hasDefinition('console.command.translation_extract')) { $definition = $container->getDefinition('console.command.translation_extract'); $definition->replaceArgument(5, $container->getParameter('twig.default_path')); if (\count($definition->getArguments()) > 7) { $definition->replaceArgument(7, $paths); } } } } translation/DependencyInjection/TranslatorPathsPass.php 0000644 00000011657 15025017654 0017533 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\AbstractRecursivePass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; /** * @author Yonel Ceruto <yonelceruto@gmail.com> */ class TranslatorPathsPass extends AbstractRecursivePass { private int $level = 0; /** * @var array<string, bool> */ private array $paths = []; /** * @var array<int, Definition> */ private array $definitions = []; /** * @var array<string, array<string, bool>> */ private array $controllers = []; public function process(ContainerBuilder $container) { if (!$container->hasDefinition('translator')) { return; } foreach ($this->findControllerArguments($container) as $controller => $argument) { $id = substr($controller, 0, strpos($controller, ':') ?: \strlen($controller)); if ($container->hasDefinition($id)) { [$locatorRef] = $argument->getValues(); $this->controllers[(string) $locatorRef][$container->getDefinition($id)->getClass()] = true; } } try { parent::process($container); $paths = []; foreach ($this->paths as $class => $_) { if (($r = $container->getReflectionClass($class)) && !$r->isInterface()) { $paths[] = $r->getFileName(); foreach ($r->getTraits() as $trait) { $paths[] = $trait->getFileName(); } } } if ($paths) { if ($container->hasDefinition('console.command.translation_debug')) { $definition = $container->getDefinition('console.command.translation_debug'); $definition->replaceArgument(6, array_merge($definition->getArgument(6), $paths)); } if ($container->hasDefinition('console.command.translation_extract')) { $definition = $container->getDefinition('console.command.translation_extract'); $definition->replaceArgument(7, array_merge($definition->getArgument(7), $paths)); } } } finally { $this->level = 0; $this->paths = []; $this->definitions = []; } } protected function processValue(mixed $value, bool $isRoot = false): mixed { if ($value instanceof Reference) { if ('translator' === (string) $value) { for ($i = $this->level - 1; $i >= 0; --$i) { $class = $this->definitions[$i]->getClass(); if (ServiceLocator::class === $class) { if (!isset($this->controllers[$this->currentId])) { continue; } foreach ($this->controllers[$this->currentId] as $class => $_) { $this->paths[$class] = true; } } else { $this->paths[$class] = true; } break; } } return $value; } if ($value instanceof Definition) { $this->definitions[$this->level++] = $value; $value = parent::processValue($value, $isRoot); unset($this->definitions[--$this->level]); return $value; } return parent::processValue($value, $isRoot); } private function findControllerArguments(ContainerBuilder $container): array { if ($container->hasDefinition('argument_resolver.service')) { $argument = $container->getDefinition('argument_resolver.service')->getArgument(0); if ($argument instanceof Reference) { $argument = $container->getDefinition($argument); } return $argument->getArgument(0); } if ($container->hasDefinition('debug.'.'argument_resolver.service')) { $argument = $container->getDefinition('debug.'.'argument_resolver.service')->getArgument(0); if ($argument instanceof Reference) { $argument = $container->getDefinition($argument); } $argument = $argument->getArgument(0); if ($argument instanceof Reference) { $argument = $container->getDefinition($argument); } return $argument->getArgument(0); } return []; } } translation/DependencyInjection/TranslationDumperPass.php 0000644 00000002067 15025017654 0020050 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; /** * Adds tagged translation.formatter services to translation writer. */ class TranslationDumperPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { if (!$container->hasDefinition('translation.writer')) { return; } $definition = $container->getDefinition('translation.writer'); foreach ($container->findTaggedServiceIds('translation.dumper', true) as $id => $attributes) { $definition->addMethodCall('addDumper', [$attributes[0]['alias'], new Reference($id)]); } } } translation/DependencyInjection/TranslationExtractorPass.php 0000644 00000002531 15025017654 0020563 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; /** * Adds tagged translation.extractor services to translation extractor. */ class TranslationExtractorPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { if (!$container->hasDefinition('translation.extractor')) { return; } $definition = $container->getDefinition('translation.extractor'); foreach ($container->findTaggedServiceIds('translation.extractor', true) as $id => $attributes) { if (!isset($attributes[0]['alias'])) { throw new RuntimeException(sprintf('The alias for the tag "translation.extractor" of service "%s" must be set.', $id)); } $definition->addMethodCall('addExtractor', [$attributes[0]['alias'], new Reference($id)]); } } } translation/Reader/TranslationReader.php 0000644 00000003337 15025017654 0014451 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Reader; use Symfony\Component\Finder\Finder; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationReader reads translation messages from translation files. * * @author Michel Salib <michelsalib@hotmail.com> */ class TranslationReader implements TranslationReaderInterface { /** * Loaders used for import. * * @var array<string, LoaderInterface> */ private array $loaders = []; /** * Adds a loader to the translation extractor. * * @param string $format The format of the loader */ public function addLoader(string $format, LoaderInterface $loader) { $this->loaders[$format] = $loader; } /** * {@inheritdoc} */ public function read(string $directory, MessageCatalogue $catalogue) { if (!is_dir($directory)) { return; } foreach ($this->loaders as $format => $loader) { // load any existing translation files $finder = new Finder(); $extension = $catalogue->getLocale().'.'.$format; $files = $finder->files()->name('*.'.$extension)->in($directory); foreach ($files as $file) { $domain = substr($file->getFilename(), 0, -1 * \strlen($extension) - 1); $catalogue->addCatalogue($loader->load($file->getPathname(), $catalogue->getLocale(), $domain)); } } } } translation/Reader/TranslationReaderInterface.php 0000644 00000001242 15025017654 0016263 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Reader; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationReader reads translation messages from translation files. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ interface TranslationReaderInterface { /** * Reads translation messages from a directory to the catalogue. */ public function read(string $directory, MessageCatalogue $catalogue); } translation/DataCollector/TranslationDataCollector.php 0000644 00000010535 15025017654 0017303 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\Translation\DataCollectorTranslator; use Symfony\Component\VarDumper\Cloner\Data; /** * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> * * @final */ class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface { private $translator; public function __construct(DataCollectorTranslator $translator) { $this->translator = $translator; } /** * {@inheritdoc} */ public function lateCollect() { $messages = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages()); $this->data += $this->computeCount($messages); $this->data['messages'] = $messages; $this->data = $this->cloneVar($this->data); } /** * {@inheritdoc} */ public function collect(Request $request, Response $response, \Throwable $exception = null) { $this->data['locale'] = $this->translator->getLocale(); $this->data['fallback_locales'] = $this->translator->getFallbackLocales(); } /** * {@inheritdoc} */ public function reset() { $this->data = []; } public function getMessages(): array|Data { return $this->data['messages'] ?? []; } public function getCountMissings(): int { return $this->data[DataCollectorTranslator::MESSAGE_MISSING] ?? 0; } public function getCountFallbacks(): int { return $this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] ?? 0; } public function getCountDefines(): int { return $this->data[DataCollectorTranslator::MESSAGE_DEFINED] ?? 0; } public function getLocale() { return !empty($this->data['locale']) ? $this->data['locale'] : null; } /** * @internal */ public function getFallbackLocales() { return (isset($this->data['fallback_locales']) && \count($this->data['fallback_locales']) > 0) ? $this->data['fallback_locales'] : []; } /** * {@inheritdoc} */ public function getName(): string { return 'translation'; } private function sanitizeCollectedMessages(array $messages) { $result = []; foreach ($messages as $key => $message) { $messageId = $message['locale'].$message['domain'].$message['id']; if (!isset($result[$messageId])) { $message['count'] = 1; $message['parameters'] = !empty($message['parameters']) ? [$message['parameters']] : []; $messages[$key]['translation'] = $this->sanitizeString($message['translation']); $result[$messageId] = $message; } else { if (!empty($message['parameters'])) { $result[$messageId]['parameters'][] = $message['parameters']; } ++$result[$messageId]['count']; } unset($messages[$key]); } return $result; } private function computeCount(array $messages) { $count = [ DataCollectorTranslator::MESSAGE_DEFINED => 0, DataCollectorTranslator::MESSAGE_MISSING => 0, DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK => 0, ]; foreach ($messages as $message) { ++$count[$message['state']]; } return $count; } private function sanitizeString(string $string, int $length = 80) { $string = trim(preg_replace('/\s+/', ' ', $string)); if (false !== $encoding = mb_detect_encoding($string, null, true)) { if (mb_strlen($string, $encoding) > $length) { return mb_substr($string, 0, $length - 3, $encoding).'...'; } } elseif (\strlen($string) > $length) { return substr($string, 0, $length - 3).'...'; } return $string; } } translation/Translator.php 0000644 00000033616 15025017654 0011762 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\Config\ConfigCacheFactory; use Symfony\Component\Config\ConfigCacheFactoryInterface; use Symfony\Component\Config\ConfigCacheInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\Formatter\IntlFormatterInterface; use Symfony\Component\Translation\Formatter\MessageFormatter; use Symfony\Component\Translation\Formatter\MessageFormatterInterface; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; // Help opcache.preload discover always-needed symbols class_exists(MessageCatalogue::class); /** * @author Fabien Potencier <fabien@symfony.com> */ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface { /** * @var MessageCatalogueInterface[] */ protected $catalogues = []; private string $locale; /** * @var string[] */ private array $fallbackLocales = []; /** * @var LoaderInterface[] */ private array $loaders = []; private array $resources = []; private $formatter; private ?string $cacheDir; private bool $debug; private array $cacheVary; private $configCacheFactory; private array $parentLocales; private bool $hasIntlFormatter; /** * @throws InvalidArgumentException If a locale contains invalid characters */ public function __construct(string $locale, MessageFormatterInterface $formatter = null, string $cacheDir = null, bool $debug = false, array $cacheVary = []) { $this->setLocale($locale); if (null === $formatter) { $formatter = new MessageFormatter(); } $this->formatter = $formatter; $this->cacheDir = $cacheDir; $this->debug = $debug; $this->cacheVary = $cacheVary; $this->hasIntlFormatter = $formatter instanceof IntlFormatterInterface; } public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory) { $this->configCacheFactory = $configCacheFactory; } /** * Adds a Loader. * * @param string $format The name of the loader (@see addResource()) */ public function addLoader(string $format, LoaderInterface $loader) { $this->loaders[$format] = $loader; } /** * Adds a Resource. * * @param string $format The name of the loader (@see addLoader()) * @param mixed $resource The resource name * * @throws InvalidArgumentException If the locale contains invalid characters */ public function addResource(string $format, mixed $resource, string $locale, string $domain = null) { if (null === $domain) { $domain = 'messages'; } $this->assertValidLocale($locale); $locale ?: $locale = class_exists(\Locale::class) ? \Locale::getDefault() : 'en'; $this->resources[$locale][] = [$format, $resource, $domain]; if (\in_array($locale, $this->fallbackLocales)) { $this->catalogues = []; } else { unset($this->catalogues[$locale]); } } /** * {@inheritdoc} */ public function setLocale(string $locale) { $this->assertValidLocale($locale); $this->locale = $locale; } /** * {@inheritdoc} */ public function getLocale(): string { return $this->locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en'); } /** * Sets the fallback locales. * * @param string[] $locales * * @throws InvalidArgumentException If a locale contains invalid characters */ public function setFallbackLocales(array $locales) { // needed as the fallback locales are linked to the already loaded catalogues $this->catalogues = []; foreach ($locales as $locale) { $this->assertValidLocale($locale); } $this->fallbackLocales = $this->cacheVary['fallback_locales'] = $locales; } /** * Gets the fallback locales. * * @internal */ public function getFallbackLocales(): array { return $this->fallbackLocales; } /** * {@inheritdoc} */ public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null): string { if (null === $id || '' === $id) { return ''; } if (null === $domain) { $domain = 'messages'; } $catalogue = $this->getCatalogue($locale); $locale = $catalogue->getLocale(); while (!$catalogue->defines($id, $domain)) { if ($cat = $catalogue->getFallbackCatalogue()) { $catalogue = $cat; $locale = $catalogue->getLocale(); } else { break; } } $len = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX); if ($this->hasIntlFormatter && ($catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX) || (\strlen($domain) > $len && 0 === substr_compare($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX, -$len, $len))) ) { return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, $parameters); } return $this->formatter->format($catalogue->get($id, $domain), $locale, $parameters); } /** * {@inheritdoc} */ public function getCatalogue(string $locale = null): MessageCatalogueInterface { if (!$locale) { $locale = $this->getLocale(); } else { $this->assertValidLocale($locale); } if (!isset($this->catalogues[$locale])) { $this->loadCatalogue($locale); } return $this->catalogues[$locale]; } /** * {@inheritdoc} */ public function getCatalogues(): array { return array_values($this->catalogues); } /** * Gets the loaders. * * @return LoaderInterface[] */ protected function getLoaders(): array { return $this->loaders; } protected function loadCatalogue(string $locale) { if (null === $this->cacheDir) { $this->initializeCatalogue($locale); } else { $this->initializeCacheCatalogue($locale); } } protected function initializeCatalogue(string $locale) { $this->assertValidLocale($locale); try { $this->doLoadCatalogue($locale); } catch (NotFoundResourceException $e) { if (!$this->computeFallbackLocales($locale)) { throw $e; } } $this->loadFallbackCatalogues($locale); } private function initializeCacheCatalogue(string $locale): void { if (isset($this->catalogues[$locale])) { /* Catalogue already initialized. */ return; } $this->assertValidLocale($locale); $cache = $this->getConfigCacheFactory()->cache($this->getCatalogueCachePath($locale), function (ConfigCacheInterface $cache) use ($locale) { $this->dumpCatalogue($locale, $cache); } ); if (isset($this->catalogues[$locale])) { /* Catalogue has been initialized as it was written out to cache. */ return; } /* Read catalogue from cache. */ $this->catalogues[$locale] = include $cache->getPath(); } private function dumpCatalogue(string $locale, ConfigCacheInterface $cache): void { $this->initializeCatalogue($locale); $fallbackContent = $this->getFallbackContent($this->catalogues[$locale]); $content = sprintf(<<<EOF <?php use Symfony\Component\Translation\MessageCatalogue; \$catalogue = new MessageCatalogue('%s', %s); %s return \$catalogue; EOF , $locale, var_export($this->getAllMessages($this->catalogues[$locale]), true), $fallbackContent ); $cache->write($content, $this->catalogues[$locale]->getResources()); } private function getFallbackContent(MessageCatalogue $catalogue): string { $fallbackContent = ''; $current = ''; $replacementPattern = '/[^a-z0-9_]/i'; $fallbackCatalogue = $catalogue->getFallbackCatalogue(); while ($fallbackCatalogue) { $fallback = $fallbackCatalogue->getLocale(); $fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback)); $currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current)); $fallbackContent .= sprintf(<<<'EOF' $catalogue%s = new MessageCatalogue('%s', %s); $catalogue%s->addFallbackCatalogue($catalogue%s); EOF , $fallbackSuffix, $fallback, var_export($this->getAllMessages($fallbackCatalogue), true), $currentSuffix, $fallbackSuffix ); $current = $fallbackCatalogue->getLocale(); $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue(); } return $fallbackContent; } private function getCatalogueCachePath(string $locale): string { return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256', serialize($this->cacheVary), true)), 0, 7), '/', '_').'.php'; } /** * @internal */ protected function doLoadCatalogue(string $locale): void { $this->catalogues[$locale] = new MessageCatalogue($locale); if (isset($this->resources[$locale])) { foreach ($this->resources[$locale] as $resource) { if (!isset($this->loaders[$resource[0]])) { if (\is_string($resource[1])) { throw new RuntimeException(sprintf('No loader is registered for the "%s" format when loading the "%s" resource.', $resource[0], $resource[1])); } throw new RuntimeException(sprintf('No loader is registered for the "%s" format.', $resource[0])); } $this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2])); } } } private function loadFallbackCatalogues(string $locale): void { $current = $this->catalogues[$locale]; foreach ($this->computeFallbackLocales($locale) as $fallback) { if (!isset($this->catalogues[$fallback])) { $this->initializeCatalogue($fallback); } $fallbackCatalogue = new MessageCatalogue($fallback, $this->getAllMessages($this->catalogues[$fallback])); foreach ($this->catalogues[$fallback]->getResources() as $resource) { $fallbackCatalogue->addResource($resource); } $current->addFallbackCatalogue($fallbackCatalogue); $current = $fallbackCatalogue; } } protected function computeFallbackLocales(string $locale) { $this->parentLocales ??= json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true); $originLocale = $locale; $locales = []; while ($locale) { $parent = $this->parentLocales[$locale] ?? null; if ($parent) { $locale = 'root' !== $parent ? $parent : null; } elseif (\function_exists('locale_parse')) { $localeSubTags = locale_parse($locale); $locale = null; if (1 < \count($localeSubTags)) { array_pop($localeSubTags); $locale = locale_compose($localeSubTags) ?: null; } } elseif ($i = strrpos($locale, '_') ?: strrpos($locale, '-')) { $locale = substr($locale, 0, $i); } else { $locale = null; } if (null !== $locale) { $locales[] = $locale; } } foreach ($this->fallbackLocales as $fallback) { if ($fallback === $originLocale) { continue; } $locales[] = $fallback; } return array_unique($locales); } /** * Asserts that the locale is valid, throws an Exception if not. * * @throws InvalidArgumentException If the locale contains invalid characters */ protected function assertValidLocale(string $locale) { if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) { throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale)); } } /** * Provides the ConfigCache factory implementation, falling back to a * default implementation if necessary. */ private function getConfigCacheFactory(): ConfigCacheFactoryInterface { $this->configCacheFactory ??= new ConfigCacheFactory($this->debug); return $this->configCacheFactory; } private function getAllMessages(MessageCatalogueInterface $catalogue): array { $allMessages = []; foreach ($catalogue->all() as $domain => $messages) { if ($intlMessages = $catalogue->all($domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) { $allMessages[$domain.MessageCatalogue::INTL_DOMAIN_SUFFIX] = $intlMessages; $messages = array_diff_key($messages, $intlMessages); } if ($messages) { $allMessages[$domain] = $messages; } } return $allMessages; } } translation/composer.json 0000644 00000003457 15025017654 0011642 0 ustar 00 { "name": "symfony/translation", "type": "library", "description": "Provides tools to internationalize your application", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.0.2", "symfony/polyfill-mbstring": "~1.0", "symfony/translation-contracts": "^2.3|^3.0" }, "require-dev": { "symfony/config": "^5.4|^6.0", "symfony/console": "^5.4|^6.0", "symfony/dependency-injection": "^5.4|^6.0", "symfony/http-client-contracts": "^1.1|^2.0|^3.0", "symfony/http-kernel": "^5.4|^6.0", "symfony/intl": "^5.4|^6.0", "symfony/polyfill-intl-icu": "^1.21", "symfony/service-contracts": "^1.1.2|^2|^3", "symfony/yaml": "^5.4|^6.0", "symfony/finder": "^5.4|^6.0", "psr/log": "^1|^2|^3" }, "conflict": { "symfony/config": "<5.4", "symfony/dependency-injection": "<5.4", "symfony/http-kernel": "<5.4", "symfony/twig-bundle": "<5.4", "symfony/yaml": "<5.4", "symfony/console": "<5.4" }, "provide": { "symfony/translation-implementation": "2.3|3.0" }, "suggest": { "symfony/config": "", "symfony/yaml": "", "psr/log-implementation": "To use logging capability in translator" }, "autoload": { "files": [ "Resources/functions.php" ], "psr-4": { "Symfony\\Component\\Translation\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } translation/CHANGELOG.md 0000644 00000014426 15025017654 0010727 0 ustar 00 CHANGELOG ========= 5.4 --- * Add `github` format & autodetection to render errors as annotations when running the XLIFF linter command in a Github Actions environment. * Translation providers are not experimental anymore 5.3 --- * Add `translation:pull` and `translation:push` commands to manage translations with third-party providers * Add `TranslatorBagInterface::getCatalogues` method * Add support to load XLIFF string in `XliffFileLoader` 5.2.0 ----- * added support for calling `trans` with ICU formatted messages * added `PseudoLocalizationTranslator` * added `TranslatableMessage` objects that represent a message that can be translated * added the `t()` function to easily create `TranslatableMessage` objects * Added support for extracting messages from `TranslatableMessage` objects 5.1.0 ----- * added support for `name` attribute on `unit` element from xliff2 to be used as a translation key instead of always the `source` element 5.0.0 ----- * removed support for using `null` as the locale in `Translator` * removed `TranslatorInterface` * removed `MessageSelector` * removed `ChoiceMessageFormatterInterface` * removed `PluralizationRule` * removed `Interval` * removed `transChoice()` methods, use the trans() method instead with a %count% parameter * removed `FileDumper::setBackup()` and `TranslationWriter::disableBackup()` * removed `MessageFormatter::choiceFormat()` * added argument `$filename` to `PhpExtractor::parseTokens()` * removed support for implicit STDIN usage in the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit. 4.4.0 ----- * deprecated support for using `null` as the locale in `Translator` * deprecated accepting STDIN implicitly when using the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit. * Marked the `TranslationDataCollector` class as `@final`. 4.3.0 ----- * Improved Xliff 1.2 loader to load the original file's metadata * Added `TranslatorPathsPass` 4.2.0 ----- * Started using ICU parent locales as fallback locales. * allow using the ICU message format using domains with the "+intl-icu" suffix * deprecated `Translator::transChoice()` in favor of using `Translator::trans()` with a `%count%` parameter * deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface` * deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead * Added `IntlFormatter` and `IntlFormatterInterface` * added support for multiple files and directories in `XliffLintCommand` * Marked `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` as internal 4.1.0 ----- * The `FileDumper::setBackup()` method is deprecated. * The `TranslationWriter::disableBackup()` method is deprecated. * The `XliffFileDumper` will write "name" on the "unit" node when dumping XLIFF 2.0. 4.0.0 ----- * removed the backup feature of the `FileDumper` class * removed `TranslationWriter::writeTranslations()` method * removed support for passing `MessageSelector` instances to the constructor of the `Translator` class 3.4.0 ----- * Added `TranslationDumperPass` * Added `TranslationExtractorPass` * Added `TranslatorPass` * Added `TranslationReader` and `TranslationReaderInterface` * Added `<notes>` section to the Xliff 2.0 dumper. * Improved Xliff 2.0 loader to load `<notes>` section. * Added `TranslationWriterInterface` * Deprecated `TranslationWriter::writeTranslations` in favor of `TranslationWriter::write` * added support for adding custom message formatter and decoupling the default one. * Added `PhpExtractor` * Added `PhpStringTokenParser` 3.2.0 ----- * Added support for escaping `|` in plural translations with double pipe. 3.1.0 ----- * Deprecated the backup feature of the file dumper classes. 3.0.0 ----- * removed `FileDumper::format()` method. * Changed the visibility of the locale property in `Translator` from protected to private. 2.8.0 ----- * deprecated FileDumper::format(), overwrite FileDumper::formatCatalogue() instead. * deprecated Translator::getMessages(), rely on TranslatorBagInterface::getCatalogue() instead. * added `FileDumper::formatCatalogue` which allows format the catalogue without dumping it into file. * added option `json_encoding` to JsonFileDumper * added options `as_tree`, `inline` to YamlFileDumper * added support for XLIFF 2.0. * added support for XLIFF target and tool attributes. * added message parameters to DataCollectorTranslator. * [DEPRECATION] The `DiffOperation` class has been deprecated and will be removed in Symfony 3.0, since its operation has nothing to do with 'diff', so the class name is misleading. The `TargetOperation` class should be used for this use-case instead. 2.7.0 ----- * added DataCollectorTranslator for collecting the translated messages. 2.6.0 ----- * added possibility to cache catalogues * added TranslatorBagInterface * added LoggingTranslator * added Translator::getMessages() for retrieving the message catalogue as an array 2.5.0 ----- * added relative file path template to the file dumpers * added optional backup to the file dumpers * changed IcuResFileDumper to extend FileDumper 2.3.0 ----- * added classes to make operations on catalogues (like making a diff or a merge on 2 catalogues) * added Translator::getFallbackLocales() * deprecated Translator::setFallbackLocale() in favor of the new Translator::setFallbackLocales() method 2.2.0 ----- * QtTranslationsLoader class renamed to QtFileLoader. QtTranslationsLoader is deprecated and will be removed in 2.3. * [BC BREAK] uniformized the exception thrown by the load() method when an error occurs. The load() method now throws Symfony\Component\Translation\Exception\NotFoundResourceException when a resource cannot be found and Symfony\Component\Translation\Exception\InvalidResourceException when a resource is invalid. * changed the exception class thrown by some load() methods from \RuntimeException to \InvalidArgumentException (IcuDatFileLoader, IcuResFileLoader and QtFileLoader) 2.1.0 ----- * added support for more than one fallback locale * added support for extracting translation messages from templates (Twig and PHP) * added dumpers for translation catalogs * added support for QT, gettext, and ResourceBundles translation/Catalogue/MergeOperation.php 0000644 00000004131 15025017654 0014443 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Merge operation between two catalogues as follows: * all = source ∪ target = {x: x ∈ source ∨ x ∈ target} * new = all ∖ source = {x: x ∈ target ∧ x ∉ source} * obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ source ∧ x ∉ target} = ∅ * Basically, the result contains messages from both catalogues. * * @author Jean-François Simon <contact@jfsimon.fr> */ class MergeOperation extends AbstractOperation { /** * {@inheritdoc} */ protected function processDomain(string $domain) { $this->messages[$domain] = [ 'all' => [], 'new' => [], 'obsolete' => [], ]; $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; foreach ($this->source->all($domain) as $id => $message) { $this->messages[$domain]['all'][$id] = $message; $d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } foreach ($this->target->all($domain) as $id => $message) { if (!$this->source->has($id, $domain)) { $this->messages[$domain]['all'][$id] = $message; $this->messages[$domain]['new'][$id] = $message; $d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } } } } translation/Catalogue/TargetOperation.php 0000644 00000006156 15025017654 0014643 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Target operation between two catalogues: * intersection = source ∩ target = {x: x ∈ source ∧ x ∈ target} * all = intersection ∪ (target ∖ intersection) = target * new = all ∖ source = {x: x ∈ target ∧ x ∉ source} * obsolete = source ∖ all = source ∖ target = {x: x ∈ source ∧ x ∉ target} * Basically, the result contains messages from the target catalogue. * * @author Michael Lee <michael.lee@zerustech.com> */ class TargetOperation extends AbstractOperation { /** * {@inheritdoc} */ protected function processDomain(string $domain) { $this->messages[$domain] = [ 'all' => [], 'new' => [], 'obsolete' => [], ]; $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; // For 'all' messages, the code can't be simplified as ``$this->messages[$domain]['all'] = $target->all($domain);``, // because doing so will drop messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback} // // For 'new' messages, the code can't be simplified as ``array_diff_assoc($this->target->all($domain), $this->source->all($domain));`` // because doing so will not exclude messages like {x: x ∈ target ∧ x ∉ source.all ∧ x ∈ source.fallback} // // For 'obsolete' messages, the code can't be simplified as ``array_diff_assoc($this->source->all($domain), $this->target->all($domain))`` // because doing so will not exclude messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback} foreach ($this->source->all($domain) as $id => $message) { if ($this->target->has($id, $domain)) { $this->messages[$domain]['all'][$id] = $message; $d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } else { $this->messages[$domain]['obsolete'][$id] = $message; } } foreach ($this->target->all($domain) as $id => $message) { if (!$this->source->has($id, $domain)) { $this->messages[$domain]['all'][$id] = $message; $this->messages[$domain]['new'][$id] = $message; $d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } } } } translation/Catalogue/AbstractOperation.php 0000644 00000014204 15025017654 0015151 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Base catalogues binary operation class. * * A catalogue binary operation performs operation on * source (the left argument) and target (the right argument) catalogues. * * @author Jean-François Simon <contact@jfsimon.fr> */ abstract class AbstractOperation implements OperationInterface { public const OBSOLETE_BATCH = 'obsolete'; public const NEW_BATCH = 'new'; public const ALL_BATCH = 'all'; protected $source; protected $target; protected $result; /** * @var array|null The domains affected by this operation */ private $domains; /** * This array stores 'all', 'new' and 'obsolete' messages for all valid domains. * * The data structure of this array is as follows: * * [ * 'domain 1' => [ * 'all' => [...], * 'new' => [...], * 'obsolete' => [...] * ], * 'domain 2' => [ * 'all' => [...], * 'new' => [...], * 'obsolete' => [...] * ], * ... * ] * * @var array The array that stores 'all', 'new' and 'obsolete' messages */ protected $messages; /** * @throws LogicException */ public function __construct(MessageCatalogueInterface $source, MessageCatalogueInterface $target) { if ($source->getLocale() !== $target->getLocale()) { throw new LogicException('Operated catalogues must belong to the same locale.'); } $this->source = $source; $this->target = $target; $this->result = new MessageCatalogue($source->getLocale()); $this->messages = []; } /** * {@inheritdoc} */ public function getDomains(): array { if (null === $this->domains) { $domains = []; foreach ([$this->source, $this->target] as $catalogue) { foreach ($catalogue->getDomains() as $domain) { $domains[$domain] = $domain; if ($catalogue->all($domainIcu = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX)) { $domains[$domainIcu] = $domainIcu; } } } $this->domains = array_values($domains); } return $this->domains; } /** * {@inheritdoc} */ public function getMessages(string $domain): array { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } if (!isset($this->messages[$domain][self::ALL_BATCH])) { $this->processDomain($domain); } return $this->messages[$domain][self::ALL_BATCH]; } /** * {@inheritdoc} */ public function getNewMessages(string $domain): array { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } if (!isset($this->messages[$domain][self::NEW_BATCH])) { $this->processDomain($domain); } return $this->messages[$domain][self::NEW_BATCH]; } /** * {@inheritdoc} */ public function getObsoleteMessages(string $domain): array { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } if (!isset($this->messages[$domain][self::OBSOLETE_BATCH])) { $this->processDomain($domain); } return $this->messages[$domain][self::OBSOLETE_BATCH]; } /** * {@inheritdoc} */ public function getResult(): MessageCatalogueInterface { foreach ($this->getDomains() as $domain) { if (!isset($this->messages[$domain])) { $this->processDomain($domain); } } return $this->result; } /** * @param self::*_BATCH $batch */ public function moveMessagesToIntlDomainsIfPossible(string $batch = self::ALL_BATCH): void { // If MessageFormatter class does not exists, intl domains are not supported. if (!class_exists(\MessageFormatter::class)) { return; } foreach ($this->getDomains() as $domain) { $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; switch ($batch) { case self::OBSOLETE_BATCH: $messages = $this->getObsoleteMessages($domain); break; case self::NEW_BATCH: $messages = $this->getNewMessages($domain); break; case self::ALL_BATCH: $messages = $this->getMessages($domain); break; default: throw new \InvalidArgumentException(sprintf('$batch argument must be one of ["%s", "%s", "%s"].', self::ALL_BATCH, self::NEW_BATCH, self::OBSOLETE_BATCH)); } if (!$messages || (!$this->source->all($intlDomain) && $this->source->all($domain))) { continue; } $result = $this->getResult(); $allIntlMessages = $result->all($intlDomain); $currentMessages = array_diff_key($messages, $result->all($domain)); $result->replace($currentMessages, $domain); $result->replace($allIntlMessages + $messages, $intlDomain); } } /** * Performs operation on source and target catalogues for the given domain and * stores the results. * * @param string $domain The domain which the operation will be performed for */ abstract protected function processDomain(string $domain); } translation/Catalogue/OperationInterface.php 0000644 00000003501 15025017654 0015304 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Represents an operation on catalogue(s). * * An instance of this interface performs an operation on one or more catalogues and * stores intermediate and final results of the operation. * * The first catalogue in its argument(s) is called the 'source catalogue' or 'source' and * the following results are stored: * * Messages: also called 'all', are valid messages for the given domain after the operation is performed. * * New Messages: also called 'new' (new = all ∖ source = {x: x ∈ all ∧ x ∉ source}). * * Obsolete Messages: also called 'obsolete' (obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ all}). * * Result: also called 'result', is the resulting catalogue for the given domain that holds the same messages as 'all'. * * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com> */ interface OperationInterface { /** * Returns domains affected by operation. */ public function getDomains(): array; /** * Returns all valid messages ('all') after operation. */ public function getMessages(string $domain): array; /** * Returns new messages ('new') after operation. */ public function getNewMessages(string $domain): array; /** * Returns obsolete messages ('obsolete') after operation. */ public function getObsoleteMessages(string $domain): array; /** * Returns resulting catalogue ('result'). */ public function getResult(): MessageCatalogueInterface; } translation/Util/ArrayConverter.php 0000644 00000005402 15025017654 0013504 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Util; /** * ArrayConverter generates tree like structure from a message catalogue. * e.g. this * 'foo.bar1' => 'test1', * 'foo.bar2' => 'test2' * converts to follows: * foo: * bar1: test1 * bar2: test2. * * @author Gennady Telegin <gtelegin@gmail.com> */ class ArrayConverter { /** * Converts linear messages array to tree-like array. * For example this array('foo.bar' => 'value') will be converted to ['foo' => ['bar' => 'value']]. * * @param array $messages Linear messages array */ public static function expandToTree(array $messages): array { $tree = []; foreach ($messages as $id => $value) { $referenceToElement = &self::getElementByPath($tree, explode('.', $id)); $referenceToElement = $value; unset($referenceToElement); } return $tree; } private static function &getElementByPath(array &$tree, array $parts) { $elem = &$tree; $parentOfElem = null; foreach ($parts as $i => $part) { if (isset($elem[$part]) && \is_string($elem[$part])) { /* Process next case: * 'foo': 'test1', * 'foo.bar': 'test2' * * $tree['foo'] was string before we found array {bar: test2}. * Treat new element as string too, e.g. add $tree['foo.bar'] = 'test2'; */ $elem = &$elem[implode('.', \array_slice($parts, $i))]; break; } $parentOfElem = &$elem; $elem = &$elem[$part]; } if ($elem && \is_array($elem) && $parentOfElem) { /* Process next case: * 'foo.bar': 'test1' * 'foo': 'test2' * * $tree['foo'] was array = {bar: 'test1'} before we found string constant `foo`. * Cancel treating $tree['foo'] as array and cancel back it expansion, * e.g. make it $tree['foo.bar'] = 'test1' again. */ self::cancelExpand($parentOfElem, $part, $elem); } return $elem; } private static function cancelExpand(array &$tree, string $prefix, array $node) { $prefix .= '.'; foreach ($node as $id => $value) { if (\is_string($value)) { $tree[$prefix.$id] = $value; } else { self::cancelExpand($tree, $prefix.$id, $value); } } } } translation/Util/XliffUtils.php 0000644 00000014465 15025017654 0012640 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Util; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\InvalidResourceException; /** * Provides some utility methods for XLIFF translation files, such as validating * their contents according to the XSD schema. * * @author Fabien Potencier <fabien@symfony.com> */ class XliffUtils { /** * Gets xliff file version based on the root "version" attribute. * * Defaults to 1.2 for backwards compatibility. * * @throws InvalidArgumentException */ public static function getVersionNumber(\DOMDocument $dom): string { /** @var \DOMNode $xliff */ foreach ($dom->getElementsByTagName('xliff') as $xliff) { $version = $xliff->attributes->getNamedItem('version'); if ($version) { return $version->nodeValue; } $namespace = $xliff->attributes->getNamedItem('xmlns'); if ($namespace) { if (0 !== substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34)) { throw new InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s".', $namespace)); } return substr($namespace, 34); } } // Falls back to v1.2 return '1.2'; } /** * Validates and parses the given file into a DOMDocument. * * @throws InvalidResourceException */ public static function validateSchema(\DOMDocument $dom): array { $xliffVersion = static::getVersionNumber($dom); $internalErrors = libxml_use_internal_errors(true); if ($shouldEnable = self::shouldEnableEntityLoader()) { $disableEntities = libxml_disable_entity_loader(false); } try { $isValid = @$dom->schemaValidateSource(self::getSchema($xliffVersion)); if (!$isValid) { return self::getXmlErrors($internalErrors); } } finally { if ($shouldEnable) { libxml_disable_entity_loader($disableEntities); } } $dom->normalizeDocument(); libxml_clear_errors(); libxml_use_internal_errors($internalErrors); return []; } private static function shouldEnableEntityLoader(): bool { static $dom, $schema; if (null === $dom) { $dom = new \DOMDocument(); $dom->loadXML('<?xml version="1.0"?><test/>'); $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); register_shutdown_function(static function () use ($tmpfile) { @unlink($tmpfile); }); $schema = '<?xml version="1.0" encoding="utf-8"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:include schemaLocation="file:///'.str_replace('\\', '/', $tmpfile).'" /> </xsd:schema>'; file_put_contents($tmpfile, '<?xml version="1.0" encoding="utf-8"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:element name="test" type="testType" /> <xsd:complexType name="testType"/> </xsd:schema>'); } return !@$dom->schemaValidateSource($schema); } public static function getErrorsAsString(array $xmlErrors): string { $errorsAsString = ''; foreach ($xmlErrors as $error) { $errorsAsString .= sprintf("[%s %s] %s (in %s - line %d, column %d)\n", \LIBXML_ERR_WARNING === $error['level'] ? 'WARNING' : 'ERROR', $error['code'], $error['message'], $error['file'], $error['line'], $error['column'] ); } return $errorsAsString; } private static function getSchema(string $xliffVersion): string { if ('1.2' === $xliffVersion) { $schemaSource = file_get_contents(__DIR__.'/../Resources/schemas/xliff-core-1.2-strict.xsd'); $xmlUri = 'http://www.w3.org/2001/xml.xsd'; } elseif ('2.0' === $xliffVersion) { $schemaSource = file_get_contents(__DIR__.'/../Resources/schemas/xliff-core-2.0.xsd'); $xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd'; } else { throw new InvalidArgumentException(sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion)); } return self::fixXmlLocation($schemaSource, $xmlUri); } /** * Internally changes the URI of a dependent xsd to be loaded locally. */ private static function fixXmlLocation(string $schemaSource, string $xmlUri): string { $newPath = str_replace('\\', '/', __DIR__).'/../Resources/schemas/xml.xsd'; $parts = explode('/', $newPath); $locationstart = 'file:///'; if (0 === stripos($newPath, 'phar://')) { $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); if ($tmpfile) { copy($newPath, $tmpfile); $parts = explode('/', str_replace('\\', '/', $tmpfile)); } else { array_shift($parts); $locationstart = 'phar:///'; } } $drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; $newPath = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts)); return str_replace($xmlUri, $newPath, $schemaSource); } /** * Returns the XML errors of the internal XML parser. */ private static function getXmlErrors(bool $internalErrors): array { $errors = []; foreach (libxml_get_errors() as $error) { $errors[] = [ 'level' => \LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', 'code' => $error->code, 'message' => trim($error->message), 'file' => $error->file ?: 'n/a', 'line' => $error->line, 'column' => $error->column, ]; } libxml_clear_errors(); libxml_use_internal_errors($internalErrors); return $errors; } } translation/Exception/LogicException.php 0000644 00000000747 15025017654 0014502 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Base LogicException for Translation component. * * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> */ class LogicException extends \LogicException implements ExceptionInterface { } translation/Exception/InvalidResourceException.php 0000644 00000000755 15025017654 0016542 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Thrown when a resource cannot be loaded. * * @author Fabien Potencier <fabien@symfony.com> */ class InvalidResourceException extends \InvalidArgumentException implements ExceptionInterface { } translation/Exception/UnsupportedSchemeException.php 0000644 00000003550 15025017654 0017115 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; use Symfony\Component\Translation\Bridge; use Symfony\Component\Translation\Provider\Dsn; class UnsupportedSchemeException extends LogicException { private const SCHEME_TO_PACKAGE_MAP = [ 'crowdin' => [ 'class' => Bridge\Crowdin\CrowdinProviderFactory::class, 'package' => 'symfony/crowdin-translation-provider', ], 'loco' => [ 'class' => Bridge\Loco\LocoProviderFactory::class, 'package' => 'symfony/loco-translation-provider', ], 'lokalise' => [ 'class' => Bridge\Lokalise\LokaliseProviderFactory::class, 'package' => 'symfony/lokalise-translation-provider', ], ]; public function __construct(Dsn $dsn, string $name = null, array $supported = []) { $provider = $dsn->getScheme(); if (false !== $pos = strpos($provider, '+')) { $provider = substr($provider, 0, $pos); } $package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null; if ($package && !class_exists($package['class'])) { parent::__construct(sprintf('Unable to synchronize translations via "%s" as the provider is not installed; try running "composer require %s".', $provider, $package['package'])); return; } $message = sprintf('The "%s" scheme is not supported', $dsn->getScheme()); if ($name && $supported) { $message .= sprintf('; supported schemes for translation provider "%s" are: "%s"', $name, implode('", "', $supported)); } parent::__construct($message.'.'); } } translation/Exception/ProviderExceptionInterface.php 0000644 00000001057 15025017654 0017053 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * @author Fabien Potencier <fabien@symfony.com> */ interface ProviderExceptionInterface extends ExceptionInterface { /* * Returns debug info coming from the Symfony\Contracts\HttpClient\ResponseInterface */ public function getDebug(): string; } translation/Exception/InvalidArgumentException.php 0000644 00000001011 15025017654 0016517 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Base InvalidArgumentException for the Translation component. * * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } translation/Exception/NotFoundResourceException.php 0000644 00000000754 15025017654 0016707 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Thrown when a resource does not exist. * * @author Fabien Potencier <fabien@symfony.com> */ class NotFoundResourceException extends \InvalidArgumentException implements ExceptionInterface { } translation/Exception/ProviderException.php 0000644 00000001760 15025017654 0015233 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Fabien Potencier <fabien@symfony.com> */ class ProviderException extends RuntimeException implements ProviderExceptionInterface { private $response; private string $debug; public function __construct(string $message, ResponseInterface $response, int $code = 0, \Exception $previous = null) { $this->response = $response; $this->debug = $response->getInfo('debug') ?? ''; parent::__construct($message, $code, $previous); } public function getResponse(): ResponseInterface { return $this->response; } public function getDebug(): string { return $this->debug; } } translation/Exception/ExceptionInterface.php 0000644 00000000725 15025017654 0015341 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Exception interface for all exceptions thrown by the component. * * @author Fabien Potencier <fabien@symfony.com> */ interface ExceptionInterface extends \Throwable { } translation/Exception/IncompleteDsnException.php 0000644 00000001164 15025017654 0016203 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; class IncompleteDsnException extends InvalidArgumentException { public function __construct(string $message, string $dsn = null, \Throwable $previous = null) { if ($dsn) { $message = sprintf('Invalid "%s" provider DSN: ', $dsn).$message; } parent::__construct($message, 0, $previous); } } translation/Exception/MissingRequiredOptionException.php 0000644 00000001234 15025017654 0017740 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * @author Oskar Stark <oskarstark@googlemail.com> */ class MissingRequiredOptionException extends IncompleteDsnException { public function __construct(string $option, string $dsn = null, \Throwable $previous = null) { $message = sprintf('The option "%s" is required but missing.', $option); parent::__construct($message, $dsn, $previous); } } translation/Exception/RuntimeException.php 0000644 00000000761 15025017654 0015064 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Base RuntimeException for the Translation component. * * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } translation/Command/TranslationPullCommand.php 0000644 00000016743 15025017654 0015643 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Translation\Catalogue\TargetOperation; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Provider\TranslationProviderCollection; use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\Writer\TranslationWriterInterface; /** * @author Mathieu Santostefano <msantostefano@protonmail.com> */ #[AsCommand(name: 'translation:pull', description: 'Pull translations from a given provider.')] final class TranslationPullCommand extends Command { use TranslationTrait; private $providerCollection; private $writer; private $reader; private string $defaultLocale; private array $transPaths; private array $enabledLocales; public function __construct(TranslationProviderCollection $providerCollection, TranslationWriterInterface $writer, TranslationReaderInterface $reader, string $defaultLocale, array $transPaths = [], array $enabledLocales = []) { $this->providerCollection = $providerCollection; $this->writer = $writer; $this->reader = $reader; $this->defaultLocale = $defaultLocale; $this->transPaths = $transPaths; $this->enabledLocales = $enabledLocales; parent::__construct(); } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestArgumentValuesFor('provider')) { $suggestions->suggestValues($this->providerCollection->keys()); return; } if ($input->mustSuggestOptionValuesFor('domains')) { $provider = $this->providerCollection->get($input->getArgument('provider')); if ($provider && method_exists($provider, 'getDomains')) { $domains = $provider->getDomains(); $suggestions->suggestValues($domains); } return; } if ($input->mustSuggestOptionValuesFor('locales')) { $suggestions->suggestValues($this->enabledLocales); return; } if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues(['php', 'xlf', 'xlf12', 'xlf20', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'json', 'ini', 'res']); } } /** * {@inheritdoc} */ protected function configure() { $keys = $this->providerCollection->keys(); $defaultProvider = 1 === \count($keys) ? $keys[0] : null; $this ->setDefinition([ new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider), new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'), new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'), new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'), new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'), new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'), ]) ->setHelp(<<<'EOF' The <info>%command.name%</> command pulls translations from the given provider. Only new translations are pulled, existing ones are not overwritten. You can overwrite existing translations (and remove the missing ones on local side) by using the <comment>--force</> flag: <info>php %command.full_name% --force provider</> Full example: <info>php %command.full_name% provider --force --domains=messages --domains=validators --locales=en</> This command pulls all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale. Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case. Local translations for others domains and locales are ignored. EOF ) ; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $provider = $this->providerCollection->get($input->getArgument('provider')); $force = $input->getOption('force'); $intlIcu = $input->getOption('intl-icu'); $locales = $input->getOption('locales') ?: $this->enabledLocales; $domains = $input->getOption('domains'); $format = $input->getOption('format'); $xliffVersion = '1.2'; if ($intlIcu && !$force) { $io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.'); } switch ($format) { case 'xlf20': $xliffVersion = '2.0'; // no break case 'xlf12': $format = 'xlf'; } $writeOptions = [ 'path' => end($this->transPaths), 'xliff_version' => $xliffVersion, 'default_locale' => $this->defaultLocale, ]; if (!$domains) { $domains = $provider->getDomains(); } $providerTranslations = $provider->read($domains, $locales); if ($force) { foreach ($providerTranslations->getCatalogues() as $catalogue) { $operation = new TargetOperation(new MessageCatalogue($catalogue->getLocale()), $catalogue); if ($intlIcu) { $operation->moveMessagesToIntlDomainsIfPossible(); } $this->writer->write($operation->getResult(), $format, $writeOptions); } $io->success(sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); return 0; } $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); // Append pulled translations to local ones. $localTranslations->addBag($providerTranslations->diff($localTranslations)); foreach ($localTranslations->getCatalogues() as $catalogue) { $this->writer->write($catalogue, $format, $writeOptions); } $io->success(sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); return 0; } } translation/Command/TranslationPushCommand.php 0000644 00000017011 15025017654 0015633 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Translation\Provider\FilteringProvider; use Symfony\Component\Translation\Provider\TranslationProviderCollection; use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\TranslatorBag; /** * @author Mathieu Santostefano <msantostefano@protonmail.com> */ #[AsCommand(name: 'translation:push', description: 'Push translations to a given provider.')] final class TranslationPushCommand extends Command { use TranslationTrait; private $providers; private $reader; private array $transPaths; private array $enabledLocales; public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = []) { $this->providers = $providers; $this->reader = $reader; $this->transPaths = $transPaths; $this->enabledLocales = $enabledLocales; parent::__construct(); } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestArgumentValuesFor('provider')) { $suggestions->suggestValues($this->providers->keys()); return; } if ($input->mustSuggestOptionValuesFor('domains')) { $provider = $this->providers->get($input->getArgument('provider')); if ($provider && method_exists($provider, 'getDomains')) { $domains = $provider->getDomains(); $suggestions->suggestValues($domains); } return; } if ($input->mustSuggestOptionValuesFor('locales')) { $suggestions->suggestValues($this->enabledLocales); } } /** * {@inheritdoc} */ protected function configure() { $keys = $this->providers->keys(); $defaultProvider = 1 === \count($keys) ? $keys[0] : null; $this ->setDefinition([ new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider), new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'), new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'), new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'), new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales), ]) ->setHelp(<<<'EOF' The <info>%command.name%</> command pushes translations to the given provider. Only new translations are pushed, existing ones are not overwritten. You can overwrite existing translations by using the <comment>--force</> flag: <info>php %command.full_name% --force provider</> You can delete provider translations which are not present locally by using the <comment>--delete-missing</> flag: <info>php %command.full_name% --delete-missing provider</> Full example: <info>php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en</> This command pushes all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale. Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case. Provider translations for others domains and locales are ignored. EOF ) ; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output): int { $provider = $this->providers->get($input->getArgument('provider')); if (!$this->enabledLocales) { throw new InvalidArgumentException(sprintf('You must define "framework.translator.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME))); } $io = new SymfonyStyle($input, $output); $domains = $input->getOption('domains'); $locales = $input->getOption('locales'); $force = $input->getOption('force'); $deleteMissing = $input->getOption('delete-missing'); if (!$domains && $provider instanceof FilteringProvider) { $domains = $provider->getDomains(); } // Reading local translations must be done after retrieving the domains from the provider // in order to manage only translations from configured domains $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); if (!$domains) { $domains = $this->getDomainsFromTranslatorBag($localTranslations); } if (!$deleteMissing && $force) { $provider->write($localTranslations); $io->success(sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); return 0; } $providerTranslations = $provider->read($domains, $locales); if ($deleteMissing) { $provider->delete($providerTranslations->diff($localTranslations)); $io->success(sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); // Read provider translations again, after missing translations deletion, // to avoid push freshly deleted translations. $providerTranslations = $provider->read($domains, $locales); } $translationsToWrite = $localTranslations->diff($providerTranslations); if ($force) { $translationsToWrite->addBag($localTranslations->intersect($providerTranslations)); } $provider->write($translationsToWrite); $io->success(sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); return 0; } private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array { $domains = []; foreach ($translatorBag->getCatalogues() as $catalogue) { $domains += $catalogue->getDomains(); } return array_unique($domains); } } translation/Command/XliffLintCommand.php 0000644 00000025167 15025017654 0014407 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\CI\GithubActionReporter; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Util\XliffUtils; /** * Validates XLIFF files syntax and outputs encountered errors. * * @author Grégoire Pineau <lyrixx@lyrixx.info> * @author Robin Chalas <robin.chalas@gmail.com> * @author Javier Eguiluz <javier.eguiluz@gmail.com> */ #[AsCommand(name: 'lint:xliff', description: 'Lint an XLIFF file and outputs encountered errors')] class XliffLintCommand extends Command { private string $format; private bool $displayCorrectFiles; private ?\Closure $directoryIteratorProvider; private ?\Closure $isReadableProvider; private bool $requireStrictFileNames; public function __construct(string $name = null, callable $directoryIteratorProvider = null, callable $isReadableProvider = null, bool $requireStrictFileNames = true) { parent::__construct($name); $this->directoryIteratorProvider = null === $directoryIteratorProvider || $directoryIteratorProvider instanceof \Closure ? $directoryIteratorProvider : \Closure::fromCallable($directoryIteratorProvider); $this->isReadableProvider = null === $isReadableProvider || $isReadableProvider instanceof \Closure ? $isReadableProvider : \Closure::fromCallable($isReadableProvider); $this->requireStrictFileNames = $requireStrictFileNames; } /** * {@inheritdoc} */ protected function configure() { $this ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format') ->setHelp(<<<EOF The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT the first encountered syntax error. You can validates XLIFF contents passed from STDIN: <info>cat filename | php %command.full_name% -</info> You can also validate the syntax of a file: <info>php %command.full_name% filename</info> Or of a whole directory: <info>php %command.full_name% dirname</info> <info>php %command.full_name% dirname --format=json</info> EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $filenames = (array) $input->getArgument('filename'); $this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt'); $this->displayCorrectFiles = $output->isVerbose(); if (['-'] === $filenames) { return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]); } if (!$filenames) { throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); } $filesInfo = []; foreach ($filenames as $filename) { if (!$this->isReadable($filename)) { throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); } foreach ($this->getFiles($filename) as $file) { $filesInfo[] = $this->validate(file_get_contents($file), $file); } } return $this->display($io, $filesInfo); } private function validate(string $content, string $file = null): array { $errors = []; // Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input if ('' === trim($content)) { return ['file' => $file, 'valid' => true]; } $internal = libxml_use_internal_errors(true); $document = new \DOMDocument(); $document->loadXML($content); if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) { $normalizedLocalePattern = sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/')); // strict file names require translation files to be named '____.locale.xlf' // otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed // also, the regexp matching must be case-insensitive, as defined for 'target-language' values // http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language $expectedFilenamePattern = $this->requireStrictFileNames ? sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern); if (0 === preg_match($expectedFilenamePattern, basename($file))) { $errors[] = [ 'line' => -1, 'column' => -1, 'message' => sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage), ]; } } foreach (XliffUtils::validateSchema($document) as $xmlError) { $errors[] = [ 'line' => $xmlError['line'], 'column' => $xmlError['column'], 'message' => $xmlError['message'], ]; } libxml_clear_errors(); libxml_use_internal_errors($internal); return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors]; } private function display(SymfonyStyle $io, array $files) { switch ($this->format) { case 'txt': return $this->displayTxt($io, $files); case 'json': return $this->displayJson($io, $files); case 'github': return $this->displayTxt($io, $files, true); default: throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format)); } } private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false) { $countFiles = \count($filesInfo); $erroredFiles = 0; $githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($io) : null; foreach ($filesInfo as $info) { if ($info['valid'] && $this->displayCorrectFiles) { $io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); } elseif (!$info['valid']) { ++$erroredFiles; $io->text('<error> ERROR </error>'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); $io->listing(array_map(function ($error) use ($info, $githubReporter) { // general document errors have a '-1' line number $line = -1 === $error['line'] ? null : $error['line']; if ($githubReporter) { $githubReporter->error($error['message'], $info['file'], $line, null !== $line ? $error['column'] : null); } return null === $line ? $error['message'] : sprintf('Line %d, Column %d: %s', $line, $error['column'], $error['message']); }, $info['messages'])); } } if (0 === $erroredFiles) { $io->success(sprintf('All %d XLIFF files contain valid syntax.', $countFiles)); } else { $io->warning(sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles)); } return min($erroredFiles, 1); } private function displayJson(SymfonyStyle $io, array $filesInfo) { $errors = 0; array_walk($filesInfo, function (&$v) use (&$errors) { $v['file'] = (string) $v['file']; if (!$v['valid']) { ++$errors; } }); $io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); return min($errors, 1); } private function getFiles(string $fileOrDirectory) { if (is_file($fileOrDirectory)) { yield new \SplFileInfo($fileOrDirectory); return; } foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) { if (!\in_array($file->getExtension(), ['xlf', 'xliff'])) { continue; } yield $file; } } private function getDirectoryIterator(string $directory) { $default = function ($directory) { return new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), \RecursiveIteratorIterator::LEAVES_ONLY ); }; if (null !== $this->directoryIteratorProvider) { return ($this->directoryIteratorProvider)($directory, $default); } return $default($directory); } private function isReadable(string $fileOrDirectory) { $default = function ($fileOrDirectory) { return is_readable($fileOrDirectory); }; if (null !== $this->isReadableProvider) { return ($this->isReadableProvider)($fileOrDirectory, $default); } return $default($fileOrDirectory); } private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string { foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) { if ('target-language' === $attribute->nodeName) { return $attribute->nodeValue; } } return null; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues(['txt', 'json', 'github']); } } } translation/Command/TranslationTrait.php 0000644 00000004525 15025017654 0014506 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Command; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogueInterface; use Symfony\Component\Translation\TranslatorBag; /** * @internal */ trait TranslationTrait { private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag { $bag = new TranslatorBag(); foreach ($locales as $locale) { $catalogue = new MessageCatalogue($locale); foreach ($transPaths as $path) { $this->reader->read($path, $catalogue); } if ($domains) { foreach ($domains as $domain) { $bag->addCatalogue($this->filterCatalogue($catalogue, $domain)); } } else { $bag->addCatalogue($catalogue); } } return $bag; } private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue { $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); // extract intl-icu messages only $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; if ($intlMessages = $catalogue->all($intlDomain)) { $filteredCatalogue->add($intlMessages, $intlDomain); } // extract all messages and subtract intl-icu messages if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { $filteredCatalogue->add($messages, $domain); } foreach ($catalogue->getResources() as $resource) { $filteredCatalogue->addResource($resource); } if ($metadata = $catalogue->getMetadata('', $intlDomain)) { foreach ($metadata as $k => $v) { $filteredCatalogue->setMetadata($k, $v, $intlDomain); } } if ($metadata = $catalogue->getMetadata('', $domain)) { foreach ($metadata as $k => $v) { $filteredCatalogue->setMetadata($k, $v, $domain); } } return $filteredCatalogue; } } translation/README.md 0000644 00000002670 15025017654 0010373 0 ustar 00 Translation Component ===================== The Translation component provides tools to internationalize your application. Getting Started --------------- ``` $ composer require symfony/translation ``` ```php use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\Loader\ArrayLoader; $translator = new Translator('fr_FR'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', [ 'Hello World!' => 'Bonjour !', ], 'fr_FR'); echo $translator->trans('Hello World!'); // outputs « Bonjour ! » ``` Sponsor ------- The Translation component for Symfony 5.4/6.0 is [backed][1] by: * [Crowdin][2], a cloud-based localization management software helping teams to go global and stay agile. * [Lokalise][3], a continuous localization and translation management platform that integrates into your development workflow so you can ship localized products, faster. Help Symfony by [sponsoring][4] its development! Resources --------- * [Documentation](https://symfony.com/doc/current/translation.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) [1]: https://symfony.com/backers [2]: https://crowdin.com [3]: https://lokalise.com [4]: https://symfony.com/sponsor translation/Test/ProviderTestCase.php 0000644 00000004163 15025017654 0013771 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Test; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\Translation\Dumper\XliffFileDumper; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Provider\ProviderInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * A test case to ease testing a translation provider. * * @author Mathieu Santostefano <msantostefano@protonmail.com> * * @internal */ abstract class ProviderTestCase extends TestCase { protected $client; protected $logger; protected string $defaultLocale; protected $loader; protected $xliffFileDumper; abstract public function createProvider(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint): ProviderInterface; /** * @return iterable<array{0: string, 1: ProviderInterface}> */ abstract public function toStringProvider(): iterable; /** * @dataProvider toStringProvider */ public function testToString(ProviderInterface $provider, string $expected) { $this->assertSame($expected, (string) $provider); } protected function getClient(): MockHttpClient { return $this->client ??= new MockHttpClient(); } protected function getLoader(): LoaderInterface { return $this->loader ??= $this->createMock(LoaderInterface::class); } protected function getLogger(): LoggerInterface { return $this->logger ??= $this->createMock(LoggerInterface::class); } protected function getDefaultLocale(): string { return $this->defaultLocale ??= 'en'; } protected function getXliffFileDumper(): XliffFileDumper { return $this->xliffFileDumper ??= $this->createMock(XliffFileDumper::class); } } translation/Test/ProviderFactoryTestCase.php 0000644 00000007504 15025017654 0015323 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Test; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\Translation\Dumper\XliffFileDumper; use Symfony\Component\Translation\Exception\IncompleteDsnException; use Symfony\Component\Translation\Exception\UnsupportedSchemeException; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Provider\Dsn; use Symfony\Component\Translation\Provider\ProviderFactoryInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * A test case to ease testing a translation provider factory. * * @author Mathieu Santostefano <msantostefano@protonmail.com> * * @internal */ abstract class ProviderFactoryTestCase extends TestCase { protected $client; protected $logger; protected string $defaultLocale; protected $loader; protected $xliffFileDumper; abstract public function createFactory(): ProviderFactoryInterface; /** * @return iterable<array{0: bool, 1: string}> */ abstract public function supportsProvider(): iterable; /** * @return iterable<array{0: string, 1: string, 2: TransportInterface}> */ abstract public function createProvider(): iterable; /** * @return iterable<array{0: string, 1: string|null}> */ public function unsupportedSchemeProvider(): iterable { return []; } /** * @return iterable<array{0: string, 1: string|null}> */ public function incompleteDsnProvider(): iterable { return []; } /** * @dataProvider supportsProvider */ public function testSupports(bool $expected, string $dsn) { $factory = $this->createFactory(); $this->assertSame($expected, $factory->supports(new Dsn($dsn))); } /** * @dataProvider createProvider */ public function testCreate(string $expected, string $dsn) { $factory = $this->createFactory(); $provider = $factory->create(new Dsn($dsn)); $this->assertSame($expected, (string) $provider); } /** * @dataProvider unsupportedSchemeProvider */ public function testUnsupportedSchemeException(string $dsn, string $message = null) { $factory = $this->createFactory(); $dsn = new Dsn($dsn); $this->expectException(UnsupportedSchemeException::class); if (null !== $message) { $this->expectExceptionMessage($message); } $factory->create($dsn); } /** * @dataProvider incompleteDsnProvider */ public function testIncompleteDsnException(string $dsn, string $message = null) { $factory = $this->createFactory(); $dsn = new Dsn($dsn); $this->expectException(IncompleteDsnException::class); if (null !== $message) { $this->expectExceptionMessage($message); } $factory->create($dsn); } protected function getClient(): HttpClientInterface { return $this->client ??= new MockHttpClient(); } protected function getLogger(): LoggerInterface { return $this->logger ??= $this->createMock(LoggerInterface::class); } protected function getDefaultLocale(): string { return $this->defaultLocale ??= 'en'; } protected function getLoader(): LoaderInterface { return $this->loader ??= $this->createMock(LoaderInterface::class); } protected function getXliffFileDumper(): XliffFileDumper { return $this->xliffFileDumper ??= $this->createMock(XliffFileDumper::class); } } translation/Loader/CsvFileLoader.php 0000644 00000003347 15025017654 0013517 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\NotFoundResourceException; /** * CsvFileLoader loads translations from CSV files. * * @author Saša Stamenković <umpirsky@gmail.com> */ class CsvFileLoader extends FileLoader { private string $delimiter = ';'; private string $enclosure = '"'; private string $escape = '\\'; /** * {@inheritdoc} */ protected function loadResource(string $resource): array { $messages = []; try { $file = new \SplFileObject($resource, 'rb'); } catch (\RuntimeException $e) { throw new NotFoundResourceException(sprintf('Error opening file "%s".', $resource), 0, $e); } $file->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY); $file->setCsvControl($this->delimiter, $this->enclosure, $this->escape); foreach ($file as $data) { if (false === $data) { continue; } if ('#' !== substr($data[0], 0, 1) && isset($data[1]) && 2 === \count($data)) { $messages[$data[0]] = $data[1]; } } return $messages; } /** * Sets the delimiter, enclosure, and escape character for CSV. */ public function setCsvControl(string $delimiter = ';', string $enclosure = '"', string $escape = '\\') { $this->delimiter = $delimiter; $this->enclosure = $enclosure; $this->escape = $escape; } } translation/Loader/FileLoader.php 0000644 00000003420 15025017654 0013033 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\MessageCatalogue; /** * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> */ abstract class FileLoader extends ArrayLoader { /** * {@inheritdoc} */ public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } if (!file_exists($resource)) { throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } $messages = $this->loadResource($resource); // empty resource if (null === $messages) { $messages = []; } // not an array if (!\is_array($messages)) { throw new InvalidResourceException(sprintf('Unable to load file "%s".', $resource)); } $catalogue = parent::load($messages, $locale, $domain); if (class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource)); } return $catalogue; } /** * @throws InvalidResourceException if stream content has an invalid format */ abstract protected function loadResource(string $resource): array; } translation/Loader/IniFileLoader.php 0000644 00000001076 15025017654 0013500 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; /** * IniFileLoader loads translations from an ini file. * * @author stealth35 */ class IniFileLoader extends FileLoader { /** * {@inheritdoc} */ protected function loadResource(string $resource): array { return parse_ini_file($resource, true); } } translation/Loader/MoFileLoader.php 0000644 00000010307 15025017654 0013331 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; /** * @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/) */ class MoFileLoader extends FileLoader { /** * Magic used for validating the format of an MO file as well as * detecting if the machine used to create that file was little endian. */ public const MO_LITTLE_ENDIAN_MAGIC = 0x950412DE; /** * Magic used for validating the format of an MO file as well as * detecting if the machine used to create that file was big endian. */ public const MO_BIG_ENDIAN_MAGIC = 0xDE120495; /** * The size of the header of an MO file in bytes. */ public const MO_HEADER_SIZE = 28; /** * Parses machine object (MO) format, independent of the machine's endian it * was created on. Both 32bit and 64bit systems are supported. * * {@inheritdoc} */ protected function loadResource(string $resource): array { $stream = fopen($resource, 'r'); $stat = fstat($stream); if ($stat['size'] < self::MO_HEADER_SIZE) { throw new InvalidResourceException('MO stream content has an invalid format.'); } $magic = unpack('V1', fread($stream, 4)); $magic = hexdec(substr(dechex(current($magic)), -8)); if (self::MO_LITTLE_ENDIAN_MAGIC == $magic) { $isBigEndian = false; } elseif (self::MO_BIG_ENDIAN_MAGIC == $magic) { $isBigEndian = true; } else { throw new InvalidResourceException('MO stream content has an invalid format.'); } // formatRevision $this->readLong($stream, $isBigEndian); $count = $this->readLong($stream, $isBigEndian); $offsetId = $this->readLong($stream, $isBigEndian); $offsetTranslated = $this->readLong($stream, $isBigEndian); // sizeHashes $this->readLong($stream, $isBigEndian); // offsetHashes $this->readLong($stream, $isBigEndian); $messages = []; for ($i = 0; $i < $count; ++$i) { $pluralId = null; $translated = null; fseek($stream, $offsetId + $i * 8); $length = $this->readLong($stream, $isBigEndian); $offset = $this->readLong($stream, $isBigEndian); if ($length < 1) { continue; } fseek($stream, $offset); $singularId = fread($stream, $length); if (str_contains($singularId, "\000")) { [$singularId, $pluralId] = explode("\000", $singularId); } fseek($stream, $offsetTranslated + $i * 8); $length = $this->readLong($stream, $isBigEndian); $offset = $this->readLong($stream, $isBigEndian); if ($length < 1) { continue; } fseek($stream, $offset); $translated = fread($stream, $length); if (str_contains($translated, "\000")) { $translated = explode("\000", $translated); } $ids = ['singular' => $singularId, 'plural' => $pluralId]; $item = compact('ids', 'translated'); if (!empty($item['ids']['singular'])) { $id = $item['ids']['singular']; if (isset($item['ids']['plural'])) { $id .= '|'.$item['ids']['plural']; } $messages[$id] = stripcslashes(implode('|', (array) $item['translated'])); } } fclose($stream); return array_filter($messages); } /** * Reads an unsigned long from stream respecting endianness. * * @param resource $stream */ private function readLong($stream, bool $isBigEndian): int { $result = unpack($isBigEndian ? 'N1' : 'V1', fread($stream, 4)); $result = current($result); return (int) substr($result, -8); } } translation/Loader/IcuResFileLoader.php 0000644 00000005415 15025017654 0014154 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\MessageCatalogue; /** * IcuResFileLoader loads translations from a resource bundle. * * @author stealth35 */ class IcuResFileLoader implements LoaderInterface { /** * {@inheritdoc} */ public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } if (!is_dir($resource)) { throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } try { $rb = new \ResourceBundle($locale, $resource); } catch (\Exception $e) { $rb = null; } if (!$rb) { throw new InvalidResourceException(sprintf('Cannot load resource "%s".', $resource)); } elseif (intl_is_failure($rb->getErrorCode())) { throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode()); } $messages = $this->flatten($rb); $catalogue = new MessageCatalogue($locale); $catalogue->add($messages, $domain); if (class_exists(DirectoryResource::class)) { $catalogue->addResource(new DirectoryResource($resource)); } return $catalogue; } /** * Flattens an ResourceBundle. * * The scheme used is: * key { key2 { key3 { "value" } } } * Becomes: * 'key.key2.key3' => 'value' * * This function takes an array by reference and will modify it * * @param \ResourceBundle $rb The ResourceBundle that will be flattened * @param array $messages Used internally for recursive calls * @param string $path Current path being parsed, used internally for recursive calls */ protected function flatten(\ResourceBundle $rb, array &$messages = [], string $path = null): array { foreach ($rb as $key => $value) { $nodePath = $path ? $path.'.'.$key : $key; if ($value instanceof \ResourceBundle) { $this->flatten($value, $messages, $nodePath); } else { $messages[$nodePath] = $value; } } return $messages; } } translation/Loader/JsonFileLoader.php 0000644 00000003256 15025017654 0013674 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; /** * JsonFileLoader loads translations from an json file. * * @author singles */ class JsonFileLoader extends FileLoader { /** * {@inheritdoc} */ protected function loadResource(string $resource): array { $messages = []; if ($data = file_get_contents($resource)) { $messages = json_decode($data, true); if (0 < $errorCode = json_last_error()) { throw new InvalidResourceException('Error parsing JSON: '.$this->getJSONErrorMessage($errorCode)); } } return $messages; } /** * Translates JSON_ERROR_* constant into meaningful message. */ private function getJSONErrorMessage(int $errorCode): string { switch ($errorCode) { case \JSON_ERROR_DEPTH: return 'Maximum stack depth exceeded'; case \JSON_ERROR_STATE_MISMATCH: return 'Underflow or the modes mismatch'; case \JSON_ERROR_CTRL_CHAR: return 'Unexpected control character found'; case \JSON_ERROR_SYNTAX: return 'Syntax error, malformed JSON'; case \JSON_ERROR_UTF8: return 'Malformed UTF-8 characters, possibly incorrectly encoded'; default: return 'Unknown error'; } } } translation/Loader/YamlFileLoader.php 0000644 00000003161 15025017654 0013660 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser as YamlParser; use Symfony\Component\Yaml\Yaml; /** * YamlFileLoader loads translations from Yaml files. * * @author Fabien Potencier <fabien@symfony.com> */ class YamlFileLoader extends FileLoader { private $yamlParser; /** * {@inheritdoc} */ protected function loadResource(string $resource): array { if (null === $this->yamlParser) { if (!class_exists(\Symfony\Component\Yaml\Parser::class)) { throw new LogicException('Loading translations from the YAML format requires the Symfony Yaml component.'); } $this->yamlParser = new YamlParser(); } try { $messages = $this->yamlParser->parseFile($resource, Yaml::PARSE_CONSTANT); } catch (ParseException $e) { throw new InvalidResourceException(sprintf('The file "%s" does not contain valid YAML: ', $resource).$e->getMessage(), 0, $e); } if (null !== $messages && !\is_array($messages)) { throw new InvalidResourceException(sprintf('Unable to load file "%s".', $resource)); } return $messages ?: []; } } translation/Loader/XliffFileLoader.php 0000644 00000020741 15025017654 0014031 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Config\Util\Exception\InvalidXmlException; use Symfony\Component\Config\Util\Exception\XmlParsingException; use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Util\XliffUtils; /** * XliffFileLoader loads translations from XLIFF files. * * @author Fabien Potencier <fabien@symfony.com> */ class XliffFileLoader implements LoaderInterface { /** * {@inheritdoc} */ public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { if (!class_exists(XmlUtils::class)) { throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.'); } if (!$this->isXmlString($resource)) { if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } if (!file_exists($resource)) { throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } if (!is_file($resource)) { throw new InvalidResourceException(sprintf('This is neither a file nor an XLIFF string "%s".', $resource)); } } try { if ($this->isXmlString($resource)) { $dom = XmlUtils::parse($resource); } else { $dom = XmlUtils::loadFile($resource); } } catch (\InvalidArgumentException|XmlParsingException|InvalidXmlException $e) { throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e); } if ($errors = XliffUtils::validateSchema($dom)) { throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors)); } $catalogue = new MessageCatalogue($locale); $this->extract($dom, $catalogue, $domain); if (is_file($resource) && class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource)); } return $catalogue; } private function extract(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain) { $xliffVersion = XliffUtils::getVersionNumber($dom); if ('1.2' === $xliffVersion) { $this->extractXliff1($dom, $catalogue, $domain); } if ('2.0' === $xliffVersion) { $this->extractXliff2($dom, $catalogue, $domain); } } /** * Extract messages and metadata from DOMDocument into a MessageCatalogue. */ private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain) { $xml = simplexml_import_dom($dom); $encoding = $dom->encoding ? strtoupper($dom->encoding) : null; $namespace = 'urn:oasis:names:tc:xliff:document:1.2'; $xml->registerXPathNamespace('xliff', $namespace); foreach ($xml->xpath('//xliff:file') as $file) { $fileAttributes = $file->attributes(); $file->registerXPathNamespace('xliff', $namespace); foreach ($file->xpath('.//xliff:trans-unit') as $translation) { $attributes = $translation->attributes(); if (!(isset($attributes['resname']) || isset($translation->source))) { continue; } $source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source; // If the xlf file has another encoding specified, try to convert it because // simple_xml will always return utf-8 encoded values $target = $this->utf8ToCharset((string) ($translation->target ?? $translation->source), $encoding); $catalogue->set((string) $source, $target, $domain); $metadata = [ 'source' => (string) $translation->source, 'file' => [ 'original' => (string) $fileAttributes['original'], ], ]; if ($notes = $this->parseNotesMetadata($translation->note, $encoding)) { $metadata['notes'] = $notes; } if (isset($translation->target) && $translation->target->attributes()) { $metadata['target-attributes'] = []; foreach ($translation->target->attributes() as $key => $value) { $metadata['target-attributes'][$key] = (string) $value; } } if (isset($attributes['id'])) { $metadata['id'] = (string) $attributes['id']; } $catalogue->setMetadata((string) $source, $metadata, $domain); } } } private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain) { $xml = simplexml_import_dom($dom); $encoding = $dom->encoding ? strtoupper($dom->encoding) : null; $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:2.0'); foreach ($xml->xpath('//xliff:unit') as $unit) { foreach ($unit->segment as $segment) { $attributes = $unit->attributes(); $source = $attributes['name'] ?? $segment->source; // If the xlf file has another encoding specified, try to convert it because // simple_xml will always return utf-8 encoded values $target = $this->utf8ToCharset((string) ($segment->target ?? $segment->source), $encoding); $catalogue->set((string) $source, $target, $domain); $metadata = []; if (isset($segment->target) && $segment->target->attributes()) { $metadata['target-attributes'] = []; foreach ($segment->target->attributes() as $key => $value) { $metadata['target-attributes'][$key] = (string) $value; } } if (isset($unit->notes)) { $metadata['notes'] = []; foreach ($unit->notes->note as $noteNode) { $note = []; foreach ($noteNode->attributes() as $key => $value) { $note[$key] = (string) $value; } $note['content'] = (string) $noteNode; $metadata['notes'][] = $note; } } $catalogue->setMetadata((string) $source, $metadata, $domain); } } } /** * Convert a UTF8 string to the specified encoding. */ private function utf8ToCharset(string $content, string $encoding = null): string { if ('UTF-8' !== $encoding && !empty($encoding)) { return mb_convert_encoding($content, $encoding, 'UTF-8'); } return $content; } private function parseNotesMetadata(\SimpleXMLElement $noteElement = null, string $encoding = null): array { $notes = []; if (null === $noteElement) { return $notes; } /** @var \SimpleXMLElement $xmlNote */ foreach ($noteElement as $xmlNote) { $noteAttributes = $xmlNote->attributes(); $note = ['content' => $this->utf8ToCharset((string) $xmlNote, $encoding)]; if (isset($noteAttributes['priority'])) { $note['priority'] = (int) $noteAttributes['priority']; } if (isset($noteAttributes['from'])) { $note['from'] = (string) $noteAttributes['from']; } $notes[] = $note; } return $notes; } private function isXmlString(string $resource): bool { return 0 === strpos($resource, '<?xml'); } } translation/Loader/PhpFileLoader.php 0000644 00000002243 15025017654 0013505 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; /** * PhpFileLoader loads translations from PHP files returning an array of translations. * * @author Fabien Potencier <fabien@symfony.com> */ class PhpFileLoader extends FileLoader { private static ?array $cache = []; /** * {@inheritdoc} */ protected function loadResource(string $resource): array { if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN))) { self::$cache = null; } if (null === self::$cache) { return require $resource; } if (isset(self::$cache[$resource])) { return self::$cache[$resource]; } return self::$cache[$resource] = require $resource; } } translation/Loader/PoFileLoader.php 0000644 00000012005 15025017654 0013331 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; /** * @copyright Copyright (c) 2010, Union of RAD https://github.com/UnionOfRAD/lithium * @copyright Copyright (c) 2012, Clemens Tolboom */ class PoFileLoader extends FileLoader { /** * Parses portable object (PO) format. * * From https://www.gnu.org/software/gettext/manual/gettext.html#PO-Files * we should be able to parse files having: * * white-space * # translator-comments * #. extracted-comments * #: reference... * #, flag... * #| msgid previous-untranslated-string * msgid untranslated-string * msgstr translated-string * * extra or different lines are: * * #| msgctxt previous-context * #| msgid previous-untranslated-string * msgctxt context * * #| msgid previous-untranslated-string-singular * #| msgid_plural previous-untranslated-string-plural * msgid untranslated-string-singular * msgid_plural untranslated-string-plural * msgstr[0] translated-string-case-0 * ... * msgstr[N] translated-string-case-n * * The definition states: * - white-space and comments are optional. * - msgid "" that an empty singleline defines a header. * * This parser sacrifices some features of the reference implementation the * differences to that implementation are as follows. * - No support for comments spanning multiple lines. * - Translator and extracted comments are treated as being the same type. * - Message IDs are allowed to have other encodings as just US-ASCII. * * Items with an empty id are ignored. * * {@inheritdoc} */ protected function loadResource(string $resource): array { $stream = fopen($resource, 'r'); $defaults = [ 'ids' => [], 'translated' => null, ]; $messages = []; $item = $defaults; $flags = []; while ($line = fgets($stream)) { $line = trim($line); if ('' === $line) { // Whitespace indicated current item is done if (!\in_array('fuzzy', $flags)) { $this->addMessage($messages, $item); } $item = $defaults; $flags = []; } elseif ('#,' === substr($line, 0, 2)) { $flags = array_map('trim', explode(',', substr($line, 2))); } elseif ('msgid "' === substr($line, 0, 7)) { // We start a new msg so save previous // TODO: this fails when comments or contexts are added $this->addMessage($messages, $item); $item = $defaults; $item['ids']['singular'] = substr($line, 7, -1); } elseif ('msgstr "' === substr($line, 0, 8)) { $item['translated'] = substr($line, 8, -1); } elseif ('"' === $line[0]) { $continues = isset($item['translated']) ? 'translated' : 'ids'; if (\is_array($item[$continues])) { end($item[$continues]); $item[$continues][key($item[$continues])] .= substr($line, 1, -1); } else { $item[$continues] .= substr($line, 1, -1); } } elseif ('msgid_plural "' === substr($line, 0, 14)) { $item['ids']['plural'] = substr($line, 14, -1); } elseif ('msgstr[' === substr($line, 0, 7)) { $size = strpos($line, ']'); $item['translated'][(int) substr($line, 7, 1)] = substr($line, $size + 3, -1); } } // save last item if (!\in_array('fuzzy', $flags)) { $this->addMessage($messages, $item); } fclose($stream); return $messages; } /** * Save a translation item to the messages. * * A .po file could contain by error missing plural indexes. We need to * fix these before saving them. */ private function addMessage(array &$messages, array $item) { if (!empty($item['ids']['singular'])) { $id = stripcslashes($item['ids']['singular']); if (isset($item['ids']['plural'])) { $id .= '|'.stripcslashes($item['ids']['plural']); } $translated = (array) $item['translated']; // PO are by definition indexed so sort by index. ksort($translated); // Make sure every index is filled. end($translated); $count = key($translated); // Fill missing spots with '-'. $empties = array_fill(0, $count + 1, '-'); $translated += $empties; ksort($translated); $messages[$id] = stripcslashes(implode('|', $translated)); } } } translation/Loader/QtFileLoader.php 0000644 00000005465 15025017654 0013353 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; /** * QtFileLoader loads translations from QT Translations XML files. * * @author Benjamin Eberlei <kontakt@beberlei.de> */ class QtFileLoader implements LoaderInterface { /** * {@inheritdoc} */ public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { if (!class_exists(XmlUtils::class)) { throw new RuntimeException('Loading translations from the QT format requires the Symfony Config component.'); } if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } if (!file_exists($resource)) { throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } try { $dom = XmlUtils::loadFile($resource); } catch (\InvalidArgumentException $e) { throw new InvalidResourceException(sprintf('Unable to load "%s".', $resource), $e->getCode(), $e); } $internalErrors = libxml_use_internal_errors(true); libxml_clear_errors(); $xpath = new \DOMXPath($dom); $nodes = $xpath->evaluate('//TS/context/name[text()="'.$domain.'"]'); $catalogue = new MessageCatalogue($locale); if (1 == $nodes->length) { $translations = $nodes->item(0)->nextSibling->parentNode->parentNode->getElementsByTagName('message'); foreach ($translations as $translation) { $translationValue = (string) $translation->getElementsByTagName('translation')->item(0)->nodeValue; if (!empty($translationValue)) { $catalogue->set( (string) $translation->getElementsByTagName('source')->item(0)->nodeValue, $translationValue, $domain ); } $translation = $translation->nextSibling; } if (class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource)); } } libxml_use_internal_errors($internalErrors); return $catalogue; } } translation/Loader/LoaderInterface.php 0000644 00000001666 15025017654 0014066 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\MessageCatalogue; /** * LoaderInterface is the interface implemented by all translation loaders. * * @author Fabien Potencier <fabien@symfony.com> */ interface LoaderInterface { /** * Loads a locale. * * @throws NotFoundResourceException when the resource cannot be found * @throws InvalidResourceException when the resource cannot be loaded */ public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue; } translation/Loader/IcuDatFileLoader.php 0000644 00000003514 15025017654 0014131 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\MessageCatalogue; /** * IcuResFileLoader loads translations from a resource bundle. * * @author stealth35 */ class IcuDatFileLoader extends IcuResFileLoader { /** * {@inheritdoc} */ public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { if (!stream_is_local($resource.'.dat')) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } if (!file_exists($resource.'.dat')) { throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } try { $rb = new \ResourceBundle($locale, $resource); } catch (\Exception $e) { $rb = null; } if (!$rb) { throw new InvalidResourceException(sprintf('Cannot load resource "%s".', $resource)); } elseif (intl_is_failure($rb->getErrorCode())) { throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode()); } $messages = $this->flatten($rb); $catalogue = new MessageCatalogue($locale); $catalogue->add($messages, $domain); if (class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource.'.dat')); } return $catalogue; } } translation/Loader/ArrayLoader.php 0000644 00000002637 15025017654 0013243 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\MessageCatalogue; /** * ArrayLoader loads translations from a PHP array. * * @author Fabien Potencier <fabien@symfony.com> */ class ArrayLoader implements LoaderInterface { /** * {@inheritdoc} */ public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { $resource = $this->flatten($resource); $catalogue = new MessageCatalogue($locale); $catalogue->add($resource, $domain); return $catalogue; } /** * Flattens an nested array of translations. * * The scheme used is: * 'key' => ['key2' => ['key3' => 'value']] * Becomes: * 'key.key2.key3' => 'value' */ private function flatten(array $messages): array { $result = []; foreach ($messages as $key => $value) { if (\is_array($value)) { foreach ($this->flatten($value) as $k => $v) { $result[$key.'.'.$k] = $v; } } else { $result[$key] = $value; } } return $result; } } translation/LICENSE 0000644 00000002051 15025017654 0010112 0 ustar 00 Copyright (c) 2004-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. translation/Formatter/MessageFormatter.php 0000644 00000003203 15025017654 0015031 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Formatter; use Symfony\Component\Translation\IdentityTranslator; use Symfony\Contracts\Translation\TranslatorInterface; // Help opcache.preload discover always-needed symbols class_exists(IntlFormatter::class); /** * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> */ class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface { private $translator; private $intlFormatter; /** * @param TranslatorInterface|null $translator An identity translator to use as selector for pluralization */ public function __construct(TranslatorInterface $translator = null, IntlFormatterInterface $intlFormatter = null) { $this->translator = $translator ?? new IdentityTranslator(); $this->intlFormatter = $intlFormatter ?? new IntlFormatter(); } /** * {@inheritdoc} */ public function format(string $message, string $locale, array $parameters = []): string { if ($this->translator instanceof TranslatorInterface) { return $this->translator->trans($message, $parameters, null, $locale); } return strtr($message, $parameters); } /** * {@inheritdoc} */ public function formatIntl(string $message, string $locale, array $parameters = []): string { return $this->intlFormatter->formatIntl($message, $locale, $parameters); } } translation/Formatter/IntlFormatter.php 0000644 00000004205 15025017654 0014356 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Formatter; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\LogicException; /** * @author Guilherme Blanco <guilhermeblanco@hotmail.com> * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> */ class IntlFormatter implements IntlFormatterInterface { private $hasMessageFormatter; private $cache = []; /** * {@inheritdoc} */ public function formatIntl(string $message, string $locale, array $parameters = []): string { // MessageFormatter constructor throws an exception if the message is empty if ('' === $message) { return ''; } if (!$formatter = $this->cache[$locale][$message] ?? null) { if (!($this->hasMessageFormatter ?? $this->hasMessageFormatter = class_exists(\MessageFormatter::class))) { throw new LogicException('Cannot parse message translation: please install the "intl" PHP extension or the "symfony/polyfill-intl-messageformatter" package.'); } try { $this->cache[$locale][$message] = $formatter = new \MessageFormatter($locale, $message); } catch (\IntlException $e) { throw new InvalidArgumentException(sprintf('Invalid message format (error #%d): ', intl_get_error_code()).intl_get_error_message(), 0, $e); } } foreach ($parameters as $key => $value) { if (\in_array($key[0] ?? null, ['%', '{'], true)) { unset($parameters[$key]); $parameters[trim($key, '%{ }')] = $value; } } if (false === $message = $formatter->format($parameters)) { throw new InvalidArgumentException(sprintf('Unable to format message (error #%s): ', $formatter->getErrorCode()).$formatter->getErrorMessage()); } return $message; } } translation/Formatter/MessageFormatterInterface.php 0000644 00000001526 15025017654 0016660 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Formatter; /** * @author Guilherme Blanco <guilhermeblanco@hotmail.com> * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> */ interface MessageFormatterInterface { /** * Formats a localized message pattern with given arguments. * * @param string $message The message (may also be an object that can be cast to string) * @param string $locale The message locale * @param array $parameters An array of parameters for the message */ public function format(string $message, string $locale, array $parameters = []): string; } translation/Formatter/IntlFormatterInterface.php 0000644 00000001262 15025017654 0016177 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Formatter; /** * Formats ICU message patterns. * * @author Nicolas Grekas <p@tchwork.com> */ interface IntlFormatterInterface { /** * Formats a localized message using rules defined by ICU MessageFormat. * * @see http://icu-project.org/apiref/icu4c/classMessageFormat.html#details */ public function formatIntl(string $message, string $locale, array $parameters = []): string; } translation/TranslatorBag.php 0000644 00000006200 15025017654 0012361 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\Translation\Catalogue\AbstractOperation; use Symfony\Component\Translation\Catalogue\TargetOperation; final class TranslatorBag implements TranslatorBagInterface { /** @var MessageCatalogue[] */ private array $catalogues = []; public function addCatalogue(MessageCatalogue $catalogue): void { if (null !== $existingCatalogue = $this->getCatalogue($catalogue->getLocale())) { $catalogue->addCatalogue($existingCatalogue); } $this->catalogues[$catalogue->getLocale()] = $catalogue; } public function addBag(TranslatorBagInterface $bag): void { foreach ($bag->getCatalogues() as $catalogue) { $this->addCatalogue($catalogue); } } /** * {@inheritdoc} */ public function getCatalogue(string $locale = null): MessageCatalogueInterface { if (null === $locale || !isset($this->catalogues[$locale])) { $this->catalogues[$locale] = new MessageCatalogue($locale); } return $this->catalogues[$locale]; } /** * {@inheritdoc} */ public function getCatalogues(): array { return array_values($this->catalogues); } public function diff(TranslatorBagInterface $diffBag): self { $diff = new self(); foreach ($this->catalogues as $locale => $catalogue) { if (null === $diffCatalogue = $diffBag->getCatalogue($locale)) { $diff->addCatalogue($catalogue); continue; } $operation = new TargetOperation($diffCatalogue, $catalogue); $operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::NEW_BATCH); $newCatalogue = new MessageCatalogue($locale); foreach ($operation->getDomains() as $domain) { $newCatalogue->add($operation->getNewMessages($domain), $domain); } $diff->addCatalogue($newCatalogue); } return $diff; } public function intersect(TranslatorBagInterface $intersectBag): self { $diff = new self(); foreach ($this->catalogues as $locale => $catalogue) { if (null === $intersectCatalogue = $intersectBag->getCatalogue($locale)) { continue; } $operation = new TargetOperation($catalogue, $intersectCatalogue); $operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::OBSOLETE_BATCH); $obsoleteCatalogue = new MessageCatalogue($locale); foreach ($operation->getDomains() as $domain) { $obsoleteCatalogue->add( array_diff($operation->getMessages($domain), $operation->getNewMessages($domain)), $domain ); } $diff->addCatalogue($obsoleteCatalogue); } return $diff; } } translation/MetadataAwareInterface.php 0000644 00000002424 15025017654 0014143 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; /** * MetadataAwareInterface. * * @author Fabien Potencier <fabien@symfony.com> */ interface MetadataAwareInterface { /** * Gets metadata for the given domain and key. * * Passing an empty domain will return an array with all metadata indexed by * domain and then by key. Passing an empty key will return an array with all * metadata for the given domain. * * @return mixed The value that was set or an array with the domains/keys or null */ public function getMetadata(string $key = '', string $domain = 'messages'): mixed; /** * Adds metadata to a message domain. */ public function setMetadata(string $key, mixed $value, string $domain = 'messages'); /** * Deletes metadata for the given key and domain. * * Passing an empty domain will delete all metadata. Passing an empty key will * delete all metadata for the given domain. */ public function deleteMetadata(string $key = '', string $domain = 'messages'); } translation/Provider/NullProviderFactory.php 0000644 00000001513 15025017654 0015367 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Provider; use Symfony\Component\Translation\Exception\UnsupportedSchemeException; /** * @author Mathieu Santostefano <msantostefano@protonmail.com> */ final class NullProviderFactory extends AbstractProviderFactory { public function create(Dsn $dsn): ProviderInterface { if ('null' === $dsn->getScheme()) { return new NullProvider(); } throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes()); } protected function getSupportedSchemes(): array { return ['null']; } } translation/Provider/TranslationProviderCollectionFactory.php 0000644 00000003261 15025017654 0020771 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Provider; use Symfony\Component\Translation\Exception\UnsupportedSchemeException; /** * @author Mathieu Santostefano <msantostefano@protonmail.com> */ class TranslationProviderCollectionFactory { private iterable $factories; private array $enabledLocales; /** * @param iterable<mixed, ProviderFactoryInterface> $factories */ public function __construct(iterable $factories, array $enabledLocales) { $this->factories = $factories; $this->enabledLocales = $enabledLocales; } public function fromConfig(array $config): TranslationProviderCollection { $providers = []; foreach ($config as $name => $currentConfig) { $providers[$name] = $this->fromDsnObject( new Dsn($currentConfig['dsn']), !$currentConfig['locales'] ? $this->enabledLocales : $currentConfig['locales'], !$currentConfig['domains'] ? [] : $currentConfig['domains'] ); } return new TranslationProviderCollection($providers); } public function fromDsnObject(Dsn $dsn, array $locales, array $domains = []): ProviderInterface { foreach ($this->factories as $factory) { if ($factory->supports($dsn)) { return new FilteringProvider($factory->create($dsn), $locales, $domains); } } throw new UnsupportedSchemeException($dsn); } } translation/Provider/NullProvider.php 0000644 00000001616 15025017654 0014043 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Provider; use Symfony\Component\Translation\TranslatorBag; use Symfony\Component\Translation\TranslatorBagInterface; /** * @author Mathieu Santostefano <msantostefano@protonmail.com> */ class NullProvider implements ProviderInterface { public function __toString(): string { return 'null'; } public function write(TranslatorBagInterface $translatorBag, bool $override = false): void { } public function read(array $domains, array $locales): TranslatorBag { return new TranslatorBag(); } public function delete(TranslatorBagInterface $translatorBag): void { } } translation/Provider/ProviderFactoryInterface.php 0000644 00000001235 15025017654 0016356 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Provider; use Symfony\Component\Translation\Exception\IncompleteDsnException; use Symfony\Component\Translation\Exception\UnsupportedSchemeException; interface ProviderFactoryInterface { /** * @throws UnsupportedSchemeException * @throws IncompleteDsnException */ public function create(Dsn $dsn): ProviderInterface; public function supports(Dsn $dsn): bool; } translation/Provider/FilteringProvider.php 0000644 00000003255 15025017654 0015055 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Provider; use Symfony\Component\Translation\TranslatorBag; use Symfony\Component\Translation\TranslatorBagInterface; /** * Filters domains and locales between the Translator config values and those specific to each provider. * * @author Mathieu Santostefano <msantostefano@protonmail.com> */ class FilteringProvider implements ProviderInterface { private $provider; private array $locales; private array $domains; public function __construct(ProviderInterface $provider, array $locales, array $domains = []) { $this->provider = $provider; $this->locales = $locales; $this->domains = $domains; } public function __toString(): string { return (string) $this->provider; } /** * {@inheritdoc} */ public function write(TranslatorBagInterface $translatorBag): void { $this->provider->write($translatorBag); } public function read(array $domains, array $locales): TranslatorBag { $domains = !$this->domains ? $domains : array_intersect($this->domains, $domains); $locales = array_intersect($this->locales, $locales); return $this->provider->read($domains, $locales); } public function delete(TranslatorBagInterface $translatorBag): void { $this->provider->delete($translatorBag); } public function getDomains(): array { return $this->domains; } } translation/Provider/Dsn.php 0000644 00000005643 15025017654 0012146 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Provider; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\MissingRequiredOptionException; /** * @author Fabien Potencier <fabien@symfony.com> * @author Oskar Stark <oskarstark@googlemail.com> */ final class Dsn { private ?string $scheme; private ?string $host; private ?string $user; private ?string $password; private ?int $port; private ?string $path; private array $options = []; private string $originalDsn; public function __construct(string $dsn) { $this->originalDsn = $dsn; if (false === $parsedDsn = parse_url($dsn)) { throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN is invalid.', $dsn)); } if (!isset($parsedDsn['scheme'])) { throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a scheme.', $dsn)); } $this->scheme = $parsedDsn['scheme']; if (!isset($parsedDsn['host'])) { throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a host (use "default" by default).', $dsn)); } $this->host = $parsedDsn['host']; $this->user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null; $this->password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null; $this->port = $parsedDsn['port'] ?? null; $this->path = $parsedDsn['path'] ?? null; parse_str($parsedDsn['query'] ?? '', $this->options); } public function getScheme(): string { return $this->scheme; } public function getHost(): string { return $this->host; } public function getUser(): ?string { return $this->user; } public function getPassword(): ?string { return $this->password; } public function getPort(int $default = null): ?int { return $this->port ?? $default; } public function getOption(string $key, mixed $default = null) { return $this->options[$key] ?? $default; } public function getRequiredOption(string $key) { if (!\array_key_exists($key, $this->options) || '' === trim($this->options[$key])) { throw new MissingRequiredOptionException($key); } return $this->options[$key]; } public function getOptions(): array { return $this->options; } public function getPath(): ?string { return $this->path; } public function getOriginalDsn(): string { return $this->originalDsn; } } translation/Provider/ProviderInterface.php 0000644 00000001666 15025017654 0015036 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Provider; use Symfony\Component\Translation\TranslatorBag; use Symfony\Component\Translation\TranslatorBagInterface; interface ProviderInterface { public function __toString(): string; /** * Translations available in the TranslatorBag only must be created. * Translations available in both the TranslatorBag and on the provider * must be overwritten. * Translations available on the provider only must be kept. */ public function write(TranslatorBagInterface $translatorBag): void; public function read(array $domains, array $locales): TranslatorBag; public function delete(TranslatorBagInterface $translatorBag): void; } translation/Provider/AbstractProviderFactory.php 0000644 00000002214 15025017654 0016217 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Provider; use Symfony\Component\Translation\Exception\IncompleteDsnException; abstract class AbstractProviderFactory implements ProviderFactoryInterface { public function supports(Dsn $dsn): bool { return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true); } /** * @return string[] */ abstract protected function getSupportedSchemes(): array; protected function getUser(Dsn $dsn): string { if (null === $user = $dsn->getUser()) { throw new IncompleteDsnException('User is not set.', $dsn->getOriginalDsn()); } return $user; } protected function getPassword(Dsn $dsn): string { if (null === $password = $dsn->getPassword()) { throw new IncompleteDsnException('Password is not set.', $dsn->getOriginalDsn()); } return $password; } } translation/Provider/TranslationProviderCollection.php 0000644 00000002562 15025017654 0017444 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Provider; use Symfony\Component\Translation\Exception\InvalidArgumentException; /** * @author Mathieu Santostefano <msantostefano@protonmail.com> */ final class TranslationProviderCollection { /** * @var array<string, ProviderInterface> */ private $providers; /** * @param array<string, ProviderInterface> $providers */ public function __construct(iterable $providers) { $this->providers = \is_array($providers) ? $providers : iterator_to_array($providers); } public function __toString(): string { return '['.implode(',', array_keys($this->providers)).']'; } public function has(string $name): bool { return isset($this->providers[$name]); } public function get(string $name): ProviderInterface { if (!$this->has($name)) { throw new InvalidArgumentException(sprintf('Provider "%s" not found. Available: "%s".', $name, (string) $this)); } return $this->providers[$name]; } public function keys(): array { return array_keys($this->providers); } } translation/DataCollectorTranslator.php 0000644 00000011237 15025017654 0014416 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> */ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface { public const MESSAGE_DEFINED = 0; public const MESSAGE_MISSING = 1; public const MESSAGE_EQUALS_FALLBACK = 2; private $translator; private array $messages = []; /** * @param TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator */ public function __construct(TranslatorInterface $translator) { if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) { throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator))); } $this->translator = $translator; } /** * {@inheritdoc} */ public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null): string { $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); $this->collectMessage($locale, $domain, $id, $trans, $parameters); return $trans; } /** * {@inheritdoc} */ public function setLocale(string $locale) { $this->translator->setLocale($locale); } /** * {@inheritdoc} */ public function getLocale(): string { return $this->translator->getLocale(); } /** * {@inheritdoc} */ public function getCatalogue(string $locale = null): MessageCatalogueInterface { return $this->translator->getCatalogue($locale); } /** * {@inheritdoc} */ public function getCatalogues(): array { return $this->translator->getCatalogues(); } /** * {@inheritdoc} * * @return string[] */ public function warmUp(string $cacheDir): array { if ($this->translator instanceof WarmableInterface) { return (array) $this->translator->warmUp($cacheDir); } return []; } /** * Gets the fallback locales. */ public function getFallbackLocales(): array { if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) { return $this->translator->getFallbackLocales(); } return []; } /** * Passes through all unknown calls onto the translator object. */ public function __call(string $method, array $args) { return $this->translator->{$method}(...$args); } public function getCollectedMessages(): array { return $this->messages; } private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = []) { if (null === $domain) { $domain = 'messages'; } $catalogue = $this->translator->getCatalogue($locale); $locale = $catalogue->getLocale(); $fallbackLocale = null; if ($catalogue->defines($id, $domain)) { $state = self::MESSAGE_DEFINED; } elseif ($catalogue->has($id, $domain)) { $state = self::MESSAGE_EQUALS_FALLBACK; $fallbackCatalogue = $catalogue->getFallbackCatalogue(); while ($fallbackCatalogue) { if ($fallbackCatalogue->defines($id, $domain)) { $fallbackLocale = $fallbackCatalogue->getLocale(); break; } $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue(); } } else { $state = self::MESSAGE_MISSING; } $this->messages[] = [ 'locale' => $locale, 'fallbackLocale' => $fallbackLocale, 'domain' => $domain, 'id' => $id, 'translation' => $translation, 'parameters' => $parameters, 'state' => $state, 'transChoiceNumber' => isset($parameters['%count%']) && is_numeric($parameters['%count%']) ? $parameters['%count%'] : null, ]; } } translation/MessageCatalogueInterface.php 0000644 00000006447 15025017654 0014665 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\Config\Resource\ResourceInterface; /** * MessageCatalogueInterface. * * @author Fabien Potencier <fabien@symfony.com> */ interface MessageCatalogueInterface { public const INTL_DOMAIN_SUFFIX = '+intl-icu'; /** * Gets the catalogue locale. */ public function getLocale(): string; /** * Gets the domains. */ public function getDomains(): array; /** * Gets the messages within a given domain. * * If $domain is null, it returns all messages. * * @param string $domain The domain name */ public function all(string $domain = null): array; /** * Sets a message translation. * * @param string $id The message id * @param string $translation The messages translation * @param string $domain The domain name */ public function set(string $id, string $translation, string $domain = 'messages'); /** * Checks if a message has a translation. * * @param string $id The message id * @param string $domain The domain name */ public function has(string $id, string $domain = 'messages'): bool; /** * Checks if a message has a translation (it does not take into account the fallback mechanism). * * @param string $id The message id * @param string $domain The domain name */ public function defines(string $id, string $domain = 'messages'): bool; /** * Gets a message translation. * * @param string $id The message id * @param string $domain The domain name */ public function get(string $id, string $domain = 'messages'): string; /** * Sets translations for a given domain. * * @param array $messages An array of translations * @param string $domain The domain name */ public function replace(array $messages, string $domain = 'messages'); /** * Adds translations for a given domain. * * @param array $messages An array of translations * @param string $domain The domain name */ public function add(array $messages, string $domain = 'messages'); /** * Merges translations from the given Catalogue into the current one. * * The two catalogues must have the same locale. */ public function addCatalogue(self $catalogue); /** * Merges translations from the given Catalogue into the current one * only when the translation does not exist. * * This is used to provide default translations when they do not exist for the current locale. */ public function addFallbackCatalogue(self $catalogue); /** * Gets the fallback catalogue. */ public function getFallbackCatalogue(): ?self; /** * Returns an array of resources loaded to build this collection. * * @return ResourceInterface[] */ public function getResources(): array; /** * Adds a resource for this collection. */ public function addResource(ResourceInterface $resource); } translation/IdentityTranslator.php 0000644 00000001225 15025017654 0013463 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; /** * IdentityTranslator does not translate anything. * * @author Fabien Potencier <fabien@symfony.com> */ class IdentityTranslator implements TranslatorInterface, LocaleAwareInterface { use TranslatorTrait; } translation/Dumper/IcuResFileDumper.php 0000644 00000005672 15025017654 0014235 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * IcuResDumper generates an ICU ResourceBundle formatted string representation of a message catalogue. * * @author Stealth35 */ class IcuResFileDumper extends FileDumper { /** * {@inheritdoc} */ protected $relativePathTemplate = '%domain%/%locale%.%extension%'; /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $data = $indexes = $resources = ''; foreach ($messages->all($domain) as $source => $target) { $indexes .= pack('v', \strlen($data) + 28); $data .= $source."\0"; } $data .= $this->writePadding($data); $keyTop = $this->getPosition($data); foreach ($messages->all($domain) as $source => $target) { $resources .= pack('V', $this->getPosition($data)); $data .= pack('V', \strlen($target)) .mb_convert_encoding($target."\0", 'UTF-16LE', 'UTF-8') .$this->writePadding($data) ; } $resOffset = $this->getPosition($data); $data .= pack('v', \count($messages->all($domain))) .$indexes .$this->writePadding($data) .$resources ; $bundleTop = $this->getPosition($data); $root = pack('V7', $resOffset + (2 << 28), // Resource Offset + Resource Type 6, // Index length $keyTop, // Index keys top $bundleTop, // Index resources top $bundleTop, // Index bundle top \count($messages->all($domain)), // Index max table length 0 // Index attributes ); $header = pack('vC2v4C12@32', 32, // Header size 0xDA, 0x27, // Magic number 1 and 2 20, 0, 0, 2, // Rest of the header, ..., Size of a char 0x52, 0x65, 0x73, 0x42, // Data format identifier 1, 2, 0, 0, // Data version 1, 4, 0, 0 // Unicode version ); return $header.$root.$data; } private function writePadding(string $data): ?string { $padding = \strlen($data) % 4; return $padding ? str_repeat("\xAA", 4 - $padding) : null; } private function getPosition(string $data) { return (\strlen($data) + 28) / 4; } /** * {@inheritdoc} */ protected function getExtension(): string { return 'res'; } } translation/Dumper/IniFileDumper.php 0000644 00000002000 15025017654 0013540 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * IniFileDumper generates an ini formatted string representation of a message catalogue. * * @author Stealth35 */ class IniFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $output = ''; foreach ($messages->all($domain) as $source => $target) { $escapeTarget = str_replace('"', '\"', $target); $output .= $source.'="'.$escapeTarget."\"\n"; } return $output; } /** * {@inheritdoc} */ protected function getExtension(): string { return 'ini'; } } translation/Dumper/PoFileDumper.php 0000644 00000007726 15025017654 0013423 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * PoFileDumper generates a gettext formatted string representation of a message catalogue. * * @author Stealth35 */ class PoFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $output = 'msgid ""'."\n"; $output .= 'msgstr ""'."\n"; $output .= '"Content-Type: text/plain; charset=UTF-8\n"'."\n"; $output .= '"Content-Transfer-Encoding: 8bit\n"'."\n"; $output .= '"Language: '.$messages->getLocale().'\n"'."\n"; $output .= "\n"; $newLine = false; foreach ($messages->all($domain) as $source => $target) { if ($newLine) { $output .= "\n"; } else { $newLine = true; } $metadata = $messages->getMetadata($source, $domain); if (isset($metadata['comments'])) { $output .= $this->formatComments($metadata['comments']); } if (isset($metadata['flags'])) { $output .= $this->formatComments(implode(',', (array) $metadata['flags']), ','); } if (isset($metadata['sources'])) { $output .= $this->formatComments(implode(' ', (array) $metadata['sources']), ':'); } $sourceRules = $this->getStandardRules($source); $targetRules = $this->getStandardRules($target); if (2 == \count($sourceRules) && [] !== $targetRules) { $output .= sprintf('msgid "%s"'."\n", $this->escape($sourceRules[0])); $output .= sprintf('msgid_plural "%s"'."\n", $this->escape($sourceRules[1])); foreach ($targetRules as $i => $targetRule) { $output .= sprintf('msgstr[%d] "%s"'."\n", $i, $this->escape($targetRule)); } } else { $output .= sprintf('msgid "%s"'."\n", $this->escape($source)); $output .= sprintf('msgstr "%s"'."\n", $this->escape($target)); } } return $output; } private function getStandardRules(string $id) { // Partly copied from TranslatorTrait::trans. $parts = []; if (preg_match('/^\|++$/', $id)) { $parts = explode('|', $id); } elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) { $parts = $matches[0]; } $intervalRegexp = <<<'EOF' /^(?P<interval> ({\s* (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*) \s*}) | (?P<left_delimiter>[\[\]]) \s* (?P<left>-Inf|\-?\d+(\.\d+)?) \s*,\s* (?P<right>\+?Inf|\-?\d+(\.\d+)?) \s* (?P<right_delimiter>[\[\]]) )\s*(?P<message>.*?)$/xs EOF; $standardRules = []; foreach ($parts as $part) { $part = trim(str_replace('||', '|', $part)); if (preg_match($intervalRegexp, $part)) { // Explicit rule is not a standard rule. return []; } else { $standardRules[] = $part; } } return $standardRules; } /** * {@inheritdoc} */ protected function getExtension(): string { return 'po'; } private function escape(string $str): string { return addcslashes($str, "\0..\37\42\134"); } private function formatComments(string|array $comments, string $prefix = ''): ?string { $output = null; foreach ((array) $comments as $comment) { $output .= sprintf('#%s %s'."\n", $prefix, $comment); } return $output; } } translation/Dumper/PhpFileDumper.php 0000644 00000001542 15025017654 0013562 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * PhpFileDumper generates PHP files from a message catalogue. * * @author Michel Salib <michelsalib@hotmail.com> */ class PhpFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { return "<?php\n\nreturn ".var_export($messages->all($domain), true).";\n"; } /** * {@inheritdoc} */ protected function getExtension(): string { return 'php'; } } translation/Dumper/QtFileDumper.php 0000644 00000003646 15025017654 0013426 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * QtFileDumper generates ts files from a message catalogue. * * @author Benjamin Eberlei <kontakt@beberlei.de> */ class QtFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; $ts = $dom->appendChild($dom->createElement('TS')); $context = $ts->appendChild($dom->createElement('context')); $context->appendChild($dom->createElement('name', $domain)); foreach ($messages->all($domain) as $source => $target) { $message = $context->appendChild($dom->createElement('message')); $metadata = $messages->getMetadata($source, $domain); if (isset($metadata['sources'])) { foreach ((array) $metadata['sources'] as $location) { $loc = explode(':', $location, 2); $location = $message->appendChild($dom->createElement('location')); $location->setAttribute('filename', $loc[0]); if (isset($loc[1])) { $location->setAttribute('line', $loc[1]); } } } $message->appendChild($dom->createElement('source', $source)); $message->appendChild($dom->createElement('translation', $target)); } return $dom->saveXML(); } /** * {@inheritdoc} */ protected function getExtension(): string { return 'ts'; } } translation/Dumper/DumperInterface.php 0000644 00000001340 15025017654 0014127 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * DumperInterface is the interface implemented by all translation dumpers. * There is no common option. * * @author Michel Salib <michelsalib@hotmail.com> */ interface DumperInterface { /** * Dumps the message catalogue. * * @param array $options Options that are used by the dumper */ public function dump(MessageCatalogue $messages, array $options = []); } translation/Dumper/CsvFileDumper.php 0000644 00000002634 15025017654 0013571 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * CsvFileDumper generates a csv formatted string representation of a message catalogue. * * @author Stealth35 */ class CsvFileDumper extends FileDumper { private string $delimiter = ';'; private string $enclosure = '"'; /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $handle = fopen('php://memory', 'r+'); foreach ($messages->all($domain) as $source => $target) { fputcsv($handle, [$source, $target], $this->delimiter, $this->enclosure); } rewind($handle); $output = stream_get_contents($handle); fclose($handle); return $output; } /** * Sets the delimiter and escape character for CSV. */ public function setCsvControl(string $delimiter = ';', string $enclosure = '"') { $this->delimiter = $delimiter; $this->enclosure = $enclosure; } /** * {@inheritdoc} */ protected function getExtension(): string { return 'csv'; } } translation/Dumper/YamlFileDumper.php 0000644 00000003113 15025017654 0013731 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Util\ArrayConverter; use Symfony\Component\Yaml\Yaml; /** * YamlFileDumper generates yaml files from a message catalogue. * * @author Michel Salib <michelsalib@hotmail.com> */ class YamlFileDumper extends FileDumper { private string $extension; public function __construct(string $extension = 'yml') { $this->extension = $extension; } /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { if (!class_exists(Yaml::class)) { throw new LogicException('Dumping translations in the YAML format requires the Symfony Yaml component.'); } $data = $messages->all($domain); if (isset($options['as_tree']) && $options['as_tree']) { $data = ArrayConverter::expandToTree($data); } if (isset($options['inline']) && ($inline = (int) $options['inline']) > 0) { return Yaml::dump($data, $inline); } return Yaml::dump($data); } /** * {@inheritdoc} */ protected function getExtension(): string { return $this->extension; } } translation/Dumper/FileDumper.php 0000644 00000007040 15025017654 0013111 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; /** * FileDumper is an implementation of DumperInterface that dump a message catalogue to file(s). * * Options: * - path (mandatory): the directory where the files should be saved * * @author Michel Salib <michelsalib@hotmail.com> */ abstract class FileDumper implements DumperInterface { /** * A template for the relative paths to files. * * @var string */ protected $relativePathTemplate = '%domain%.%locale%.%extension%'; /** * Sets the template for the relative paths to files. * * @param string $relativePathTemplate A template for the relative paths to files */ public function setRelativePathTemplate(string $relativePathTemplate) { $this->relativePathTemplate = $relativePathTemplate; } /** * {@inheritdoc} */ public function dump(MessageCatalogue $messages, array $options = []) { if (!\array_key_exists('path', $options)) { throw new InvalidArgumentException('The file dumper needs a path option.'); } // save a file for each domain foreach ($messages->getDomains() as $domain) { $fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale()); if (!file_exists($fullpath)) { $directory = \dirname($fullpath); if (!file_exists($directory) && !@mkdir($directory, 0777, true)) { throw new RuntimeException(sprintf('Unable to create directory "%s".', $directory)); } } $intlDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX; $intlMessages = $messages->all($intlDomain); if ($intlMessages) { $intlPath = $options['path'].'/'.$this->getRelativePath($intlDomain, $messages->getLocale()); file_put_contents($intlPath, $this->formatCatalogue($messages, $intlDomain, $options)); $messages->replace([], $intlDomain); try { if ($messages->all($domain)) { file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options)); } continue; } finally { $messages->replace($intlMessages, $intlDomain); } } file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options)); } } /** * Transforms a domain of a message catalogue to its string representation. */ abstract public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string; /** * Gets the file extension of the dumper. */ abstract protected function getExtension(): string; /** * Gets the relative file path using the template. */ private function getRelativePath(string $domain, string $locale): string { return strtr($this->relativePathTemplate, [ '%domain%' => $domain, '%locale%' => $locale, '%extension%' => $this->getExtension(), ]); } } translation/Dumper/XliffFileDumper.php 0000644 00000017512 15025017654 0014107 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\MessageCatalogue; /** * XliffFileDumper generates xliff files from a message catalogue. * * @author Michel Salib <michelsalib@hotmail.com> */ class XliffFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $xliffVersion = '1.2'; if (\array_key_exists('xliff_version', $options)) { $xliffVersion = $options['xliff_version']; } if (\array_key_exists('default_locale', $options)) { $defaultLocale = $options['default_locale']; } else { $defaultLocale = \Locale::getDefault(); } if ('1.2' === $xliffVersion) { return $this->dumpXliff1($defaultLocale, $messages, $domain, $options); } if ('2.0' === $xliffVersion) { return $this->dumpXliff2($defaultLocale, $messages, $domain); } throw new InvalidArgumentException(sprintf('No support implemented for dumping XLIFF version "%s".', $xliffVersion)); } /** * {@inheritdoc} */ protected function getExtension(): string { return 'xlf'; } private function dumpXliff1(string $defaultLocale, MessageCatalogue $messages, ?string $domain, array $options = []) { $toolInfo = ['tool-id' => 'symfony', 'tool-name' => 'Symfony']; if (\array_key_exists('tool_info', $options)) { $toolInfo = array_merge($toolInfo, $options['tool_info']); } $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; $xliff = $dom->appendChild($dom->createElement('xliff')); $xliff->setAttribute('version', '1.2'); $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2'); $xliffFile = $xliff->appendChild($dom->createElement('file')); $xliffFile->setAttribute('source-language', str_replace('_', '-', $defaultLocale)); $xliffFile->setAttribute('target-language', str_replace('_', '-', $messages->getLocale())); $xliffFile->setAttribute('datatype', 'plaintext'); $xliffFile->setAttribute('original', 'file.ext'); $xliffHead = $xliffFile->appendChild($dom->createElement('header')); $xliffTool = $xliffHead->appendChild($dom->createElement('tool')); foreach ($toolInfo as $id => $value) { $xliffTool->setAttribute($id, $value); } $xliffBody = $xliffFile->appendChild($dom->createElement('body')); foreach ($messages->all($domain) as $source => $target) { $translation = $dom->createElement('trans-unit'); $translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._')); $translation->setAttribute('resname', $source); $s = $translation->appendChild($dom->createElement('source')); $s->appendChild($dom->createTextNode($source)); // Does the target contain characters requiring a CDATA section? $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); $targetElement = $dom->createElement('target'); $metadata = $messages->getMetadata($source, $domain); if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { foreach ($metadata['target-attributes'] as $name => $value) { $targetElement->setAttribute($name, $value); } } $t = $translation->appendChild($targetElement); $t->appendChild($text); if ($this->hasMetadataArrayInfo('notes', $metadata)) { foreach ($metadata['notes'] as $note) { if (!isset($note['content'])) { continue; } $n = $translation->appendChild($dom->createElement('note')); $n->appendChild($dom->createTextNode($note['content'])); if (isset($note['priority'])) { $n->setAttribute('priority', $note['priority']); } if (isset($note['from'])) { $n->setAttribute('from', $note['from']); } } } $xliffBody->appendChild($translation); } return $dom->saveXML(); } private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ?string $domain) { $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; $xliff = $dom->appendChild($dom->createElement('xliff')); $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:2.0'); $xliff->setAttribute('version', '2.0'); $xliff->setAttribute('srcLang', str_replace('_', '-', $defaultLocale)); $xliff->setAttribute('trgLang', str_replace('_', '-', $messages->getLocale())); $xliffFile = $xliff->appendChild($dom->createElement('file')); if (str_ends_with($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX)) { $xliffFile->setAttribute('id', substr($domain, 0, -\strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX)).'.'.$messages->getLocale()); } else { $xliffFile->setAttribute('id', $domain.'.'.$messages->getLocale()); } foreach ($messages->all($domain) as $source => $target) { $translation = $dom->createElement('unit'); $translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._')); if (\strlen($source) <= 80) { $translation->setAttribute('name', $source); } $metadata = $messages->getMetadata($source, $domain); // Add notes section if ($this->hasMetadataArrayInfo('notes', $metadata)) { $notesElement = $dom->createElement('notes'); foreach ($metadata['notes'] as $note) { $n = $dom->createElement('note'); $n->appendChild($dom->createTextNode($note['content'] ?? '')); unset($note['content']); foreach ($note as $name => $value) { $n->setAttribute($name, $value); } $notesElement->appendChild($n); } $translation->appendChild($notesElement); } $segment = $translation->appendChild($dom->createElement('segment')); $s = $segment->appendChild($dom->createElement('source')); $s->appendChild($dom->createTextNode($source)); // Does the target contain characters requiring a CDATA section? $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); $targetElement = $dom->createElement('target'); if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { foreach ($metadata['target-attributes'] as $name => $value) { $targetElement->setAttribute($name, $value); } } $t = $segment->appendChild($targetElement); $t->appendChild($text); $xliffFile->appendChild($translation); } return $dom->saveXML(); } private function hasMetadataArrayInfo(string $key, array $metadata = null): bool { return is_iterable($metadata[$key] ?? null); } } translation/Dumper/JsonFileDumper.php 0000644 00000001617 15025017654 0013747 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * JsonFileDumper generates an json formatted string representation of a message catalogue. * * @author singles */ class JsonFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $flags = $options['json_encoding'] ?? \JSON_PRETTY_PRINT; return json_encode($messages->all($domain), $flags); } /** * {@inheritdoc} */ protected function getExtension(): string { return 'json'; } } translation/Dumper/MoFileDumper.php 0000644 00000004461 15025017654 0013411 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Loader\MoFileLoader; use Symfony\Component\Translation\MessageCatalogue; /** * MoFileDumper generates a gettext formatted string representation of a message catalogue. * * @author Stealth35 */ class MoFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $sources = $targets = $sourceOffsets = $targetOffsets = ''; $offsets = []; $size = 0; foreach ($messages->all($domain) as $source => $target) { $offsets[] = array_map('strlen', [$sources, $source, $targets, $target]); $sources .= "\0".$source; $targets .= "\0".$target; ++$size; } $header = [ 'magicNumber' => MoFileLoader::MO_LITTLE_ENDIAN_MAGIC, 'formatRevision' => 0, 'count' => $size, 'offsetId' => MoFileLoader::MO_HEADER_SIZE, 'offsetTranslated' => MoFileLoader::MO_HEADER_SIZE + (8 * $size), 'sizeHashes' => 0, 'offsetHashes' => MoFileLoader::MO_HEADER_SIZE + (16 * $size), ]; $sourcesSize = \strlen($sources); $sourcesStart = $header['offsetHashes'] + 1; foreach ($offsets as $offset) { $sourceOffsets .= $this->writeLong($offset[1]) .$this->writeLong($offset[0] + $sourcesStart); $targetOffsets .= $this->writeLong($offset[3]) .$this->writeLong($offset[2] + $sourcesStart + $sourcesSize); } $output = implode('', array_map([$this, 'writeLong'], $header)) .$sourceOffsets .$targetOffsets .$sources .$targets ; return $output; } /** * {@inheritdoc} */ protected function getExtension(): string { return 'mo'; } private function writeLong(mixed $str): string { return pack('V*', $str); } } translation/TranslatorBagInterface.php 0000644 00000001624 15025017654 0014207 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\Translation\Exception\InvalidArgumentException; /** * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> */ interface TranslatorBagInterface { /** * Gets the catalogue by locale. * * @param string|null $locale The locale or null to use the default * * @throws InvalidArgumentException If the locale contains invalid characters */ public function getCatalogue(string $locale = null): MessageCatalogueInterface; /** * Returns all catalogues of the instance. * * @return MessageCatalogueInterface[] */ public function getCatalogues(): array; } translation/PseudoLocalizationTranslator.php 0000644 00000025567 15025017654 0015521 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Contracts\Translation\TranslatorInterface; /** * This translator should only be used in a development environment. */ final class PseudoLocalizationTranslator implements TranslatorInterface { private const EXPANSION_CHARACTER = '~'; private $translator; private bool $accents; private float $expansionFactor; private bool $brackets; private bool $parseHTML; /** * @var string[] */ private array $localizableHTMLAttributes; /** * Available options: * * accents: * type: boolean * default: true * description: replace ASCII characters of the translated string with accented versions or similar characters * example: if true, "foo" => "ƒöö". * * * expansion_factor: * type: float * default: 1 * validation: it must be greater than or equal to 1 * description: expand the translated string by the given factor with spaces and tildes * example: if 2, "foo" => "~foo ~" * * * brackets: * type: boolean * default: true * description: wrap the translated string with brackets * example: if true, "foo" => "[foo]" * * * parse_html: * type: boolean * default: false * description: parse the translated string as HTML - looking for HTML tags has a performance impact but allows to preserve them from alterations - it also allows to compute the visible translated string length which is useful to correctly expand ot when it contains HTML * warning: unclosed tags are unsupported, they will be fixed (closed) by the parser - eg, "foo <div>bar" => "foo <div>bar</div>" * * * localizable_html_attributes: * type: string[] * default: [] * description: the list of HTML attributes whose values can be altered - it is only useful when the "parse_html" option is set to true * example: if ["title"], and with the "accents" option set to true, "<a href="#" title="Go to your profile">Profile</a>" => "<a href="#" title="Ĝö ţö ýöûŕ þŕöƒîļé">Þŕöƒîļé</a>" - if "title" was not in the "localizable_html_attributes" list, the title attribute data would be left unchanged. */ public function __construct(TranslatorInterface $translator, array $options = []) { $this->translator = $translator; $this->accents = $options['accents'] ?? true; if (1.0 > ($this->expansionFactor = $options['expansion_factor'] ?? 1.0)) { throw new \InvalidArgumentException('The expansion factor must be greater than or equal to 1.'); } $this->brackets = $options['brackets'] ?? true; $this->parseHTML = $options['parse_html'] ?? false; if ($this->parseHTML && !$this->accents && 1.0 === $this->expansionFactor) { $this->parseHTML = false; } $this->localizableHTMLAttributes = $options['localizable_html_attributes'] ?? []; } /** * {@inheritdoc} */ public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string { $trans = ''; $visibleText = ''; foreach ($this->getParts($this->translator->trans($id, $parameters, $domain, $locale)) as [$visible, $localizable, $text]) { if ($visible) { $visibleText .= $text; } if (!$localizable) { $trans .= $text; continue; } $this->addAccents($trans, $text); } $this->expand($trans, $visibleText); $this->addBrackets($trans); return $trans; } public function getLocale(): string { return $this->translator->getLocale(); } private function getParts(string $originalTrans): array { if (!$this->parseHTML) { return [[true, true, $originalTrans]]; } $html = mb_encode_numericentity($originalTrans, [0x80, 0xFFFF, 0, 0xFFFF], mb_detect_encoding($originalTrans, null, true) ?: 'UTF-8'); $useInternalErrors = libxml_use_internal_errors(true); $dom = new \DOMDocument(); $dom->loadHTML('<trans>'.$html.'</trans>'); libxml_clear_errors(); libxml_use_internal_errors($useInternalErrors); return $this->parseNode($dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0)); } private function parseNode(\DOMNode $node): array { $parts = []; foreach ($node->childNodes as $childNode) { if (!$childNode instanceof \DOMElement) { $parts[] = [true, true, $childNode->nodeValue]; continue; } $parts[] = [false, false, '<'.$childNode->tagName]; /** @var \DOMAttr $attribute */ foreach ($childNode->attributes as $attribute) { $parts[] = [false, false, ' '.$attribute->nodeName.'="']; $localizableAttribute = \in_array($attribute->nodeName, $this->localizableHTMLAttributes, true); foreach (preg_split('/(&(?:amp|quot|#039|lt|gt);+)/', htmlspecialchars($attribute->nodeValue, \ENT_QUOTES, 'UTF-8'), -1, \PREG_SPLIT_DELIM_CAPTURE) as $i => $match) { if ('' === $match) { continue; } $parts[] = [false, $localizableAttribute && 0 === $i % 2, $match]; } $parts[] = [false, false, '"']; } $parts[] = [false, false, '>']; $parts = array_merge($parts, $this->parseNode($childNode, $parts)); $parts[] = [false, false, '</'.$childNode->tagName.'>']; } return $parts; } private function addAccents(string &$trans, string $text): void { $trans .= $this->accents ? strtr($text, [ ' ' => ' ', '!' => '¡', '"' => '″', '#' => '♯', '$' => '€', '%' => '‰', '&' => '⅋', '\'' => '´', '(' => '{', ')' => '}', '*' => '⁎', '+' => '⁺', ',' => '،', '-' => '‐', '.' => '·', '/' => '⁄', '0' => '⓪', '1' => '①', '2' => '②', '3' => '③', '4' => '④', '5' => '⑤', '6' => '⑥', '7' => '⑦', '8' => '⑧', '9' => '⑨', ':' => '∶', ';' => '⁏', '<' => '≤', '=' => '≂', '>' => '≥', '?' => '¿', '@' => '՞', 'A' => 'Å', 'B' => 'Ɓ', 'C' => 'Ç', 'D' => 'Ð', 'E' => 'É', 'F' => 'Ƒ', 'G' => 'Ĝ', 'H' => 'Ĥ', 'I' => 'Î', 'J' => 'Ĵ', 'K' => 'Ķ', 'L' => 'Ļ', 'M' => 'Ṁ', 'N' => 'Ñ', 'O' => 'Ö', 'P' => 'Þ', 'Q' => 'Ǫ', 'R' => 'Ŕ', 'S' => 'Š', 'T' => 'Ţ', 'U' => 'Û', 'V' => 'Ṽ', 'W' => 'Ŵ', 'X' => 'Ẋ', 'Y' => 'Ý', 'Z' => 'Ž', '[' => '⁅', '\\' => '∖', ']' => '⁆', '^' => '˄', '_' => '‿', '`' => '‵', 'a' => 'å', 'b' => 'ƀ', 'c' => 'ç', 'd' => 'ð', 'e' => 'é', 'f' => 'ƒ', 'g' => 'ĝ', 'h' => 'ĥ', 'i' => 'î', 'j' => 'ĵ', 'k' => 'ķ', 'l' => 'ļ', 'm' => 'ɱ', 'n' => 'ñ', 'o' => 'ö', 'p' => 'þ', 'q' => 'ǫ', 'r' => 'ŕ', 's' => 'š', 't' => 'ţ', 'u' => 'û', 'v' => 'ṽ', 'w' => 'ŵ', 'x' => 'ẋ', 'y' => 'ý', 'z' => 'ž', '{' => '(', '|' => '¦', '}' => ')', '~' => '˞', ]) : $text; } private function expand(string &$trans, string $visibleText): void { if (1.0 >= $this->expansionFactor) { return; } $visibleLength = $this->strlen($visibleText); $missingLength = (int) ceil($visibleLength * $this->expansionFactor) - $visibleLength; if ($this->brackets) { $missingLength -= 2; } if (0 >= $missingLength) { return; } $words = []; $wordsCount = 0; foreach (preg_split('/ +/', $visibleText, -1, \PREG_SPLIT_NO_EMPTY) as $word) { $wordLength = $this->strlen($word); if ($wordLength >= $missingLength) { continue; } if (!isset($words[$wordLength])) { $words[$wordLength] = 0; } ++$words[$wordLength]; ++$wordsCount; } if (!$words) { $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); return; } arsort($words, \SORT_NUMERIC); $longestWordLength = max(array_keys($words)); while (true) { $r = mt_rand(1, $wordsCount); foreach ($words as $length => $count) { $r -= $count; if ($r <= 0) { break; } } $trans .= ' '.str_repeat(self::EXPANSION_CHARACTER, $length); $missingLength -= $length + 1; if (0 === $missingLength) { return; } while ($longestWordLength >= $missingLength) { $wordsCount -= $words[$longestWordLength]; unset($words[$longestWordLength]); if (!$words) { $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); return; } $longestWordLength = max(array_keys($words)); } } } private function addBrackets(string &$trans): void { if (!$this->brackets) { return; } $trans = '['.$trans.']'; } private function strlen(string $s): int { return false === ($encoding = mb_detect_encoding($s, null, true)) ? \strlen($s) : mb_strlen($s, $encoding); } } translation/Resources/bin/translation-status.php 0000644 00000021262 15025017654 0016224 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if ('cli' !== \PHP_SAPI) { throw new Exception('This script must be run from the command line.'); } $usageInstructions = <<<END Usage instructions ------------------------------------------------------------------------------- $ cd symfony-code-root-directory/ # show the translation status of all locales $ php translation-status.php # only show the translation status of incomplete or erroneous locales $ php translation-status.php --incomplete # show the translation status of all locales, all their missing translations and mismatches between trans-unit id and source $ php translation-status.php -v # show the status of a single locale $ php translation-status.php fr # show the status of a single locale, missing translations and mismatches between trans-unit id and source $ php translation-status.php fr -v END; $config = [ // if TRUE, the full list of missing translations is displayed 'verbose_output' => false, // NULL = analyze all locales 'locale_to_analyze' => null, // append --incomplete to only show incomplete languages 'include_completed_languages' => true, // the reference files all the other translations are compared to 'original_files' => [ 'src/Symfony/Component/Form/Resources/translations/validators.en.xlf', 'src/Symfony/Component/Security/Core/Resources/translations/security.en.xlf', 'src/Symfony/Component/Validator/Resources/translations/validators.en.xlf', ], ]; $argc = $_SERVER['argc']; $argv = $_SERVER['argv']; if ($argc > 4) { echo str_replace('translation-status.php', $argv[0], $usageInstructions); exit(1); } foreach (array_slice($argv, 1) as $argumentOrOption) { if ('--incomplete' === $argumentOrOption) { $config['include_completed_languages'] = false; continue; } if (str_starts_with($argumentOrOption, '-')) { $config['verbose_output'] = true; } else { $config['locale_to_analyze'] = $argumentOrOption; } } foreach ($config['original_files'] as $originalFilePath) { if (!file_exists($originalFilePath)) { echo sprintf('The following file does not exist. Make sure that you execute this command at the root dir of the Symfony code repository.%s %s', \PHP_EOL, $originalFilePath); exit(1); } } $totalMissingTranslations = 0; $totalTranslationMismatches = 0; foreach ($config['original_files'] as $originalFilePath) { $translationFilePaths = findTranslationFiles($originalFilePath, $config['locale_to_analyze']); $translationStatus = calculateTranslationStatus($originalFilePath, $translationFilePaths); $totalMissingTranslations += array_sum(array_map(function ($translation) { return count($translation['missingKeys']); }, array_values($translationStatus))); $totalTranslationMismatches += array_sum(array_map(function ($translation) { return count($translation['mismatches']); }, array_values($translationStatus))); printTranslationStatus($originalFilePath, $translationStatus, $config['verbose_output'], $config['include_completed_languages']); } exit($totalTranslationMismatches > 0 ? 1 : 0); function findTranslationFiles($originalFilePath, $localeToAnalyze) { $translations = []; $translationsDir = dirname($originalFilePath); $originalFileName = basename($originalFilePath); $translationFileNamePattern = str_replace('.en.', '.*.', $originalFileName); $translationFiles = glob($translationsDir.'/'.$translationFileNamePattern, \GLOB_NOSORT); sort($translationFiles); foreach ($translationFiles as $filePath) { $locale = extractLocaleFromFilePath($filePath); if (null !== $localeToAnalyze && $locale !== $localeToAnalyze) { continue; } $translations[$locale] = $filePath; } return $translations; } function calculateTranslationStatus($originalFilePath, $translationFilePaths) { $translationStatus = []; $allTranslationKeys = extractTranslationKeys($originalFilePath); foreach ($translationFilePaths as $locale => $translationPath) { $translatedKeys = extractTranslationKeys($translationPath); $missingKeys = array_diff_key($allTranslationKeys, $translatedKeys); $mismatches = findTransUnitMismatches($allTranslationKeys, $translatedKeys); $translationStatus[$locale] = [ 'total' => count($allTranslationKeys), 'translated' => count($translatedKeys), 'missingKeys' => $missingKeys, 'mismatches' => $mismatches, ]; $translationStatus[$locale]['is_completed'] = isTranslationCompleted($translationStatus[$locale]); } return $translationStatus; } function isTranslationCompleted(array $translationStatus): bool { return $translationStatus['total'] === $translationStatus['translated'] && 0 === count($translationStatus['mismatches']); } function printTranslationStatus($originalFilePath, $translationStatus, $verboseOutput, $includeCompletedLanguages) { printTitle($originalFilePath); printTable($translationStatus, $verboseOutput, $includeCompletedLanguages); echo \PHP_EOL.\PHP_EOL; } function extractLocaleFromFilePath($filePath) { $parts = explode('.', $filePath); return $parts[count($parts) - 2]; } function extractTranslationKeys($filePath) { $translationKeys = []; $contents = new \SimpleXMLElement(file_get_contents($filePath)); foreach ($contents->file->body->{'trans-unit'} as $translationKey) { $translationId = (string) $translationKey['id']; $translationKey = (string) $translationKey->source; $translationKeys[$translationId] = $translationKey; } return $translationKeys; } /** * Check whether the trans-unit id and source match with the base translation. */ function findTransUnitMismatches(array $baseTranslationKeys, array $translatedKeys): array { $mismatches = []; foreach ($baseTranslationKeys as $translationId => $translationKey) { if (!isset($translatedKeys[$translationId])) { continue; } if ($translatedKeys[$translationId] !== $translationKey) { $mismatches[$translationId] = [ 'found' => $translatedKeys[$translationId], 'expected' => $translationKey, ]; } } return $mismatches; } function printTitle($title) { echo $title.\PHP_EOL; echo str_repeat('=', strlen($title)).\PHP_EOL.\PHP_EOL; } function printTable($translations, $verboseOutput, bool $includeCompletedLanguages) { if (0 === count($translations)) { echo 'No translations found'; return; } $longestLocaleNameLength = max(array_map('strlen', array_keys($translations))); foreach ($translations as $locale => $translation) { if (!$includeCompletedLanguages && $translation['is_completed']) { continue; } if ($translation['translated'] > $translation['total']) { textColorRed(); } elseif (count($translation['mismatches']) > 0) { textColorRed(); } elseif ($translation['is_completed']) { textColorGreen(); } echo sprintf( '| Locale: %-'.$longestLocaleNameLength.'s | Translated: %2d/%2d | Mismatches: %d |', $locale, $translation['translated'], $translation['total'], count($translation['mismatches']) ).\PHP_EOL; textColorNormal(); $shouldBeClosed = false; if (true === $verboseOutput && count($translation['missingKeys']) > 0) { echo '| Missing Translations:'.\PHP_EOL; foreach ($translation['missingKeys'] as $id => $content) { echo sprintf('| (id=%s) %s', $id, $content).\PHP_EOL; } $shouldBeClosed = true; } if (true === $verboseOutput && count($translation['mismatches']) > 0) { echo '| Mismatches between trans-unit id and source:'.\PHP_EOL; foreach ($translation['mismatches'] as $id => $content) { echo sprintf('| (id=%s) Expected: %s', $id, $content['expected']).\PHP_EOL; echo sprintf('| Found: %s', $content['found']).\PHP_EOL; } $shouldBeClosed = true; } if ($shouldBeClosed) { echo str_repeat('-', 80).\PHP_EOL; } } } function textColorGreen() { echo "\033[32m"; } function textColorRed() { echo "\033[31m"; } function textColorNormal() { echo "\033[0m"; } translation/Resources/schemas/xliff-core-2.0.xsd 0000644 00000040554 15025017654 0015567 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <!-- XLIFF Version 2.0 OASIS Standard 05 August 2014 Copyright (c) OASIS Open 2014. All rights reserved. Source: http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/schemas/ --> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" xmlns:xlf="urn:oasis:names:tc:xliff:document:2.0" targetNamespace="urn:oasis:names:tc:xliff:document:2.0"> <!-- Import --> <xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="informativeCopiesOf3rdPartySchemas/w3c/xml.xsd"/> <!-- Element Group --> <xs:group name="inline"> <xs:choice> <xs:element ref="xlf:cp"/> <xs:element ref="xlf:ph"/> <xs:element ref="xlf:pc"/> <xs:element ref="xlf:sc"/> <xs:element ref="xlf:ec"/> <xs:element ref="xlf:mrk"/> <xs:element ref="xlf:sm"/> <xs:element ref="xlf:em"/> </xs:choice> </xs:group> <!-- Attribute Types --> <xs:simpleType name="yesNo"> <xs:restriction base="xs:string"> <xs:enumeration value="yes"/> <xs:enumeration value="no"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="yesNoFirstNo"> <xs:restriction base="xs:string"> <xs:enumeration value="yes"/> <xs:enumeration value="firstNo"/> <xs:enumeration value="no"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="dirValue"> <xs:restriction base="xs:string"> <xs:enumeration value="ltr"/> <xs:enumeration value="rtl"/> <xs:enumeration value="auto"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="appliesTo"> <xs:restriction base="xs:string"> <xs:enumeration value="source"/> <xs:enumeration value="target"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="userDefinedValue"> <xs:restriction base="xs:string"> <xs:pattern value="[^\s:]+:[^\s:]+"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="attrType_type"> <xs:restriction base="xs:string"> <xs:enumeration value="fmt"/> <xs:enumeration value="ui"/> <xs:enumeration value="quote"/> <xs:enumeration value="link"/> <xs:enumeration value="image"/> <xs:enumeration value="other"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="typeForMrkValues"> <xs:restriction base="xs:NMTOKEN"> <xs:enumeration value="generic"/> <xs:enumeration value="comment"/> <xs:enumeration value="term"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="attrType_typeForMrk"> <xs:union memberTypes="xlf:typeForMrkValues xlf:userDefinedValue"/> </xs:simpleType> <xs:simpleType name="priorityValue"> <xs:restriction base="xs:positiveInteger"> <xs:minInclusive value="1"/> <xs:maxInclusive value="10"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="stateType"> <xs:restriction base="xs:string"> <xs:enumeration value="initial"/> <xs:enumeration value="translated"/> <xs:enumeration value="reviewed"/> <xs:enumeration value="final"/> </xs:restriction> </xs:simpleType> <!-- Structural Elements --> <xs:element name="xliff"> <xs:complexType mixed="false"> <xs:sequence> <xs:element minOccurs="1" maxOccurs="unbounded" ref="xlf:file"/> </xs:sequence> <xs:attribute name="version" use="required"/> <xs:attribute name="srcLang" use="required"/> <xs:attribute name="trgLang" use="optional"/> <xs:attribute ref="xml:space" use="optional" default="default"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:complexType> </xs:element> <xs:element name="file"> <xs:complexType mixed="false"> <xs:sequence> <xs:element minOccurs="0" maxOccurs="1" ref="xlf:skeleton"/> <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other" processContents="lax"/> <xs:element minOccurs="0" maxOccurs="1" ref="xlf:notes"/> <xs:choice minOccurs="1" maxOccurs="unbounded"> <xs:element ref="xlf:unit"/> <xs:element ref="xlf:group"/> </xs:choice> </xs:sequence> <xs:attribute name="id" use="required" type="xs:NMTOKEN"/> <xs:attribute name="canResegment" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="original" use="optional"/> <xs:attribute name="translate" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="srcDir" use="optional" type="xlf:dirValue" default="auto"/> <xs:attribute name="trgDir" use="optional" type="xlf:dirValue" default="auto"/> <xs:attribute ref="xml:space" use="optional"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:complexType> </xs:element> <xs:element name="skeleton"> <xs:complexType mixed="true"> <xs:sequence> <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other" processContents="lax"/> </xs:sequence> <xs:attribute name="href" use="optional"/> </xs:complexType> </xs:element> <xs:element name="group"> <xs:complexType mixed="false"> <xs:sequence> <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other" processContents="lax"/> <xs:element minOccurs="0" maxOccurs="1" ref="xlf:notes"/> <xs:choice minOccurs="0" maxOccurs="unbounded"> <xs:element ref="xlf:unit"/> <xs:element ref="xlf:group"/> </xs:choice> </xs:sequence> <xs:attribute name="id" use="required" type="xs:NMTOKEN"/> <xs:attribute name="name" use="optional"/> <xs:attribute name="canResegment" use="optional" type="xlf:yesNo"/> <xs:attribute name="translate" use="optional" type="xlf:yesNo"/> <xs:attribute name="srcDir" use="optional" type="xlf:dirValue"/> <xs:attribute name="trgDir" use="optional" type="xlf:dirValue"/> <xs:attribute name="type" use="optional" type="xlf:userDefinedValue"/> <xs:attribute ref="xml:space" use="optional"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:complexType> </xs:element> <xs:element name="unit"> <xs:complexType mixed="false"> <xs:sequence> <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other" processContents="lax"/> <xs:element minOccurs="0" maxOccurs="1" ref="xlf:notes"/> <xs:element minOccurs="0" maxOccurs="1" ref="xlf:originalData"/> <xs:choice minOccurs="1" maxOccurs="unbounded"> <xs:element ref="xlf:segment"/> <xs:element ref="xlf:ignorable"/> </xs:choice> </xs:sequence> <xs:attribute name="id" use="required" type="xs:NMTOKEN"/> <xs:attribute name="name" use="optional"/> <xs:attribute name="canResegment" use="optional" type="xlf:yesNo"/> <xs:attribute name="translate" use="optional" type="xlf:yesNo"/> <xs:attribute name="srcDir" use="optional" type="xlf:dirValue"/> <xs:attribute name="trgDir" use="optional" type="xlf:dirValue"/> <xs:attribute ref="xml:space" use="optional"/> <xs:attribute name="type" use="optional" type="xlf:userDefinedValue"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:complexType> </xs:element> <xs:element name="segment"> <xs:complexType mixed="false"> <xs:sequence> <xs:element minOccurs="1" maxOccurs="1" ref="xlf:source"/> <xs:element minOccurs="0" maxOccurs="1" ref="xlf:target"/> </xs:sequence> <xs:attribute name="id" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="canResegment" use="optional" type="xlf:yesNo"/> <xs:attribute name="state" use="optional" type="xlf:stateType" default="initial"/> <xs:attribute name="subState" use="optional"/> </xs:complexType> </xs:element> <xs:element name="ignorable"> <xs:complexType mixed="false"> <xs:sequence> <xs:element minOccurs="1" maxOccurs="1" ref="xlf:source"/> <xs:element minOccurs="0" maxOccurs="1" ref="xlf:target"/> </xs:sequence> <xs:attribute name="id" use="optional" type="xs:NMTOKEN"/> </xs:complexType> </xs:element> <xs:element name="notes"> <xs:complexType mixed="false"> <xs:sequence> <xs:element minOccurs="1" maxOccurs="unbounded" ref="xlf:note"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="note"> <xs:complexType mixed="true"> <xs:attribute name="id" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="appliesTo" use="optional" type="xlf:appliesTo"/> <xs:attribute name="category" use="optional"/> <xs:attribute name="priority" use="optional" type="xlf:priorityValue" default="1"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:complexType> </xs:element> <xs:element name="originalData"> <xs:complexType mixed="false"> <xs:sequence> <xs:element minOccurs="1" maxOccurs="unbounded" ref="xlf:data"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="data"> <xs:complexType mixed="true"> <xs:sequence> <xs:element minOccurs="0" maxOccurs="unbounded" ref="xlf:cp"/> </xs:sequence> <xs:attribute name="id" use="required" type="xs:NMTOKEN"/> <xs:attribute name="dir" use="optional" type="xlf:dirValue" default="auto"/> <xs:attribute ref="xml:space" use="optional" fixed="preserve"/> </xs:complexType> </xs:element> <xs:element name="source"> <xs:complexType mixed="true"> <xs:group ref="xlf:inline" minOccurs="0" maxOccurs="unbounded"/> <xs:attribute ref="xml:lang" use="optional"/> <xs:attribute ref="xml:space" use="optional"/> </xs:complexType> </xs:element> <xs:element name="target"> <xs:complexType mixed="true"> <xs:group ref="xlf:inline" minOccurs="0" maxOccurs="unbounded"/> <xs:attribute ref="xml:lang" use="optional"/> <xs:attribute ref="xml:space" use="optional"/> <xs:attribute name="order" use="optional" type="xs:positiveInteger"/> </xs:complexType> </xs:element> <!-- Inline Elements --> <xs:element name="cp"> <!-- Code Point --> <xs:complexType mixed="false"> <xs:attribute name="hex" use="required" type="xs:hexBinary"/> </xs:complexType> </xs:element> <xs:element name="ph"> <!-- Placeholder --> <xs:complexType mixed="false"> <xs:attribute name="canCopy" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="canDelete" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="canReorder" use="optional" type="xlf:yesNoFirstNo" default="yes"/> <xs:attribute name="copyOf" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="disp" use="optional"/> <xs:attribute name="equiv" use="optional"/> <xs:attribute name="id" use="required" type="xs:NMTOKEN"/> <xs:attribute name="dataRef" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="subFlows" use="optional" type="xs:NMTOKENS"/> <xs:attribute name="subType" use="optional" type="xlf:userDefinedValue"/> <xs:attribute name="type" use="optional" type="xlf:attrType_type"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:complexType> </xs:element> <xs:element name="pc"> <!-- Paired Code --> <xs:complexType mixed="true"> <xs:group ref="xlf:inline" minOccurs="0" maxOccurs="unbounded"/> <xs:attribute name="canCopy" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="canDelete" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="canOverlap" use="optional" type="xlf:yesNo"/> <xs:attribute name="canReorder" use="optional" type="xlf:yesNoFirstNo" default="yes"/> <xs:attribute name="copyOf" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="dispEnd" use="optional"/> <xs:attribute name="dispStart" use="optional"/> <xs:attribute name="equivEnd" use="optional"/> <xs:attribute name="equivStart" use="optional"/> <xs:attribute name="id" use="required" type="xs:NMTOKEN"/> <xs:attribute name="dataRefEnd" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="dataRefStart" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="subFlowsEnd" use="optional" type="xs:NMTOKENS"/> <xs:attribute name="subFlowsStart" use="optional" type="xs:NMTOKENS"/> <xs:attribute name="subType" use="optional" type="xlf:userDefinedValue"/> <xs:attribute name="type" use="optional" type="xlf:attrType_type"/> <xs:attribute name="dir" use="optional" type="xlf:dirValue"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:complexType> </xs:element> <xs:element name="sc"> <!-- Start Code --> <xs:complexType mixed="false"> <xs:attribute name="canCopy" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="canDelete" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="canOverlap" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="canReorder" use="optional" type="xlf:yesNoFirstNo" default="yes"/> <xs:attribute name="copyOf" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="dataRef" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="dir" use="optional" type="xlf:dirValue"/> <xs:attribute name="disp" use="optional"/> <xs:attribute name="equiv" use="optional"/> <xs:attribute name="id" use="required" type="xs:NMTOKEN"/> <xs:attribute name="isolated" use="optional" type="xlf:yesNo" default="no"/> <xs:attribute name="subFlows" use="optional" type="xs:NMTOKENS"/> <xs:attribute name="subType" use="optional" type="xlf:userDefinedValue"/> <xs:attribute name="type" use="optional" type="xlf:attrType_type"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:complexType> </xs:element> <xs:element name="ec"> <!-- End Code --> <xs:complexType mixed="false"> <xs:attribute name="canCopy" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="canDelete" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="canOverlap" use="optional" type="xlf:yesNo" default="yes"/> <xs:attribute name="canReorder" use="optional" type="xlf:yesNoFirstNo" default="yes"/> <xs:attribute name="copyOf" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="dataRef" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="dir" use="optional" type="xlf:dirValue"/> <xs:attribute name="disp" use="optional"/> <xs:attribute name="equiv" use="optional"/> <xs:attribute name="id" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="isolated" use="optional" type="xlf:yesNo" default="no"/> <xs:attribute name="startRef" use="optional" type="xs:NMTOKEN"/> <xs:attribute name="subFlows" use="optional" type="xs:NMTOKENS"/> <xs:attribute name="subType" use="optional" type="xlf:userDefinedValue"/> <xs:attribute name="type" use="optional" type="xlf:attrType_type"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:complexType> </xs:element> <xs:element name="mrk"> <!-- Annotation Marker --> <xs:complexType mixed="true"> <xs:group ref="xlf:inline" minOccurs="0" maxOccurs="unbounded"/> <xs:attribute name="id" use="required" type="xs:NMTOKEN"/> <xs:attribute name="translate" use="optional" type="xlf:yesNo"/> <xs:attribute name="type" use="optional" type="xlf:attrType_typeForMrk"/> <xs:attribute name="ref" use="optional" type="xs:anyURI"/> <xs:attribute name="value" use="optional"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:complexType> </xs:element> <xs:element name="sm"> <!-- Start Annotation Marker --> <xs:complexType mixed="false"> <xs:attribute name="id" use="required" type="xs:NMTOKEN"/> <xs:attribute name="translate" use="optional" type="xlf:yesNo"/> <xs:attribute name="type" use="optional" type="xlf:attrType_typeForMrk"/> <xs:attribute name="ref" use="optional" type="xs:anyURI"/> <xs:attribute name="value" use="optional"/> <xs:anyAttribute namespace="##other" processContents="lax"/> </xs:complexType> </xs:element> <xs:element name="em"> <!-- End Annotation Marker --> <xs:complexType mixed="false"> <xs:attribute name="startRef" use="required" type="xs:NMTOKEN"/> </xs:complexType> </xs:element> </xs:schema> translation/Resources/schemas/xliff-core-1.2-strict.xsd 0000644 00000312001 15025017654 0017063 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <!-- May-19-2004: - Changed the <choice> for ElemType_header, moving minOccurs="0" maxOccurs="unbounded" from its elements to <choice> itself. - Added <choice> for ElemType_trans-unit to allow "any order" for <context-group>, <count-group>, <prop-group>, <note>, and <alt-trans>. Oct-2005 - updated version info to 1.2 - equiv-trans attribute to <trans-unit> element - merged-trans attribute for <group> element - Add the <seg-source> element as optional in the <trans-unit> and <alt-trans> content models, at the same level as <source> - Create a new value "seg" for the mtype attribute of the <mrk> element - Add mid as an optional attribute for the <alt-trans> element Nov-14-2005 - Changed name attribute for <context-group> from required to optional - Added extension point at <xliff> Jan-9-2006 - Added alttranstype type attribute to <alt-trans>, and values Jan-10-2006 - Corrected error with overwritten purposeValueList - Corrected name="AttrType_Version", attribute should have been "name" --> <xsd:schema xmlns:xlf="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="urn:oasis:names:tc:xliff:document:1.2" xml:lang="en"> <!-- Import for xml:lang and xml:space --> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2001/xml.xsd"/> <!-- Attributes Lists --> <xsd:simpleType name="XTend"> <xsd:restriction base="xsd:string"> <xsd:pattern value="x-[^\s]+"/> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="context-typeValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'context-type'.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:string"> <xsd:enumeration value="database"> <xsd:annotation> <xsd:documentation>Indicates a database content.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="element"> <xsd:annotation> <xsd:documentation>Indicates the content of an element within an XML document.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="elementtitle"> <xsd:annotation> <xsd:documentation>Indicates the name of an element within an XML document.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="linenumber"> <xsd:annotation> <xsd:documentation>Indicates the line number from the sourcefile (see context-type="sourcefile") where the <source> is found.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="numparams"> <xsd:annotation> <xsd:documentation>Indicates a the number of parameters contained within the <source>.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="paramnotes"> <xsd:annotation> <xsd:documentation>Indicates notes pertaining to the parameters in the <source>.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="record"> <xsd:annotation> <xsd:documentation>Indicates the content of a record within a database.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="recordtitle"> <xsd:annotation> <xsd:documentation>Indicates the name of a record within a database.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="sourcefile"> <xsd:annotation> <xsd:documentation>Indicates the original source file in the case that multiple files are merged to form the original file from which the XLIFF file is created. This differs from the original <file> attribute in that this sourcefile is one of many that make up that file.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="count-typeValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'count-type'.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="num-usages"> <xsd:annotation> <xsd:documentation>Indicates the count units are items that are used X times in a certain context; example: this is a reusable text unit which is used 42 times in other texts.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="repetition"> <xsd:annotation> <xsd:documentation>Indicates the count units are translation units existing already in the same document.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="total"> <xsd:annotation> <xsd:documentation>Indicates a total count.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="InlineDelimitersValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'ctype' when used other elements than <ph> or <x>.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="bold"> <xsd:annotation> <xsd:documentation>Indicates a run of bolded text.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="italic"> <xsd:annotation> <xsd:documentation>Indicates a run of text in italics.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="underlined"> <xsd:annotation> <xsd:documentation>Indicates a run of underlined text.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="link"> <xsd:annotation> <xsd:documentation>Indicates a run of hyper-text.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="InlinePlaceholdersValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'ctype' when used with <ph> or <x>.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="image"> <xsd:annotation> <xsd:documentation>Indicates a inline image.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="pb"> <xsd:annotation> <xsd:documentation>Indicates a page break.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="lb"> <xsd:annotation> <xsd:documentation>Indicates a line break.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="mime-typeValueList"> <xsd:restriction base="xsd:string"> <xsd:pattern value="(text|multipart|message|application|image|audio|video|model)(/.+)*"/> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="datatypeValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'datatype'.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="asp"> <xsd:annotation> <xsd:documentation>Indicates Active Server Page data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="c"> <xsd:annotation> <xsd:documentation>Indicates C source file data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="cdf"> <xsd:annotation> <xsd:documentation>Indicates Channel Definition Format (CDF) data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="cfm"> <xsd:annotation> <xsd:documentation>Indicates ColdFusion data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="cpp"> <xsd:annotation> <xsd:documentation>Indicates C++ source file data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="csharp"> <xsd:annotation> <xsd:documentation>Indicates C-Sharp data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="cstring"> <xsd:annotation> <xsd:documentation>Indicates strings from C, ASM, and driver files data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="csv"> <xsd:annotation> <xsd:documentation>Indicates comma-separated values data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="database"> <xsd:annotation> <xsd:documentation>Indicates database data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="documentfooter"> <xsd:annotation> <xsd:documentation>Indicates portions of document that follows data and contains metadata.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="documentheader"> <xsd:annotation> <xsd:documentation>Indicates portions of document that precedes data and contains metadata.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="filedialog"> <xsd:annotation> <xsd:documentation>Indicates data from standard UI file operations dialogs (e.g., Open, Save, Save As, Export, Import).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="form"> <xsd:annotation> <xsd:documentation>Indicates standard user input screen data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="html"> <xsd:annotation> <xsd:documentation>Indicates HyperText Markup Language (HTML) data - document instance.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="htmlbody"> <xsd:annotation> <xsd:documentation>Indicates content within an HTML document’s <body> element.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="ini"> <xsd:annotation> <xsd:documentation>Indicates Windows INI file data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="interleaf"> <xsd:annotation> <xsd:documentation>Indicates Interleaf data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="javaclass"> <xsd:annotation> <xsd:documentation>Indicates Java source file data (extension '.java').</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="javapropertyresourcebundle"> <xsd:annotation> <xsd:documentation>Indicates Java property resource bundle data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="javalistresourcebundle"> <xsd:annotation> <xsd:documentation>Indicates Java list resource bundle data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="javascript"> <xsd:annotation> <xsd:documentation>Indicates JavaScript source file data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="jscript"> <xsd:annotation> <xsd:documentation>Indicates JScript source file data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="layout"> <xsd:annotation> <xsd:documentation>Indicates information relating to formatting.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="lisp"> <xsd:annotation> <xsd:documentation>Indicates LISP source file data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="margin"> <xsd:annotation> <xsd:documentation>Indicates information relating to margin formats.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="menufile"> <xsd:annotation> <xsd:documentation>Indicates a file containing menu.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="messagefile"> <xsd:annotation> <xsd:documentation>Indicates numerically identified string table.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="mif"> <xsd:annotation> <xsd:documentation>Indicates Maker Interchange Format (MIF) data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="mimetype"> <xsd:annotation> <xsd:documentation>Indicates that the datatype attribute value is a MIME Type value and is defined in the mime-type attribute.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="mo"> <xsd:annotation> <xsd:documentation>Indicates GNU Machine Object data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="msglib"> <xsd:annotation> <xsd:documentation>Indicates Message Librarian strings created by Novell's Message Librarian Tool.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="pagefooter"> <xsd:annotation> <xsd:documentation>Indicates information to be displayed at the bottom of each page of a document.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="pageheader"> <xsd:annotation> <xsd:documentation>Indicates information to be displayed at the top of each page of a document.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="parameters"> <xsd:annotation> <xsd:documentation>Indicates a list of property values (e.g., settings within INI files or preferences dialog).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="pascal"> <xsd:annotation> <xsd:documentation>Indicates Pascal source file data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="php"> <xsd:annotation> <xsd:documentation>Indicates Hypertext Preprocessor data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="plaintext"> <xsd:annotation> <xsd:documentation>Indicates plain text file (no formatting other than, possibly, wrapping).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="po"> <xsd:annotation> <xsd:documentation>Indicates GNU Portable Object file.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="report"> <xsd:annotation> <xsd:documentation>Indicates dynamically generated user defined document. e.g. Oracle Report, Crystal Report, etc.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="resources"> <xsd:annotation> <xsd:documentation>Indicates Windows .NET binary resources.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="resx"> <xsd:annotation> <xsd:documentation>Indicates Windows .NET Resources.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="rtf"> <xsd:annotation> <xsd:documentation>Indicates Rich Text Format (RTF) data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="sgml"> <xsd:annotation> <xsd:documentation>Indicates Standard Generalized Markup Language (SGML) data - document instance.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="sgmldtd"> <xsd:annotation> <xsd:documentation>Indicates Standard Generalized Markup Language (SGML) data - Document Type Definition (DTD).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="svg"> <xsd:annotation> <xsd:documentation>Indicates Scalable Vector Graphic (SVG) data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="vbscript"> <xsd:annotation> <xsd:documentation>Indicates VisualBasic Script source file.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="warning"> <xsd:annotation> <xsd:documentation>Indicates warning message.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="winres"> <xsd:annotation> <xsd:documentation>Indicates Windows (Win32) resources (i.e. resources extracted from an RC script, a message file, or a compiled file).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="xhtml"> <xsd:annotation> <xsd:documentation>Indicates Extensible HyperText Markup Language (XHTML) data - document instance.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="xml"> <xsd:annotation> <xsd:documentation>Indicates Extensible Markup Language (XML) data - document instance.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="xmldtd"> <xsd:annotation> <xsd:documentation>Indicates Extensible Markup Language (XML) data - Document Type Definition (DTD).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="xsl"> <xsd:annotation> <xsd:documentation>Indicates Extensible Stylesheet Language (XSL) data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="xul"> <xsd:annotation> <xsd:documentation>Indicates XUL elements.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="mtypeValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'mtype'.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="abbrev"> <xsd:annotation> <xsd:documentation>Indicates the marked text is an abbreviation.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="abbreviated-form"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.8: A term resulting from the omission of any part of the full term while designating the same concept.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="abbreviation"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.8.1: An abbreviated form of a simple term resulting from the omission of some of its letters (e.g. 'adj.' for 'adjective').</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="acronym"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.8.4: An abbreviated form of a term made up of letters from the full form of a multiword term strung together into a sequence pronounced only syllabically (e.g. 'radar' for 'radio detecting and ranging').</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="appellation"> <xsd:annotation> <xsd:documentation>ISO-12620: A proper-name term, such as the name of an agency or other proper entity.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="collocation"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.18.1: A recurrent word combination characterized by cohesion in that the components of the collocation must co-occur within an utterance or series of utterances, even though they do not necessarily have to maintain immediate proximity to one another.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="common-name"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.5: A synonym for an international scientific term that is used in general discourse in a given language.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="datetime"> <xsd:annotation> <xsd:documentation>Indicates the marked text is a date and/or time.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="equation"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.15: An expression used to represent a concept based on a statement that two mathematical expressions are, for instance, equal as identified by the equal sign (=), or assigned to one another by a similar sign.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="expanded-form"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.7: The complete representation of a term for which there is an abbreviated form.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="formula"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.14: Figures, symbols or the like used to express a concept briefly, such as a mathematical or chemical formula.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="head-term"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.1: The concept designation that has been chosen to head a terminological record.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="initialism"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.8.3: An abbreviated form of a term consisting of some of the initial letters of the words making up a multiword term or the term elements making up a compound term when these letters are pronounced individually (e.g. 'BSE' for 'bovine spongiform encephalopathy').</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="international-scientific-term"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.4: A term that is part of an international scientific nomenclature as adopted by an appropriate scientific body.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="internationalism"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.6: A term that has the same or nearly identical orthographic or phonemic form in many languages.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="logical-expression"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.16: An expression used to represent a concept based on mathematical or logical relations, such as statements of inequality, set relationships, Boolean operations, and the like.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="materials-management-unit"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.17: A unit to track object.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="name"> <xsd:annotation> <xsd:documentation>Indicates the marked text is a name.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="near-synonym"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.3: A term that represents the same or a very similar concept as another term in the same language, but for which interchangeability is limited to some contexts and inapplicable in others.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="part-number"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.17.2: A unique alphanumeric designation assigned to an object in a manufacturing system.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="phrase"> <xsd:annotation> <xsd:documentation>Indicates the marked text is a phrase.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="phraseological-unit"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.18: Any group of two or more words that form a unit, the meaning of which frequently cannot be deduced based on the combined sense of the words making up the phrase.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="protected"> <xsd:annotation> <xsd:documentation>Indicates the marked text should not be translated.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="romanized-form"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.12: A form of a term resulting from an operation whereby non-Latin writing systems are converted to the Latin alphabet.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="seg"> <xsd:annotation> <xsd:documentation>Indicates that the marked text represents a segment.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="set-phrase"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.18.2: A fixed, lexicalized phrase.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="short-form"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.8.2: A variant of a multiword term that includes fewer words than the full form of the term (e.g. 'Group of Twenty-four' for 'Intergovernmental Group of Twenty-four on International Monetary Affairs').</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="sku"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.17.1: Stock keeping unit, an inventory item identified by a unique alphanumeric designation assigned to an object in an inventory control system.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="standard-text"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.19: A fixed chunk of recurring text.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="symbol"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.13: A designation of a concept by letters, numerals, pictograms or any combination thereof.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="synonym"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.2: Any term that represents the same or a very similar concept as the main entry term in a term entry.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="synonymous-phrase"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.18.3: Phraseological unit in a language that expresses the same semantic content as another phrase in that same language.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="term"> <xsd:annotation> <xsd:documentation>Indicates the marked text is a term.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="transcribed-form"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.11: A form of a term resulting from an operation whereby the characters of one writing system are represented by characters from another writing system, taking into account the pronunciation of the characters converted.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="transliterated-form"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.10: A form of a term resulting from an operation whereby the characters of an alphabetic writing system are represented by characters from another alphabetic writing system.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="truncated-term"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.8.5: An abbreviated form of a term resulting from the omission of one or more term elements or syllables (e.g. 'flu' for 'influenza').</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="variant"> <xsd:annotation> <xsd:documentation>ISO-12620 2.1.9: One of the alternate forms of a term.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="restypeValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'restype'.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="auto3state"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC AUTO3STATE control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="autocheckbox"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC AUTOCHECKBOX control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="autoradiobutton"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC AUTORADIOBUTTON control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="bedit"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC BEDIT control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="bitmap"> <xsd:annotation> <xsd:documentation>Indicates a bitmap, for example a BITMAP resource in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="button"> <xsd:annotation> <xsd:documentation>Indicates a button object, for example a BUTTON control Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="caption"> <xsd:annotation> <xsd:documentation>Indicates a caption, such as the caption of a dialog box.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="cell"> <xsd:annotation> <xsd:documentation>Indicates the cell in a table, for example the content of the <td> element in HTML.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="checkbox"> <xsd:annotation> <xsd:documentation>Indicates check box object, for example a CHECKBOX control in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="checkboxmenuitem"> <xsd:annotation> <xsd:documentation>Indicates a menu item with an associated checkbox.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="checkedlistbox"> <xsd:annotation> <xsd:documentation>Indicates a list box, but with a check-box for each item.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="colorchooser"> <xsd:annotation> <xsd:documentation>Indicates a color selection dialog.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="combobox"> <xsd:annotation> <xsd:documentation>Indicates a combination of edit box and listbox object, for example a COMBOBOX control in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="comboboxexitem"> <xsd:annotation> <xsd:documentation>Indicates an initialization entry of an extended combobox DLGINIT resource block. (code 0x1234).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="comboboxitem"> <xsd:annotation> <xsd:documentation>Indicates an initialization entry of a combobox DLGINIT resource block (code 0x0403).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="component"> <xsd:annotation> <xsd:documentation>Indicates a UI base class element that cannot be represented by any other element.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="contextmenu"> <xsd:annotation> <xsd:documentation>Indicates a context menu.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="ctext"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC CTEXT control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="cursor"> <xsd:annotation> <xsd:documentation>Indicates a cursor, for example a CURSOR resource in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="datetimepicker"> <xsd:annotation> <xsd:documentation>Indicates a date/time picker.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="defpushbutton"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC DEFPUSHBUTTON control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="dialog"> <xsd:annotation> <xsd:documentation>Indicates a dialog box.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="dlginit"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC DLGINIT resource block.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="edit"> <xsd:annotation> <xsd:documentation>Indicates an edit box object, for example an EDIT control in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="file"> <xsd:annotation> <xsd:documentation>Indicates a filename.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="filechooser"> <xsd:annotation> <xsd:documentation>Indicates a file dialog.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="fn"> <xsd:annotation> <xsd:documentation>Indicates a footnote.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="font"> <xsd:annotation> <xsd:documentation>Indicates a font name.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="footer"> <xsd:annotation> <xsd:documentation>Indicates a footer.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="frame"> <xsd:annotation> <xsd:documentation>Indicates a frame object.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="grid"> <xsd:annotation> <xsd:documentation>Indicates a XUL grid element.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="groupbox"> <xsd:annotation> <xsd:documentation>Indicates a groupbox object, for example a GROUPBOX control in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="header"> <xsd:annotation> <xsd:documentation>Indicates a header item.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="heading"> <xsd:annotation> <xsd:documentation>Indicates a heading, such has the content of <h1>, <h2>, etc. in HTML.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="hedit"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC HEDIT control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="hscrollbar"> <xsd:annotation> <xsd:documentation>Indicates a horizontal scrollbar.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="icon"> <xsd:annotation> <xsd:documentation>Indicates an icon, for example an ICON resource in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="iedit"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC IEDIT control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="keywords"> <xsd:annotation> <xsd:documentation>Indicates keyword list, such as the content of the Keywords meta-data in HTML, or a K footnote in WinHelp RTF.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="label"> <xsd:annotation> <xsd:documentation>Indicates a label object.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="linklabel"> <xsd:annotation> <xsd:documentation>Indicates a label that is also a HTML link (not necessarily a URL).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="list"> <xsd:annotation> <xsd:documentation>Indicates a list (a group of list-items, for example an <ol> or <ul> element in HTML).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="listbox"> <xsd:annotation> <xsd:documentation>Indicates a listbox object, for example an LISTBOX control in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="listitem"> <xsd:annotation> <xsd:documentation>Indicates an list item (an entry in a list).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="ltext"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC LTEXT control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="menu"> <xsd:annotation> <xsd:documentation>Indicates a menu (a group of menu-items).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="menubar"> <xsd:annotation> <xsd:documentation>Indicates a toolbar containing one or more tope level menus.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="menuitem"> <xsd:annotation> <xsd:documentation>Indicates a menu item (an entry in a menu).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="menuseparator"> <xsd:annotation> <xsd:documentation>Indicates a XUL menuseparator element.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="message"> <xsd:annotation> <xsd:documentation>Indicates a message, for example an entry in a MESSAGETABLE resource in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="monthcalendar"> <xsd:annotation> <xsd:documentation>Indicates a calendar control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="numericupdown"> <xsd:annotation> <xsd:documentation>Indicates an edit box beside a spin control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="panel"> <xsd:annotation> <xsd:documentation>Indicates a catch all for rectangular areas.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="popupmenu"> <xsd:annotation> <xsd:documentation>Indicates a standalone menu not necessarily associated with a menubar.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="pushbox"> <xsd:annotation> <xsd:documentation>Indicates a pushbox object, for example a PUSHBOX control in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="pushbutton"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC PUSHBUTTON control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="radio"> <xsd:annotation> <xsd:documentation>Indicates a radio button object.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="radiobuttonmenuitem"> <xsd:annotation> <xsd:documentation>Indicates a menuitem with associated radio button.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="rcdata"> <xsd:annotation> <xsd:documentation>Indicates raw data resources for an application.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="row"> <xsd:annotation> <xsd:documentation>Indicates a row in a table.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="rtext"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC RTEXT control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="scrollpane"> <xsd:annotation> <xsd:documentation>Indicates a user navigable container used to show a portion of a document.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="separator"> <xsd:annotation> <xsd:documentation>Indicates a generic divider object (e.g. menu group separator).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="shortcut"> <xsd:annotation> <xsd:documentation>Windows accelerators, shortcuts in resource or property files.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="spinner"> <xsd:annotation> <xsd:documentation>Indicates a UI control to indicate process activity but not progress.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="splitter"> <xsd:annotation> <xsd:documentation>Indicates a splitter bar.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="state3"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC STATE3 control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="statusbar"> <xsd:annotation> <xsd:documentation>Indicates a window for providing feedback to the users, like 'read-only', etc.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="string"> <xsd:annotation> <xsd:documentation>Indicates a string, for example an entry in a STRINGTABLE resource in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="tabcontrol"> <xsd:annotation> <xsd:documentation>Indicates a layers of controls with a tab to select layers.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="table"> <xsd:annotation> <xsd:documentation>Indicates a display and edits regular two-dimensional tables of cells.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="textbox"> <xsd:annotation> <xsd:documentation>Indicates a XUL textbox element.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="togglebutton"> <xsd:annotation> <xsd:documentation>Indicates a UI button that can be toggled to on or off state.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="toolbar"> <xsd:annotation> <xsd:documentation>Indicates an array of controls, usually buttons.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="tooltip"> <xsd:annotation> <xsd:documentation>Indicates a pop up tool tip text.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="trackbar"> <xsd:annotation> <xsd:documentation>Indicates a bar with a pointer indicating a position within a certain range.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="tree"> <xsd:annotation> <xsd:documentation>Indicates a control that displays a set of hierarchical data.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="uri"> <xsd:annotation> <xsd:documentation>Indicates a URI (URN or URL).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="userbutton"> <xsd:annotation> <xsd:documentation>Indicates a Windows RC USERBUTTON control.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="usercontrol"> <xsd:annotation> <xsd:documentation>Indicates a user-defined control like CONTROL control in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="var"> <xsd:annotation> <xsd:documentation>Indicates the text of a variable.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="versioninfo"> <xsd:annotation> <xsd:documentation>Indicates version information about a resource like VERSIONINFO in Windows.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="vscrollbar"> <xsd:annotation> <xsd:documentation>Indicates a vertical scrollbar.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="window"> <xsd:annotation> <xsd:documentation>Indicates a graphical window.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="size-unitValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'size-unit'.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="byte"> <xsd:annotation> <xsd:documentation>Indicates a size in 8-bit bytes.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="char"> <xsd:annotation> <xsd:documentation>Indicates a size in Unicode characters.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="col"> <xsd:annotation> <xsd:documentation>Indicates a size in columns. Used for HTML text area.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="cm"> <xsd:annotation> <xsd:documentation>Indicates a size in centimeters.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="dlgunit"> <xsd:annotation> <xsd:documentation>Indicates a size in dialog units, as defined in Windows resources.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="em"> <xsd:annotation> <xsd:documentation>Indicates a size in 'font-size' units (as defined in CSS).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="ex"> <xsd:annotation> <xsd:documentation>Indicates a size in 'x-height' units (as defined in CSS).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="glyph"> <xsd:annotation> <xsd:documentation>Indicates a size in glyphs. A glyph is considered to be one or more combined Unicode characters that represent a single displayable text character. Sometimes referred to as a 'grapheme cluster'</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="in"> <xsd:annotation> <xsd:documentation>Indicates a size in inches.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="mm"> <xsd:annotation> <xsd:documentation>Indicates a size in millimeters.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="percent"> <xsd:annotation> <xsd:documentation>Indicates a size in percentage.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="pixel"> <xsd:annotation> <xsd:documentation>Indicates a size in pixels.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="point"> <xsd:annotation> <xsd:documentation>Indicates a size in point.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="row"> <xsd:annotation> <xsd:documentation>Indicates a size in rows. Used for HTML text area.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="stateValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'state'.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="final"> <xsd:annotation> <xsd:documentation>Indicates the terminating state.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="needs-adaptation"> <xsd:annotation> <xsd:documentation>Indicates only non-textual information needs adaptation.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="needs-l10n"> <xsd:annotation> <xsd:documentation>Indicates both text and non-textual information needs adaptation.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="needs-review-adaptation"> <xsd:annotation> <xsd:documentation>Indicates only non-textual information needs review.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="needs-review-l10n"> <xsd:annotation> <xsd:documentation>Indicates both text and non-textual information needs review.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="needs-review-translation"> <xsd:annotation> <xsd:documentation>Indicates that only the text of the item needs to be reviewed.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="needs-translation"> <xsd:annotation> <xsd:documentation>Indicates that the item needs to be translated.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="new"> <xsd:annotation> <xsd:documentation>Indicates that the item is new. For example, translation units that were not in a previous version of the document.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="signed-off"> <xsd:annotation> <xsd:documentation>Indicates that changes are reviewed and approved.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="translated"> <xsd:annotation> <xsd:documentation>Indicates that the item has been translated.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="state-qualifierValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'state-qualifier'.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="exact-match"> <xsd:annotation> <xsd:documentation>Indicates an exact match. An exact match occurs when a source text of a segment is exactly the same as the source text of a segment that was translated previously.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="fuzzy-match"> <xsd:annotation> <xsd:documentation>Indicates a fuzzy match. A fuzzy match occurs when a source text of a segment is very similar to the source text of a segment that was translated previously (e.g. when the difference is casing, a few changed words, white-space discripancy, etc.).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="id-match"> <xsd:annotation> <xsd:documentation>Indicates a match based on matching IDs (in addition to matching text).</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="leveraged-glossary"> <xsd:annotation> <xsd:documentation>Indicates a translation derived from a glossary.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="leveraged-inherited"> <xsd:annotation> <xsd:documentation>Indicates a translation derived from existing translation.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="leveraged-mt"> <xsd:annotation> <xsd:documentation>Indicates a translation derived from machine translation.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="leveraged-repository"> <xsd:annotation> <xsd:documentation>Indicates a translation derived from a translation repository.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="leveraged-tm"> <xsd:annotation> <xsd:documentation>Indicates a translation derived from a translation memory.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="mt-suggestion"> <xsd:annotation> <xsd:documentation>Indicates the translation is suggested by machine translation.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="rejected-grammar"> <xsd:annotation> <xsd:documentation>Indicates that the item has been rejected because of incorrect grammar.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="rejected-inaccurate"> <xsd:annotation> <xsd:documentation>Indicates that the item has been rejected because it is incorrect.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="rejected-length"> <xsd:annotation> <xsd:documentation>Indicates that the item has been rejected because it is too long or too short.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="rejected-spelling"> <xsd:annotation> <xsd:documentation>Indicates that the item has been rejected because of incorrect spelling.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="tm-suggestion"> <xsd:annotation> <xsd:documentation>Indicates the translation is suggested by translation memory.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="unitValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'unit'.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="word"> <xsd:annotation> <xsd:documentation>Refers to words.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="page"> <xsd:annotation> <xsd:documentation>Refers to pages.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="trans-unit"> <xsd:annotation> <xsd:documentation>Refers to <trans-unit> elements.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="bin-unit"> <xsd:annotation> <xsd:documentation>Refers to <bin-unit> elements.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="glyph"> <xsd:annotation> <xsd:documentation>Refers to glyphs.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="item"> <xsd:annotation> <xsd:documentation>Refers to <trans-unit> and/or <bin-unit> elements.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="instance"> <xsd:annotation> <xsd:documentation>Refers to the occurrences of instances defined by the count-type value.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="character"> <xsd:annotation> <xsd:documentation>Refers to characters.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="line"> <xsd:annotation> <xsd:documentation>Refers to lines.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="sentence"> <xsd:annotation> <xsd:documentation>Refers to sentences.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="paragraph"> <xsd:annotation> <xsd:documentation>Refers to paragraphs.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="segment"> <xsd:annotation> <xsd:documentation>Refers to segments.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="placeable"> <xsd:annotation> <xsd:documentation>Refers to placeables (inline elements).</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="priorityValueList"> <xsd:annotation> <xsd:documentation>Values for the attribute 'priority'.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:positiveInteger"> <xsd:enumeration value="1"> <xsd:annotation> <xsd:documentation>Highest priority.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="2"> <xsd:annotation> <xsd:documentation>High priority.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="3"> <xsd:annotation> <xsd:documentation>High priority, but not as important as 2.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="4"> <xsd:annotation> <xsd:documentation>High priority, but not as important as 3.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="5"> <xsd:annotation> <xsd:documentation>Medium priority, but more important than 6.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="6"> <xsd:annotation> <xsd:documentation>Medium priority, but less important than 5.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="7"> <xsd:annotation> <xsd:documentation>Low priority, but more important than 8.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="8"> <xsd:annotation> <xsd:documentation>Low priority, but more important than 9.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="9"> <xsd:annotation> <xsd:documentation>Low priority.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="10"> <xsd:annotation> <xsd:documentation>Lowest priority.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="reformatValueYesNo"> <xsd:restriction base="xsd:string"> <xsd:enumeration value="yes"> <xsd:annotation> <xsd:documentation>This value indicates that all properties can be reformatted. This value must be used alone.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="no"> <xsd:annotation> <xsd:documentation>This value indicates that no properties should be reformatted. This value must be used alone.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="reformatValueList"> <xsd:list> <xsd:simpleType> <xsd:union memberTypes="xlf:XTend"> <xsd:simpleType> <xsd:restriction base="xsd:string"> <xsd:enumeration value="coord"> <xsd:annotation> <xsd:documentation>This value indicates that all information in the coord attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="coord-x"> <xsd:annotation> <xsd:documentation>This value indicates that the x information in the coord attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="coord-y"> <xsd:annotation> <xsd:documentation>This value indicates that the y information in the coord attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="coord-cx"> <xsd:annotation> <xsd:documentation>This value indicates that the cx information in the coord attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="coord-cy"> <xsd:annotation> <xsd:documentation>This value indicates that the cy information in the coord attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="font"> <xsd:annotation> <xsd:documentation>This value indicates that all the information in the font attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="font-name"> <xsd:annotation> <xsd:documentation>This value indicates that the name information in the font attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="font-size"> <xsd:annotation> <xsd:documentation>This value indicates that the size information in the font attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="font-weight"> <xsd:annotation> <xsd:documentation>This value indicates that the weight information in the font attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="css-style"> <xsd:annotation> <xsd:documentation>This value indicates that the information in the css-style attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="style"> <xsd:annotation> <xsd:documentation>This value indicates that the information in the style attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="ex-style"> <xsd:annotation> <xsd:documentation>This value indicates that the information in the exstyle attribute can be modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> </xsd:union> </xsd:simpleType> </xsd:list> </xsd:simpleType> <xsd:simpleType name="purposeValueList"> <xsd:restriction base="xsd:string"> <xsd:enumeration value="information"> <xsd:annotation> <xsd:documentation>Indicates that the context is informational in nature, specifying for example, how a term should be translated. Thus, should be displayed to anyone editing the XLIFF document.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="location"> <xsd:annotation> <xsd:documentation>Indicates that the context-group is used to specify where the term was found in the translatable source. Thus, it is not displayed.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="match"> <xsd:annotation> <xsd:documentation>Indicates that the context information should be used during translation memory lookups. Thus, it is not displayed.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="alttranstypeValueList"> <xsd:restriction base="xsd:string"> <xsd:enumeration value="proposal"> <xsd:annotation> <xsd:documentation>Represents a translation proposal from a translation memory or other resource.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="previous-version"> <xsd:annotation> <xsd:documentation>Represents a previous version of the target element.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="rejected"> <xsd:annotation> <xsd:documentation>Represents a rejected version of the target element.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="reference"> <xsd:annotation> <xsd:documentation>Represents a translation to be used for reference purposes only, for example from a related product or a different language.</xsd:documentation> </xsd:annotation> </xsd:enumeration> <xsd:enumeration value="accepted"> <xsd:annotation> <xsd:documentation>Represents a proposed translation that was used for the translation of the trans-unit, possibly modified.</xsd:documentation> </xsd:annotation> </xsd:enumeration> </xsd:restriction> </xsd:simpleType> <!-- Other Types --> <xsd:complexType name="ElemType_ExternalReference"> <xsd:choice> <xsd:element ref="xlf:internal-file"/> <xsd:element ref="xlf:external-file"/> </xsd:choice> </xsd:complexType> <xsd:simpleType name="AttrType_purpose"> <xsd:list> <xsd:simpleType> <xsd:union memberTypes="xlf:purposeValueList xlf:XTend"/> </xsd:simpleType> </xsd:list> </xsd:simpleType> <xsd:simpleType name="AttrType_datatype"> <xsd:union memberTypes="xlf:datatypeValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_restype"> <xsd:union memberTypes="xlf:restypeValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_alttranstype"> <xsd:union memberTypes="xlf:alttranstypeValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_context-type"> <xsd:union memberTypes="xlf:context-typeValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_state"> <xsd:union memberTypes="xlf:stateValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_state-qualifier"> <xsd:union memberTypes="xlf:state-qualifierValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_count-type"> <xsd:union memberTypes="xlf:restypeValueList xlf:count-typeValueList xlf:datatypeValueList xlf:stateValueList xlf:state-qualifierValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_InlineDelimiters"> <xsd:union memberTypes="xlf:InlineDelimitersValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_InlinePlaceholders"> <xsd:union memberTypes="xlf:InlinePlaceholdersValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_size-unit"> <xsd:union memberTypes="xlf:size-unitValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_mtype"> <xsd:union memberTypes="xlf:mtypeValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_unit"> <xsd:union memberTypes="xlf:unitValueList xlf:XTend"/> </xsd:simpleType> <xsd:simpleType name="AttrType_priority"> <xsd:union memberTypes="xlf:priorityValueList"/> </xsd:simpleType> <xsd:simpleType name="AttrType_reformat"> <xsd:union memberTypes="xlf:reformatValueYesNo xlf:reformatValueList"/> </xsd:simpleType> <xsd:simpleType name="AttrType_YesNo"> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="yes"/> <xsd:enumeration value="no"/> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="AttrType_Position"> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="open"/> <xsd:enumeration value="close"/> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="AttrType_assoc"> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="preceding"/> <xsd:enumeration value="following"/> <xsd:enumeration value="both"/> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="AttrType_annotates"> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="source"/> <xsd:enumeration value="target"/> <xsd:enumeration value="general"/> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="AttrType_Coordinates"> <xsd:annotation> <xsd:documentation>Values for the attribute 'coord'.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:string"> <xsd:pattern value="(-?\d+|#);(-?\d+|#);(-?\d+|#);(-?\d+|#)"/> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="AttrType_Version"> <xsd:annotation> <xsd:documentation>Version values: 1.0 and 1.1 are allowed for backward compatibility.</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:string"> <xsd:enumeration value="1.2"/> <xsd:enumeration value="1.1"/> <xsd:enumeration value="1.0"/> </xsd:restriction> </xsd:simpleType> <!-- Groups --> <xsd:group name="ElemGroup_TextContent"> <xsd:choice> <xsd:element ref="xlf:g"/> <xsd:element ref="xlf:bpt"/> <xsd:element ref="xlf:ept"/> <xsd:element ref="xlf:ph"/> <xsd:element ref="xlf:it"/> <xsd:element ref="xlf:mrk"/> <xsd:element ref="xlf:x"/> <xsd:element ref="xlf:bx"/> <xsd:element ref="xlf:ex"/> </xsd:choice> </xsd:group> <xsd:attributeGroup name="AttrGroup_TextContent"> <xsd:attribute name="id" type="xsd:string" use="required"/> <xsd:attribute name="xid" type="xsd:string" use="optional"/> <xsd:attribute name="equiv-text" type="xsd:string" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:attributeGroup> <!-- XLIFF Structure --> <xsd:element name="xliff"> <xsd:complexType> <xsd:sequence maxOccurs="unbounded"> <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/> <xsd:element ref="xlf:file"/> </xsd:sequence> <xsd:attribute name="version" type="xlf:AttrType_Version" use="required"/> <xsd:attribute ref="xml:lang" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> </xsd:element> <xsd:element name="file"> <xsd:complexType> <xsd:sequence> <xsd:element minOccurs="0" ref="xlf:header"/> <xsd:element ref="xlf:body"/> </xsd:sequence> <xsd:attribute name="original" type="xsd:string" use="required"/> <xsd:attribute name="source-language" type="xsd:language" use="required"/> <xsd:attribute name="datatype" type="xlf:AttrType_datatype" use="required"/> <xsd:attribute name="tool-id" type="xsd:string" use="optional"/> <xsd:attribute name="date" type="xsd:dateTime" use="optional"/> <xsd:attribute ref="xml:space" use="optional"/> <xsd:attribute name="category" type="xsd:string" use="optional"/> <xsd:attribute name="target-language" type="xsd:language" use="optional"/> <xsd:attribute name="product-name" type="xsd:string" use="optional"/> <xsd:attribute name="product-version" type="xsd:string" use="optional"/> <xsd:attribute name="build-num" type="xsd:string" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> <xsd:unique name="U_group_id"> <xsd:selector xpath=".//xlf:group"/> <xsd:field xpath="@id"/> </xsd:unique> <xsd:key name="K_unit_id"> <xsd:selector xpath=".//xlf:trans-unit|.//xlf:bin-unit"/> <xsd:field xpath="@id"/> </xsd:key> <xsd:keyref name="KR_unit_id" refer="xlf:K_unit_id"> <xsd:selector xpath=".//bpt|.//ept|.//it|.//ph|.//g|.//x|.//bx|.//ex|.//sub"/> <xsd:field xpath="@xid"/> </xsd:keyref> <xsd:key name="K_tool-id"> <xsd:selector xpath="xlf:header/xlf:tool"/> <xsd:field xpath="@tool-id"/> </xsd:key> <xsd:keyref name="KR_file_tool-id" refer="xlf:K_tool-id"> <xsd:selector xpath="."/> <xsd:field xpath="@tool-id"/> </xsd:keyref> <xsd:keyref name="KR_phase_tool-id" refer="xlf:K_tool-id"> <xsd:selector xpath="xlf:header/xlf:phase-group/xlf:phase"/> <xsd:field xpath="@tool-id"/> </xsd:keyref> <xsd:keyref name="KR_alt-trans_tool-id" refer="xlf:K_tool-id"> <xsd:selector xpath=".//xlf:trans-unit/xlf:alt-trans"/> <xsd:field xpath="@tool-id"/> </xsd:keyref> <xsd:key name="K_count-group_name"> <xsd:selector xpath=".//xlf:count-group"/> <xsd:field xpath="@name"/> </xsd:key> <xsd:unique name="U_context-group_name"> <xsd:selector xpath=".//xlf:context-group"/> <xsd:field xpath="@name"/> </xsd:unique> <xsd:key name="K_phase-name"> <xsd:selector xpath="xlf:header/xlf:phase-group/xlf:phase"/> <xsd:field xpath="@phase-name"/> </xsd:key> <xsd:keyref name="KR_phase-name" refer="xlf:K_phase-name"> <xsd:selector xpath=".//xlf:count|.//xlf:trans-unit|.//xlf:target|.//bin-unit|.//bin-target"/> <xsd:field xpath="@phase-name"/> </xsd:keyref> <xsd:unique name="U_uid"> <xsd:selector xpath=".//xlf:external-file"/> <xsd:field xpath="@uid"/> </xsd:unique> </xsd:element> <xsd:element name="header"> <xsd:complexType> <xsd:sequence> <xsd:element minOccurs="0" name="skl" type="xlf:ElemType_ExternalReference"/> <xsd:element minOccurs="0" ref="xlf:phase-group"/> <xsd:choice maxOccurs="unbounded" minOccurs="0"> <xsd:element name="glossary" type="xlf:ElemType_ExternalReference"/> <xsd:element name="reference" type="xlf:ElemType_ExternalReference"/> <xsd:element ref="xlf:count-group"/> <xsd:element ref="xlf:note"/> <xsd:element ref="xlf:tool"/> </xsd:choice> <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="internal-file"> <xsd:complexType> <xsd:simpleContent> <xsd:extension base="xsd:string"> <xsd:attribute name="form" type="xsd:string"/> <xsd:attribute name="crc" type="xsd:NMTOKEN"/> </xsd:extension> </xsd:simpleContent> </xsd:complexType> </xsd:element> <xsd:element name="external-file"> <xsd:complexType> <xsd:attribute name="href" type="xsd:string" use="required"/> <xsd:attribute name="crc" type="xsd:NMTOKEN"/> <xsd:attribute name="uid" type="xsd:NMTOKEN"/> </xsd:complexType> </xsd:element> <xsd:element name="note"> <xsd:complexType> <xsd:simpleContent> <xsd:extension base="xsd:string"> <xsd:attribute ref="xml:lang" use="optional"/> <xsd:attribute default="1" name="priority" type="xlf:AttrType_priority" use="optional"/> <xsd:attribute name="from" type="xsd:string" use="optional"/> <xsd:attribute default="general" name="annotates" type="xlf:AttrType_annotates" use="optional"/> </xsd:extension> </xsd:simpleContent> </xsd:complexType> </xsd:element> <xsd:element name="phase-group"> <xsd:complexType> <xsd:sequence maxOccurs="unbounded"> <xsd:element ref="xlf:phase"/> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="phase"> <xsd:complexType> <xsd:sequence maxOccurs="unbounded" minOccurs="0"> <xsd:element ref="xlf:note"/> </xsd:sequence> <xsd:attribute name="phase-name" type="xsd:string" use="required"/> <xsd:attribute name="process-name" type="xsd:string" use="required"/> <xsd:attribute name="company-name" type="xsd:string" use="optional"/> <xsd:attribute name="tool-id" type="xsd:string" use="optional"/> <xsd:attribute name="date" type="xsd:dateTime" use="optional"/> <xsd:attribute name="job-id" type="xsd:string" use="optional"/> <xsd:attribute name="contact-name" type="xsd:string" use="optional"/> <xsd:attribute name="contact-email" type="xsd:string" use="optional"/> <xsd:attribute name="contact-phone" type="xsd:string" use="optional"/> </xsd:complexType> </xsd:element> <xsd:element name="count-group"> <xsd:complexType> <xsd:sequence maxOccurs="unbounded" minOccurs="0"> <xsd:element ref="xlf:count"/> </xsd:sequence> <xsd:attribute name="name" type="xsd:string" use="required"/> </xsd:complexType> </xsd:element> <xsd:element name="count"> <xsd:complexType> <xsd:simpleContent> <xsd:extension base="xsd:string"> <xsd:attribute name="count-type" type="xlf:AttrType_count-type" use="optional"/> <xsd:attribute name="phase-name" type="xsd:string" use="optional"/> <xsd:attribute default="word" name="unit" type="xlf:AttrType_unit" use="optional"/> </xsd:extension> </xsd:simpleContent> </xsd:complexType> </xsd:element> <xsd:element name="context-group"> <xsd:complexType> <xsd:sequence maxOccurs="unbounded"> <xsd:element ref="xlf:context"/> </xsd:sequence> <xsd:attribute name="name" type="xsd:string" use="optional"/> <xsd:attribute name="crc" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="purpose" type="xlf:AttrType_purpose" use="optional"/> </xsd:complexType> </xsd:element> <xsd:element name="context"> <xsd:complexType> <xsd:simpleContent> <xsd:extension base="xsd:string"> <xsd:attribute name="context-type" type="xlf:AttrType_context-type" use="required"/> <xsd:attribute default="no" name="match-mandatory" type="xlf:AttrType_YesNo" use="optional"/> <xsd:attribute name="crc" type="xsd:NMTOKEN" use="optional"/> </xsd:extension> </xsd:simpleContent> </xsd:complexType> </xsd:element> <xsd:element name="tool"> <xsd:complexType mixed="true"> <xsd:sequence> <xsd:any namespace="##any" processContents="strict" minOccurs="0" maxOccurs="unbounded"/> </xsd:sequence> <xsd:attribute name="tool-id" type="xsd:string" use="required"/> <xsd:attribute name="tool-name" type="xsd:string" use="required"/> <xsd:attribute name="tool-version" type="xsd:string" use="optional"/> <xsd:attribute name="tool-company" type="xsd:string" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> </xsd:element> <xsd:element name="body"> <xsd:complexType> <xsd:choice maxOccurs="unbounded" minOccurs="0"> <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:group"/> <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:trans-unit"/> <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:bin-unit"/> </xsd:choice> </xsd:complexType> </xsd:element> <xsd:element name="group"> <xsd:complexType> <xsd:sequence> <xsd:sequence> <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:context-group"/> <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:count-group"/> <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:note"/> <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/> </xsd:sequence> <xsd:choice maxOccurs="unbounded"> <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:group"/> <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:trans-unit"/> <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:bin-unit"/> </xsd:choice> </xsd:sequence> <xsd:attribute name="id" type="xsd:string" use="optional"/> <xsd:attribute name="datatype" type="xlf:AttrType_datatype" use="optional"/> <xsd:attribute default="default" ref="xml:space" use="optional"/> <xsd:attribute name="restype" type="xlf:AttrType_restype" use="optional"/> <xsd:attribute name="resname" type="xsd:string" use="optional"/> <xsd:attribute name="extradata" type="xsd:string" use="optional"/> <xsd:attribute name="extype" type="xsd:string" use="optional"/> <xsd:attribute name="help-id" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="menu" type="xsd:string" use="optional"/> <xsd:attribute name="menu-option" type="xsd:string" use="optional"/> <xsd:attribute name="menu-name" type="xsd:string" use="optional"/> <xsd:attribute name="coord" type="xlf:AttrType_Coordinates" use="optional"/> <xsd:attribute name="font" type="xsd:string" use="optional"/> <xsd:attribute name="css-style" type="xsd:string" use="optional"/> <xsd:attribute name="style" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="exstyle" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute default="yes" name="translate" type="xlf:AttrType_YesNo" use="optional"/> <xsd:attribute default="yes" name="reformat" type="xlf:AttrType_reformat" use="optional"/> <xsd:attribute default="pixel" name="size-unit" type="xlf:AttrType_size-unit" use="optional"/> <xsd:attribute name="maxwidth" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="minwidth" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="maxheight" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="minheight" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="maxbytes" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="minbytes" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="charclass" type="xsd:string" use="optional"/> <xsd:attribute default="no" name="merged-trans" type="xlf:AttrType_YesNo" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> </xsd:element> <xsd:element name="trans-unit"> <xsd:complexType> <xsd:sequence> <xsd:element ref="xlf:source"/> <xsd:element minOccurs="0" ref="xlf:seg-source"/> <xsd:element minOccurs="0" ref="xlf:target"/> <xsd:choice maxOccurs="unbounded" minOccurs="0"> <xsd:element ref="xlf:context-group"/> <xsd:element ref="xlf:count-group"/> <xsd:element ref="xlf:note"/> <xsd:element ref="xlf:alt-trans"/> </xsd:choice> <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/> </xsd:sequence> <xsd:attribute name="id" type="xsd:string" use="required"/> <xsd:attribute name="approved" type="xlf:AttrType_YesNo" use="optional"/> <xsd:attribute default="yes" name="translate" type="xlf:AttrType_YesNo" use="optional"/> <xsd:attribute default="yes" name="reformat" type="xlf:AttrType_reformat" use="optional"/> <xsd:attribute default="default" ref="xml:space" use="optional"/> <xsd:attribute name="datatype" type="xlf:AttrType_datatype" use="optional"/> <xsd:attribute name="phase-name" type="xsd:string" use="optional"/> <xsd:attribute name="restype" type="xlf:AttrType_restype" use="optional"/> <xsd:attribute name="resname" type="xsd:string" use="optional"/> <xsd:attribute name="extradata" type="xsd:string" use="optional"/> <xsd:attribute name="extype" type="xsd:string" use="optional"/> <xsd:attribute name="help-id" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="menu" type="xsd:string" use="optional"/> <xsd:attribute name="menu-option" type="xsd:string" use="optional"/> <xsd:attribute name="menu-name" type="xsd:string" use="optional"/> <xsd:attribute name="coord" type="xlf:AttrType_Coordinates" use="optional"/> <xsd:attribute name="font" type="xsd:string" use="optional"/> <xsd:attribute name="css-style" type="xsd:string" use="optional"/> <xsd:attribute name="style" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="exstyle" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute default="pixel" name="size-unit" type="xlf:AttrType_size-unit" use="optional"/> <xsd:attribute name="maxwidth" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="minwidth" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="maxheight" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="minheight" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="maxbytes" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="minbytes" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="charclass" type="xsd:string" use="optional"/> <xsd:attribute default="yes" name="merged-trans" type="xlf:AttrType_YesNo" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> <xsd:unique name="U_tu_segsrc_mid"> <xsd:selector xpath="./xlf:seg-source/xlf:mrk"/> <xsd:field xpath="@mid"/> </xsd:unique> <xsd:keyref name="KR_tu_segsrc_mid" refer="xlf:U_tu_segsrc_mid"> <xsd:selector xpath="./xlf:target/xlf:mrk|./xlf:alt-trans"/> <xsd:field xpath="@mid"/> </xsd:keyref> </xsd:element> <xsd:element name="source"> <xsd:complexType mixed="true"> <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/> <xsd:attribute ref="xml:lang" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> <xsd:unique name="U_source_bpt_rid"> <xsd:selector xpath=".//xlf:bpt"/> <xsd:field xpath="@rid"/> </xsd:unique> <xsd:keyref name="KR_source_ept_rid" refer="xlf:U_source_bpt_rid"> <xsd:selector xpath=".//xlf:ept"/> <xsd:field xpath="@rid"/> </xsd:keyref> <xsd:unique name="U_source_bx_rid"> <xsd:selector xpath=".//xlf:bx"/> <xsd:field xpath="@rid"/> </xsd:unique> <xsd:keyref name="KR_source_ex_rid" refer="xlf:U_source_bx_rid"> <xsd:selector xpath=".//xlf:ex"/> <xsd:field xpath="@rid"/> </xsd:keyref> </xsd:element> <xsd:element name="seg-source"> <xsd:complexType mixed="true"> <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/> <xsd:attribute ref="xml:lang" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> <xsd:unique name="U_segsrc_bpt_rid"> <xsd:selector xpath=".//xlf:bpt"/> <xsd:field xpath="@rid"/> </xsd:unique> <xsd:keyref name="KR_segsrc_ept_rid" refer="xlf:U_segsrc_bpt_rid"> <xsd:selector xpath=".//xlf:ept"/> <xsd:field xpath="@rid"/> </xsd:keyref> <xsd:unique name="U_segsrc_bx_rid"> <xsd:selector xpath=".//xlf:bx"/> <xsd:field xpath="@rid"/> </xsd:unique> <xsd:keyref name="KR_segsrc_ex_rid" refer="xlf:U_segsrc_bx_rid"> <xsd:selector xpath=".//xlf:ex"/> <xsd:field xpath="@rid"/> </xsd:keyref> </xsd:element> <xsd:element name="target"> <xsd:complexType mixed="true"> <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/> <xsd:attribute name="state" type="xlf:AttrType_state" use="optional"/> <xsd:attribute name="state-qualifier" type="xlf:AttrType_state-qualifier" use="optional"/> <xsd:attribute name="phase-name" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute ref="xml:lang" use="optional"/> <xsd:attribute name="resname" type="xsd:string" use="optional"/> <xsd:attribute name="coord" type="xlf:AttrType_Coordinates" use="optional"/> <xsd:attribute name="font" type="xsd:string" use="optional"/> <xsd:attribute name="css-style" type="xsd:string" use="optional"/> <xsd:attribute name="style" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="exstyle" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute default="yes" name="equiv-trans" type="xlf:AttrType_YesNo" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> <xsd:unique name="U_target_bpt_rid"> <xsd:selector xpath=".//xlf:bpt"/> <xsd:field xpath="@rid"/> </xsd:unique> <xsd:keyref name="KR_target_ept_rid" refer="xlf:U_target_bpt_rid"> <xsd:selector xpath=".//xlf:ept"/> <xsd:field xpath="@rid"/> </xsd:keyref> <xsd:unique name="U_target_bx_rid"> <xsd:selector xpath=".//xlf:bx"/> <xsd:field xpath="@rid"/> </xsd:unique> <xsd:keyref name="KR_target_ex_rid" refer="xlf:U_target_bx_rid"> <xsd:selector xpath=".//xlf:ex"/> <xsd:field xpath="@rid"/> </xsd:keyref> </xsd:element> <xsd:element name="alt-trans"> <xsd:complexType> <xsd:sequence> <xsd:element minOccurs="0" ref="xlf:source"/> <xsd:element minOccurs="0" ref="xlf:seg-source"/> <xsd:element maxOccurs="1" ref="xlf:target"/> <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:context-group"/> <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:note"/> <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/> </xsd:sequence> <xsd:attribute name="match-quality" type="xsd:string" use="optional"/> <xsd:attribute name="tool-id" type="xsd:string" use="optional"/> <xsd:attribute name="crc" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute ref="xml:lang" use="optional"/> <xsd:attribute name="origin" type="xsd:string" use="optional"/> <xsd:attribute name="datatype" type="xlf:AttrType_datatype" use="optional"/> <xsd:attribute default="default" ref="xml:space" use="optional"/> <xsd:attribute name="restype" type="xlf:AttrType_restype" use="optional"/> <xsd:attribute name="resname" type="xsd:string" use="optional"/> <xsd:attribute name="extradata" type="xsd:string" use="optional"/> <xsd:attribute name="extype" type="xsd:string" use="optional"/> <xsd:attribute name="help-id" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="menu" type="xsd:string" use="optional"/> <xsd:attribute name="menu-option" type="xsd:string" use="optional"/> <xsd:attribute name="menu-name" type="xsd:string" use="optional"/> <xsd:attribute name="mid" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="coord" type="xlf:AttrType_Coordinates" use="optional"/> <xsd:attribute name="font" type="xsd:string" use="optional"/> <xsd:attribute name="css-style" type="xsd:string" use="optional"/> <xsd:attribute name="style" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="exstyle" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="phase-name" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute default="proposal" name="alttranstype" type="xlf:AttrType_alttranstype" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> <xsd:unique name="U_at_segsrc_mid"> <xsd:selector xpath="./xlf:seg-source/xlf:mrk"/> <xsd:field xpath="@mid"/> </xsd:unique> <xsd:keyref name="KR_at_segsrc_mid" refer="xlf:U_at_segsrc_mid"> <xsd:selector xpath="./xlf:target/xlf:mrk"/> <xsd:field xpath="@mid"/> </xsd:keyref> </xsd:element> <xsd:element name="bin-unit"> <xsd:complexType> <xsd:sequence> <xsd:element ref="xlf:bin-source"/> <xsd:element minOccurs="0" ref="xlf:bin-target"/> <xsd:choice maxOccurs="unbounded" minOccurs="0"> <xsd:element ref="xlf:context-group"/> <xsd:element ref="xlf:count-group"/> <xsd:element ref="xlf:note"/> <xsd:element ref="xlf:trans-unit"/> </xsd:choice> <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/> </xsd:sequence> <xsd:attribute name="id" type="xsd:string" use="required"/> <xsd:attribute name="mime-type" type="xlf:mime-typeValueList" use="required"/> <xsd:attribute name="approved" type="xlf:AttrType_YesNo" use="optional"/> <xsd:attribute default="yes" name="translate" type="xlf:AttrType_YesNo" use="optional"/> <xsd:attribute default="yes" name="reformat" type="xlf:AttrType_reformat" use="optional"/> <xsd:attribute name="restype" type="xlf:AttrType_restype" use="optional"/> <xsd:attribute name="resname" type="xsd:string" use="optional"/> <xsd:attribute name="phase-name" type="xsd:string" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> </xsd:element> <xsd:element name="bin-source"> <xsd:complexType> <xsd:choice> <xsd:element ref="xlf:internal-file"/> <xsd:element ref="xlf:external-file"/> </xsd:choice> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> </xsd:element> <xsd:element name="bin-target"> <xsd:complexType> <xsd:choice> <xsd:element ref="xlf:internal-file"/> <xsd:element ref="xlf:external-file"/> </xsd:choice> <xsd:attribute name="mime-type" type="xlf:mime-typeValueList" use="optional"/> <xsd:attribute name="state" type="xlf:AttrType_state" use="optional"/> <xsd:attribute name="state-qualifier" type="xlf:AttrType_state-qualifier" use="optional"/> <xsd:attribute name="phase-name" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="restype" type="xlf:AttrType_restype" use="optional"/> <xsd:attribute name="resname" type="xsd:string" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> </xsd:element> <!-- Element for inline codes --> <xsd:element name="g"> <xsd:complexType mixed="true"> <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/> <xsd:attribute name="ctype" type="xlf:AttrType_InlineDelimiters" use="optional"/> <xsd:attribute default="yes" name="clone" type="xlf:AttrType_YesNo" use="optional"/> <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/> </xsd:complexType> </xsd:element> <xsd:element name="x"> <xsd:complexType> <xsd:attribute name="ctype" type="xlf:AttrType_InlinePlaceholders" use="optional"/> <xsd:attribute default="yes" name="clone" type="xlf:AttrType_YesNo" use="optional"/> <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/> </xsd:complexType> </xsd:element> <xsd:element name="bx"> <xsd:complexType> <xsd:attribute name="rid" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="ctype" type="xlf:AttrType_InlineDelimiters" use="optional"/> <xsd:attribute default="yes" name="clone" type="xlf:AttrType_YesNo" use="optional"/> <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/> </xsd:complexType> </xsd:element> <xsd:element name="ex"> <xsd:complexType> <xsd:attribute name="rid" type="xsd:NMTOKEN" use="optional"/> <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/> </xsd:complexType> </xsd:element> <xsd:element name="ph"> <xsd:complexType mixed="true"> <xsd:sequence maxOccurs="unbounded" minOccurs="0"> <xsd:element ref="xlf:sub"/> </xsd:sequence> <xsd:attribute name="ctype" type="xlf:AttrType_InlinePlaceholders" use="optional"/> <xsd:attribute name="crc" type="xsd:string" use="optional"/> <xsd:attribute name="assoc" type="xlf:AttrType_assoc" use="optional"/> <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/> </xsd:complexType> </xsd:element> <xsd:element name="bpt"> <xsd:complexType mixed="true"> <xsd:sequence maxOccurs="unbounded" minOccurs="0"> <xsd:element ref="xlf:sub"/> </xsd:sequence> <xsd:attribute name="rid" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="ctype" type="xlf:AttrType_InlineDelimiters" use="optional"/> <xsd:attribute name="crc" type="xsd:string" use="optional"/> <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/> </xsd:complexType> </xsd:element> <xsd:element name="ept"> <xsd:complexType mixed="true"> <xsd:sequence maxOccurs="unbounded" minOccurs="0"> <xsd:element ref="xlf:sub"/> </xsd:sequence> <xsd:attribute name="rid" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="crc" type="xsd:string" use="optional"/> <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/> </xsd:complexType> </xsd:element> <xsd:element name="it"> <xsd:complexType mixed="true"> <xsd:sequence maxOccurs="unbounded" minOccurs="0"> <xsd:element ref="xlf:sub"/> </xsd:sequence> <xsd:attribute name="pos" type="xlf:AttrType_Position" use="required"/> <xsd:attribute name="rid" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="ctype" type="xlf:AttrType_InlineDelimiters" use="optional"/> <xsd:attribute name="crc" type="xsd:string" use="optional"/> <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/> </xsd:complexType> </xsd:element> <xsd:element name="sub"> <xsd:complexType mixed="true"> <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/> <xsd:attribute name="datatype" type="xlf:AttrType_datatype" use="optional"/> <xsd:attribute name="ctype" type="xlf:AttrType_InlineDelimiters" use="optional"/> <xsd:attribute name="xid" type="xsd:string" use="optional"/> </xsd:complexType> </xsd:element> <xsd:element name="mrk"> <xsd:complexType mixed="true"> <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/> <xsd:attribute name="mtype" type="xlf:AttrType_mtype" use="required"/> <xsd:attribute name="mid" type="xsd:NMTOKEN" use="optional"/> <xsd:attribute name="comment" type="xsd:string" use="optional"/> <xsd:anyAttribute namespace="##other" processContents="strict"/> </xsd:complexType> </xsd:element> </xsd:schema> translation/Resources/schemas/xml.xsd 0000644 00000021220 15025017654 0014021 0 ustar 00 <?xml version='1.0'?> <?xml-stylesheet href="../2008/09/xsd.xsl" type="text/xsl"?> <xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns ="http://www.w3.org/1999/xhtml" xml:lang="en"> <xs:annotation> <xs:documentation> <div> <h1>About the XML namespace</h1> <div class="bodytext"> <p> This schema document describes the XML namespace, in a form suitable for import by other schema documents. </p> <p> See <a href="http://www.w3.org/XML/1998/namespace.html"> http://www.w3.org/XML/1998/namespace.html</a> and <a href="http://www.w3.org/TR/REC-xml"> http://www.w3.org/TR/REC-xml</a> for information about this namespace. </p> <p> Note that local names in this namespace are intended to be defined only by the World Wide Web Consortium or its subgroups. The names currently defined in this namespace are listed below. They should not be used with conflicting semantics by any Working Group, specification, or document instance. </p> <p> See further below in this document for more information about <a href="#usage">how to refer to this schema document from your own XSD schema documents</a> and about <a href="#nsversioning">the namespace-versioning policy governing this schema document</a>. </p> </div> </div> </xs:documentation> </xs:annotation> <xs:attribute name="lang"> <xs:annotation> <xs:documentation> <div> <h3>lang (as an attribute name)</h3> <p> denotes an attribute whose value is a language code for the natural language of the content of any element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.</p> </div> <div> <h4>Notes</h4> <p> Attempting to install the relevant ISO 2- and 3-letter codes as the enumerated possible values is probably never going to be a realistic possibility. </p> <p> See BCP 47 at <a href="http://www.rfc-editor.org/rfc/bcp/bcp47.txt"> http://www.rfc-editor.org/rfc/bcp/bcp47.txt</a> and the IANA language subtag registry at <a href="http://www.iana.org/assignments/language-subtag-registry"> http://www.iana.org/assignments/language-subtag-registry</a> for further information. </p> <p> The union allows for the 'un-declaration' of xml:lang with the empty string. </p> </div> </xs:documentation> </xs:annotation> <xs:simpleType> <xs:union memberTypes="xs:language"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value=""/> </xs:restriction> </xs:simpleType> </xs:union> </xs:simpleType> </xs:attribute> <xs:attribute name="space"> <xs:annotation> <xs:documentation> <div> <h3>space (as an attribute name)</h3> <p> denotes an attribute whose value is a keyword indicating what whitespace processing discipline is intended for the content of the element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.</p> </div> </xs:documentation> </xs:annotation> <xs:simpleType> <xs:restriction base="xs:NCName"> <xs:enumeration value="default"/> <xs:enumeration value="preserve"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="base" type="xs:anyURI"> <xs:annotation> <xs:documentation> <div> <h3>base (as an attribute name)</h3> <p> denotes an attribute whose value provides a URI to be used as the base for interpreting any relative URIs in the scope of the element on which it appears; its value is inherited. This name is reserved by virtue of its definition in the XML Base specification.</p> <p> See <a href="http://www.w3.org/TR/xmlbase/">http://www.w3.org/TR/xmlbase/</a> for information about this attribute. </p> </div> </xs:documentation> </xs:annotation> </xs:attribute> <xs:attribute name="id" type="xs:ID"> <xs:annotation> <xs:documentation> <div> <h3>id (as an attribute name)</h3> <p> denotes an attribute whose value should be interpreted as if declared to be of type ID. This name is reserved by virtue of its definition in the xml:id specification.</p> <p> See <a href="http://www.w3.org/TR/xml-id/">http://www.w3.org/TR/xml-id/</a> for information about this attribute. </p> </div> </xs:documentation> </xs:annotation> </xs:attribute> <xs:attributeGroup name="specialAttrs"> <xs:attribute ref="xml:base"/> <xs:attribute ref="xml:lang"/> <xs:attribute ref="xml:space"/> <xs:attribute ref="xml:id"/> </xs:attributeGroup> <xs:annotation> <xs:documentation> <div> <h3>Father (in any context at all)</h3> <div class="bodytext"> <p> denotes Jon Bosak, the chair of the original XML Working Group. This name is reserved by the following decision of the W3C XML Plenary and XML Coordination groups: </p> <blockquote> <p> In appreciation for his vision, leadership and dedication the W3C XML Plenary on this 10th day of February, 2000, reserves for Jon Bosak in perpetuity the XML name "xml:Father". </p> </blockquote> </div> </div> </xs:documentation> </xs:annotation> <xs:annotation> <xs:documentation> <div xml:id="usage" id="usage"> <h2><a name="usage">About this schema document</a></h2> <div class="bodytext"> <p> This schema defines attributes and an attribute group suitable for use by schemas wishing to allow <code>xml:base</code>, <code>xml:lang</code>, <code>xml:space</code> or <code>xml:id</code> attributes on elements they define. </p> <p> To enable this, such a schema must import this schema for the XML namespace, e.g. as follows: </p> <pre> <schema.. .> .. . <import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2001/xml.xsd"/> </pre> <p> or </p> <pre> <import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2009/01/xml.xsd"/> </pre> <p> Subsequently, qualified reference to any of the attributes or the group defined below will have the desired effect, e.g. </p> <pre> <type.. .> .. . <attributeGroup ref="xml:specialAttrs"/> </pre> <p> will define a type which will schema-validate an instance element with any of those attributes. </p> </div> </div> </xs:documentation> </xs:annotation> <xs:annotation> <xs:documentation> <div id="nsversioning" xml:id="nsversioning"> <h2><a name="nsversioning">Versioning policy for this schema document</a></h2> <div class="bodytext"> <p> In keeping with the XML Schema WG's standard versioning policy, this schema document will persist at <a href="http://www.w3.org/2009/01/xml.xsd"> http://www.w3.org/2009/01/xml.xsd</a>. </p> <p> At the date of issue it can also be found at <a href="http://www.w3.org/2001/xml.xsd"> http://www.w3.org/2001/xml.xsd</a>. </p> <p> The schema document at that URI may however change in the future, in order to remain compatible with the latest version of XML Schema itself, or with the XML namespace itself. In other words, if the XML Schema or XML namespaces change, the version of this document at <a href="http://www.w3.org/2001/xml.xsd"> http://www.w3.org/2001/xml.xsd </a> will change accordingly; the version at <a href="http://www.w3.org/2009/01/xml.xsd"> http://www.w3.org/2009/01/xml.xsd </a> will not change. </p> <p> Previous dated (and unchanging) versions of this schema document are at: </p> <ul> <li><a href="http://www.w3.org/2009/01/xml.xsd"> http://www.w3.org/2009/01/xml.xsd</a></li> <li><a href="http://www.w3.org/2007/08/xml.xsd"> http://www.w3.org/2007/08/xml.xsd</a></li> <li><a href="http://www.w3.org/2004/10/xml.xsd"> http://www.w3.org/2004/10/xml.xsd</a></li> <li><a href="http://www.w3.org/2001/03/xml.xsd"> http://www.w3.org/2001/03/xml.xsd</a></li> </ul> </div> </div> </xs:documentation> </xs:annotation> </xs:schema> translation/Resources/data/parents.json 0000644 00000006163 15025017654 0014347 0 ustar 00 { "az_Cyrl": "root", "bs_Cyrl": "root", "en_150": "en_001", "en_AG": "en_001", "en_AI": "en_001", "en_AT": "en_150", "en_AU": "en_001", "en_BB": "en_001", "en_BE": "en_150", "en_BM": "en_001", "en_BS": "en_001", "en_BW": "en_001", "en_BZ": "en_001", "en_CC": "en_001", "en_CH": "en_150", "en_CK": "en_001", "en_CM": "en_001", "en_CX": "en_001", "en_CY": "en_001", "en_DE": "en_150", "en_DG": "en_001", "en_DK": "en_150", "en_DM": "en_001", "en_ER": "en_001", "en_FI": "en_150", "en_FJ": "en_001", "en_FK": "en_001", "en_FM": "en_001", "en_GB": "en_001", "en_GD": "en_001", "en_GG": "en_001", "en_GH": "en_001", "en_GI": "en_001", "en_GM": "en_001", "en_GY": "en_001", "en_HK": "en_001", "en_IE": "en_001", "en_IL": "en_001", "en_IM": "en_001", "en_IN": "en_001", "en_IO": "en_001", "en_JE": "en_001", "en_JM": "en_001", "en_KE": "en_001", "en_KI": "en_001", "en_KN": "en_001", "en_KY": "en_001", "en_LC": "en_001", "en_LR": "en_001", "en_LS": "en_001", "en_MG": "en_001", "en_MO": "en_001", "en_MS": "en_001", "en_MT": "en_001", "en_MU": "en_001", "en_MV": "en_001", "en_MW": "en_001", "en_MY": "en_001", "en_NA": "en_001", "en_NF": "en_001", "en_NG": "en_001", "en_NL": "en_150", "en_NR": "en_001", "en_NU": "en_001", "en_NZ": "en_001", "en_PG": "en_001", "en_PK": "en_001", "en_PN": "en_001", "en_PW": "en_001", "en_RW": "en_001", "en_SB": "en_001", "en_SC": "en_001", "en_SD": "en_001", "en_SE": "en_150", "en_SG": "en_001", "en_SH": "en_001", "en_SI": "en_150", "en_SL": "en_001", "en_SS": "en_001", "en_SX": "en_001", "en_SZ": "en_001", "en_TC": "en_001", "en_TK": "en_001", "en_TO": "en_001", "en_TT": "en_001", "en_TV": "en_001", "en_TZ": "en_001", "en_UG": "en_001", "en_VC": "en_001", "en_VG": "en_001", "en_VU": "en_001", "en_WS": "en_001", "en_ZA": "en_001", "en_ZM": "en_001", "en_ZW": "en_001", "es_AR": "es_419", "es_BO": "es_419", "es_BR": "es_419", "es_BZ": "es_419", "es_CL": "es_419", "es_CO": "es_419", "es_CR": "es_419", "es_CU": "es_419", "es_DO": "es_419", "es_EC": "es_419", "es_GT": "es_419", "es_HN": "es_419", "es_MX": "es_419", "es_NI": "es_419", "es_PA": "es_419", "es_PE": "es_419", "es_PR": "es_419", "es_PY": "es_419", "es_SV": "es_419", "es_US": "es_419", "es_UY": "es_419", "es_VE": "es_419", "ff_Adlm": "root", "hi_Latn": "en_IN", "ks_Deva": "root", "nb": "no", "nn": "no", "pa_Arab": "root", "pt_AO": "pt_PT", "pt_CH": "pt_PT", "pt_CV": "pt_PT", "pt_GQ": "pt_PT", "pt_GW": "pt_PT", "pt_LU": "pt_PT", "pt_MO": "pt_PT", "pt_MZ": "pt_PT", "pt_ST": "pt_PT", "pt_TL": "pt_PT", "sd_Deva": "root", "sr_Latn": "root", "uz_Arab": "root", "uz_Cyrl": "root", "zh_Hant": "root", "zh_Hant_MO": "zh_Hant_HK" } translation/Resources/functions.php 0000644 00000001062 15025017654 0013601 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; if (!\function_exists(t::class)) { /** * @author Nate Wiebe <nate@northern.co> */ function t(string $message, array $parameters = [], string $domain = null): TranslatableMessage { return new TranslatableMessage($message, $parameters, $domain); } } translation/LoggingTranslator.php 0000644 00000007263 15025017654 0013270 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Psr\Log\LoggerInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> */ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface { private $translator; private $logger; /** * @param TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator The translator must implement TranslatorBagInterface */ public function __construct(TranslatorInterface $translator, LoggerInterface $logger) { if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) { throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator))); } $this->translator = $translator; $this->logger = $logger; } /** * {@inheritdoc} */ public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null): string { $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); $this->log($id, $domain, $locale); return $trans; } /** * {@inheritdoc} */ public function setLocale(string $locale) { $prev = $this->translator->getLocale(); $this->translator->setLocale($locale); if ($prev === $locale) { return; } $this->logger->debug(sprintf('The locale of the translator has changed from "%s" to "%s".', $prev, $locale)); } /** * {@inheritdoc} */ public function getLocale(): string { return $this->translator->getLocale(); } /** * {@inheritdoc} */ public function getCatalogue(string $locale = null): MessageCatalogueInterface { return $this->translator->getCatalogue($locale); } /** * {@inheritdoc} */ public function getCatalogues(): array { return $this->translator->getCatalogues(); } /** * Gets the fallback locales. */ public function getFallbackLocales(): array { if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) { return $this->translator->getFallbackLocales(); } return []; } /** * Passes through all unknown calls onto the translator object. */ public function __call(string $method, array $args) { return $this->translator->{$method}(...$args); } /** * Logs for missing translations. */ private function log(string $id, ?string $domain, ?string $locale) { if (null === $domain) { $domain = 'messages'; } $catalogue = $this->translator->getCatalogue($locale); if ($catalogue->defines($id, $domain)) { return; } if ($catalogue->has($id, $domain)) { $this->logger->debug('Translation use fallback catalogue.', ['id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale()]); } else { $this->logger->warning('Translation not found.', ['id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale()]); } } } string/LazyString.php 0000644 00000010454 15025017654 0010702 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; /** * A string whose value is computed lazily by a callback. * * @author Nicolas Grekas <p@tchwork.com> */ class LazyString implements \Stringable, \JsonSerializable { private \Closure|string $value; /** * @param callable|array $callback A callable or a [Closure, method] lazy-callable */ public static function fromCallable(callable|array $callback, mixed ...$arguments): static { if (\is_array($callback) && !\is_callable($callback) && !(($callback[0] ?? null) instanceof \Closure || 2 < \count($callback))) { throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a callable or a [Closure, method] lazy-callable, "%s" given.', __METHOD__, '['.implode(', ', array_map('get_debug_type', $callback)).']')); } $lazyString = new static(); $lazyString->value = static function () use (&$callback, &$arguments, &$value): string { if (null !== $arguments) { if (!\is_callable($callback)) { $callback[0] = $callback[0](); $callback[1] = $callback[1] ?? '__invoke'; } $value = $callback(...$arguments); $callback = self::getPrettyName($callback); $arguments = null; } return $value ?? ''; }; return $lazyString; } public static function fromStringable(string|int|float|bool|\Stringable $value): static { if (\is_object($value)) { return static::fromCallable([$value, '__toString']); } $lazyString = new static(); $lazyString->value = (string) $value; return $lazyString; } /** * Tells whether the provided value can be cast to string. */ final public static function isStringable(mixed $value): bool { return \is_string($value) || $value instanceof \Stringable || \is_scalar($value); } /** * Casts scalars and stringable objects to strings. * * @throws \TypeError When the provided value is not stringable */ final public static function resolve(\Stringable|string|int|float|bool $value): string { return $value; } public function __toString(): string { if (\is_string($this->value)) { return $this->value; } try { return $this->value = ($this->value)(); } catch (\Throwable $e) { if (\TypeError::class === \get_class($e) && __FILE__ === $e->getFile()) { $type = explode(', ', $e->getMessage()); $type = substr(array_pop($type), 0, -\strlen(' returned')); $r = new \ReflectionFunction($this->value); $callback = $r->getStaticVariables()['callback']; $e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type)); } throw $e; } } public function __sleep(): array { $this->__toString(); return ['value']; } public function jsonSerialize(): string { return $this->__toString(); } private function __construct() { } private static function getPrettyName(callable $callback): string { if (\is_string($callback)) { return $callback; } if (\is_array($callback)) { $class = \is_object($callback[0]) ? get_debug_type($callback[0]) : $callback[0]; $method = $callback[1]; } elseif ($callback instanceof \Closure) { $r = new \ReflectionFunction($callback); if (false !== strpos($r->name, '{closure}') || !$class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { return $r->name; } $class = $class->name; $method = $r->name; } else { $class = get_debug_type($callback); $method = '__invoke'; } return $class.'::'.$method; } } string/CodePointString.php 0000644 00000017054 15025017654 0011652 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; /** * Represents a string of Unicode code points encoded as UTF-8. * * @author Nicolas Grekas <p@tchwork.com> * @author Hugo Hamon <hugohamon@neuf.fr> * * @throws ExceptionInterface */ class CodePointString extends AbstractUnicodeString { public function __construct(string $string = '') { if ('' !== $string && !preg_match('//u', $string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $this->string = $string; } public function append(string ...$suffix): static { $str = clone $this; $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix); if (!preg_match('//u', $str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function chunk(int $length = 1): array { if (1 > $length) { throw new InvalidArgumentException('The chunk length must be greater than zero.'); } if ('' === $this->string) { return []; } $rx = '/('; while (65535 < $length) { $rx .= '.{65535}'; $length -= 65535; } $rx .= '.{'.$length.'})/us'; $str = clone $this; $chunks = []; foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) { $str->string = $chunk; $chunks[] = clone $str; } return $chunks; } public function codePointsAt(int $offset): array { $str = $offset ? $this->slice($offset, 1) : $this; return '' === $str->string ? [] : [mb_ord($str->string, 'UTF-8')]; } public function endsWith(string|iterable|AbstractString $suffix): bool { if ($suffix instanceof AbstractString) { $suffix = $suffix->string; } elseif (!\is_string($suffix)) { return parent::endsWith($suffix); } if ('' === $suffix || !preg_match('//u', $suffix)) { return false; } if ($this->ignoreCase) { return preg_match('{'.preg_quote($suffix).'$}iuD', $this->string); } return \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix)); } public function equalsTo(string|iterable|AbstractString $string): bool { if ($string instanceof AbstractString) { $string = $string->string; } elseif (!\is_string($string)) { return parent::equalsTo($string); } if ('' !== $string && $this->ignoreCase) { return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); } return $string === $this->string; } public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOf($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? mb_stripos($this->string, $needle, $offset, 'UTF-8') : mb_strpos($this->string, $needle, $offset, 'UTF-8'); return false === $i ? null : $i; } public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOfLast($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? mb_strripos($this->string, $needle, $offset, 'UTF-8') : mb_strrpos($this->string, $needle, $offset, 'UTF-8'); return false === $i ? null : $i; } public function length(): int { return mb_strlen($this->string, 'UTF-8'); } public function prepend(string ...$prefix): static { $str = clone $this; $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string; if (!preg_match('//u', $str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function replace(string $from, string $to): static { $str = clone $this; if ('' === $from || !preg_match('//u', $from)) { return $str; } if ('' !== $to && !preg_match('//u', $to)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } if ($this->ignoreCase) { $str->string = implode($to, preg_split('{'.preg_quote($from).'}iuD', $this->string)); } else { $str->string = str_replace($from, $to, $this->string); } return $str; } public function slice(int $start = 0, int $length = null): static { $str = clone $this; $str->string = mb_substr($this->string, $start, $length, 'UTF-8'); return $str; } public function splice(string $replacement, int $start = 0, int $length = null): static { if (!preg_match('//u', $replacement)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $str = clone $this; $start = $start ? \strlen(mb_substr($this->string, 0, $start, 'UTF-8')) : 0; $length = $length ? \strlen(mb_substr($this->string, $start, $length, 'UTF-8')) : $length; $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); return $str; } public function split(string $delimiter, int $limit = null, int $flags = null): array { if (1 > $limit = $limit ?? \PHP_INT_MAX) { throw new InvalidArgumentException('Split limit must be a positive integer.'); } if ('' === $delimiter) { throw new InvalidArgumentException('Split delimiter is empty.'); } if (null !== $flags) { return parent::split($delimiter.'u', $limit, $flags); } if (!preg_match('//u', $delimiter)) { throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.'); } $str = clone $this; $chunks = $this->ignoreCase ? preg_split('{'.preg_quote($delimiter).'}iuD', $this->string, $limit) : explode($delimiter, $this->string, $limit); foreach ($chunks as &$chunk) { $str->string = $chunk; $chunk = clone $str; } return $chunks; } public function startsWith(string|iterable|AbstractString $prefix): bool { if ($prefix instanceof AbstractString) { $prefix = $prefix->string; } elseif (!\is_string($prefix)) { return parent::startsWith($prefix); } if ('' === $prefix || !preg_match('//u', $prefix)) { return false; } if ($this->ignoreCase) { return 0 === mb_stripos($this->string, $prefix, 0, 'UTF-8'); } return 0 === strncmp($this->string, $prefix, \strlen($prefix)); } } string/composer.json 0000644 00000002465 15025017654 0010610 0 ustar 00 { "name": "symfony/string", "type": "library", "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", "keywords": ["string", "utf8", "utf-8", "grapheme", "i18n", "unicode"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.0.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { "symfony/error-handler": "^5.4|^6.0", "symfony/http-client": "^5.4|^6.0", "symfony/translation-contracts": "^2.0|^3.0", "symfony/var-exporter": "^5.4|^6.0" }, "conflict": { "symfony/translation-contracts": "<2.0" }, "autoload": { "psr-4": { "Symfony\\Component\\String\\": "" }, "files": [ "Resources/functions.php" ], "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } string/CHANGELOG.md 0000644 00000001504 15025017654 0007670 0 ustar 00 CHANGELOG ========= 5.4 --- * Add `trimSuffix()` and `trimPrefix()` methods 5.3 --- * Made `AsciiSlugger` fallback to parent locale's symbolsMap 5.2.0 ----- * added a `FrenchInflector` class 5.1.0 ----- * added the `AbstractString::reverse()` method * made `AbstractString::width()` follow POSIX.1-2001 * added `LazyString` which provides memoizing stringable objects * The component is not marked as `@experimental` anymore * added the `s()` helper method to get either an `UnicodeString` or `ByteString` instance, depending of the input string UTF-8 compliancy * added `$cut` parameter to `Symfony\Component\String\AbstractString::truncate()` * added `AbstractString::containsAny()` * allow passing a string of custom characters to `ByteString::fromRandom()` 5.0.0 ----- * added the component as experimental string/UnicodeString.php 0000644 00000027710 15025017654 0011354 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; /** * Represents a string of Unicode grapheme clusters encoded as UTF-8. * * A letter followed by combining characters (accents typically) form what Unicode defines * as a grapheme cluster: a character as humans mean it in written texts. This class knows * about the concept and won't split a letter apart from its combining accents. It also * ensures all string comparisons happen on their canonically-composed representation, * ignoring e.g. the order in which accents are listed when a letter has many of them. * * @see https://unicode.org/reports/tr15/ * * @author Nicolas Grekas <p@tchwork.com> * @author Hugo Hamon <hugohamon@neuf.fr> * * @throws ExceptionInterface */ class UnicodeString extends AbstractUnicodeString { public function __construct(string $string = '') { $this->string = normalizer_is_normalized($string) ? $string : normalizer_normalize($string); if (false === $this->string) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } } public function append(string ...$suffix): static { $str = clone $this; $str->string = $this->string.(1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix)); normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); if (false === $str->string) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function chunk(int $length = 1): array { if (1 > $length) { throw new InvalidArgumentException('The chunk length must be greater than zero.'); } if ('' === $this->string) { return []; } $rx = '/('; while (65535 < $length) { $rx .= '\X{65535}'; $length -= 65535; } $rx .= '\X{'.$length.'})/u'; $str = clone $this; $chunks = []; foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) { $str->string = $chunk; $chunks[] = clone $str; } return $chunks; } public function endsWith(string|iterable|AbstractString $suffix): bool { if ($suffix instanceof AbstractString) { $suffix = $suffix->string; } elseif (!\is_string($suffix)) { return parent::endsWith($suffix); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($suffix, $form) ?: $suffix = normalizer_normalize($suffix, $form); if ('' === $suffix || false === $suffix) { return false; } if ($this->ignoreCase) { return 0 === mb_stripos(grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)), $suffix, 0, 'UTF-8'); } return $suffix === grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)); } public function equalsTo(string|iterable|AbstractString $string): bool { if ($string instanceof AbstractString) { $string = $string->string; } elseif (!\is_string($string)) { return parent::equalsTo($string); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($string, $form) ?: $string = normalizer_normalize($string, $form); if ('' !== $string && false !== $string && $this->ignoreCase) { return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); } return $string === $this->string; } public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOf($needle, $offset); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form); if ('' === $needle || false === $needle) { return null; } try { $i = $this->ignoreCase ? grapheme_stripos($this->string, $needle, $offset) : grapheme_strpos($this->string, $needle, $offset); } catch (\ValueError $e) { return null; } return false === $i ? null : $i; } public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOfLast($needle, $offset); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form); if ('' === $needle || false === $needle) { return null; } $string = $this->string; if (0 > $offset) { // workaround https://bugs.php.net/74264 if (0 > $offset += grapheme_strlen($needle)) { $string = grapheme_substr($string, 0, $offset); } $offset = 0; } $i = $this->ignoreCase ? grapheme_strripos($string, $needle, $offset) : grapheme_strrpos($string, $needle, $offset); return false === $i ? null : $i; } public function join(array $strings, string $lastGlue = null): static { $str = parent::join($strings, $lastGlue); normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); return $str; } public function length(): int { return grapheme_strlen($this->string); } public function normalize(int $form = self::NFC): static { $str = clone $this; if (\in_array($form, [self::NFC, self::NFKC], true)) { normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form); } elseif (!\in_array($form, [self::NFD, self::NFKD], true)) { throw new InvalidArgumentException('Unsupported normalization form.'); } elseif (!normalizer_is_normalized($str->string, $form)) { $str->string = normalizer_normalize($str->string, $form); $str->ignoreCase = null; } return $str; } public function prepend(string ...$prefix): static { $str = clone $this; $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string; normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); if (false === $str->string) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function replace(string $from, string $to): static { $str = clone $this; normalizer_is_normalized($from) ?: $from = normalizer_normalize($from); if ('' !== $from && false !== $from) { $tail = $str->string; $result = ''; $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos'; while ('' !== $tail && false !== $i = $indexOf($tail, $from)) { $slice = grapheme_substr($tail, 0, $i); $result .= $slice.$to; $tail = substr($tail, \strlen($slice) + \strlen($from)); } $str->string = $result.$tail; normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); if (false === $str->string) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } } return $str; } public function replaceMatches(string $fromRegexp, string|callable $to): static { $str = parent::replaceMatches($fromRegexp, $to); normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); return $str; } public function slice(int $start = 0, int $length = null): static { $str = clone $this; $str->string = (string) grapheme_substr($this->string, $start, $length ?? 2147483647); return $str; } public function splice(string $replacement, int $start = 0, int $length = null): static { $str = clone $this; $start = $start ? \strlen(grapheme_substr($this->string, 0, $start)) : 0; $length = $length ? \strlen(grapheme_substr($this->string, $start, $length ?? 2147483647)) : $length; $str->string = substr_replace($this->string, $replacement, $start, $length ?? 2147483647); normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); if (false === $str->string) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function split(string $delimiter, int $limit = null, int $flags = null): array { if (1 > $limit = $limit ?? 2147483647) { throw new InvalidArgumentException('Split limit must be a positive integer.'); } if ('' === $delimiter) { throw new InvalidArgumentException('Split delimiter is empty.'); } if (null !== $flags) { return parent::split($delimiter.'u', $limit, $flags); } normalizer_is_normalized($delimiter) ?: $delimiter = normalizer_normalize($delimiter); if (false === $delimiter) { throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.'); } $str = clone $this; $tail = $this->string; $chunks = []; $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos'; while (1 < $limit && false !== $i = $indexOf($tail, $delimiter)) { $str->string = grapheme_substr($tail, 0, $i); $chunks[] = clone $str; $tail = substr($tail, \strlen($str->string) + \strlen($delimiter)); --$limit; } $str->string = $tail; $chunks[] = clone $str; return $chunks; } public function startsWith(string|iterable|AbstractString $prefix): bool { if ($prefix instanceof AbstractString) { $prefix = $prefix->string; } elseif (!\is_string($prefix)) { return parent::startsWith($prefix); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($prefix, $form) ?: $prefix = normalizer_normalize($prefix, $form); if ('' === $prefix || false === $prefix) { return false; } if ($this->ignoreCase) { return 0 === mb_stripos(grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES), $prefix, 0, 'UTF-8'); } return $prefix === grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES); } public function __wakeup() { if (!\is_string($this->string)) { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string); } public function __clone() { if (null === $this->ignoreCase) { normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string); } $this->ignoreCase = false; } } string/AbstractUnicodeString.php 0000644 00000064772 15025017654 0013051 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; use Symfony\Component\String\Exception\RuntimeException; /** * Represents a string of abstract Unicode characters. * * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters). * This class is the abstract type to use as a type-hint when the logic you want to * implement is Unicode-aware but doesn't care about code points vs grapheme clusters. * * @author Nicolas Grekas <p@tchwork.com> * * @throws ExceptionInterface */ abstract class AbstractUnicodeString extends AbstractString { public const NFC = \Normalizer::NFC; public const NFD = \Normalizer::NFD; public const NFKC = \Normalizer::NFKC; public const NFKD = \Normalizer::NFKD; // all ASCII letters sorted by typical frequency of occurrence private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; // the subset of folded case mappings that is not in lower case mappings private const FOLD_FROM = ['İ', 'µ', 'ſ', "\xCD\x85", 'ς', 'ϐ', 'ϑ', 'ϕ', 'ϖ', 'ϰ', 'ϱ', 'ϵ', 'ẛ', "\xE1\xBE\xBE", 'ß', 'İ', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'և', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'ẚ', 'ẞ', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ᾀ', 'ᾁ', 'ᾂ', 'ᾃ', 'ᾄ', 'ᾅ', 'ᾆ', 'ᾇ', 'ᾈ', 'ᾉ', 'ᾊ', 'ᾋ', 'ᾌ', 'ᾍ', 'ᾎ', 'ᾏ', 'ᾐ', 'ᾑ', 'ᾒ', 'ᾓ', 'ᾔ', 'ᾕ', 'ᾖ', 'ᾗ', 'ᾘ', 'ᾙ', 'ᾚ', 'ᾛ', 'ᾜ', 'ᾝ', 'ᾞ', 'ᾟ', 'ᾠ', 'ᾡ', 'ᾢ', 'ᾣ', 'ᾤ', 'ᾥ', 'ᾦ', 'ᾧ', 'ᾨ', 'ᾩ', 'ᾪ', 'ᾫ', 'ᾬ', 'ᾭ', 'ᾮ', 'ᾯ', 'ᾲ', 'ᾳ', 'ᾴ', 'ᾶ', 'ᾷ', 'ᾼ', 'ῂ', 'ῃ', 'ῄ', 'ῆ', 'ῇ', 'ῌ', 'ῒ', 'ΐ', 'ῖ', 'ῗ', 'ῢ', 'ΰ', 'ῤ', 'ῦ', 'ῧ', 'ῲ', 'ῳ', 'ῴ', 'ῶ', 'ῷ', 'ῼ', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'ſt', 'st', 'ﬓ', 'ﬔ', 'ﬕ', 'ﬖ', 'ﬗ']; private const FOLD_TO = ['i̇', 'μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', 'ṡ', 'ι', 'ss', 'i̇', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'եւ', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'aʾ', 'ss', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὰι', 'αι', 'άι', 'ᾶ', 'ᾶι', 'αι', 'ὴι', 'ηι', 'ήι', 'ῆ', 'ῆι', 'ηι', 'ῒ', 'ΐ', 'ῖ', 'ῗ', 'ῢ', 'ΰ', 'ῤ', 'ῦ', 'ῧ', 'ὼι', 'ωι', 'ώι', 'ῶ', 'ῶι', 'ωι', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'st', 'st', 'մն', 'մե', 'մի', 'վն', 'մխ']; // the subset of upper case mappings that map one code point to many code points private const UPPER_FROM = ['ß', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'ſt', 'st', 'և', 'ﬓ', 'ﬔ', 'ﬕ', 'ﬖ', 'ﬗ', 'ʼn', 'ΐ', 'ΰ', 'ǰ', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'ẚ', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ᾶ', 'ῆ', 'ῒ', 'ΐ', 'ῖ', 'ῗ', 'ῢ', 'ΰ', 'ῤ', 'ῦ', 'ῧ', 'ῶ']; private const UPPER_TO = ['SS', 'FF', 'FI', 'FL', 'FFI', 'FFL', 'ST', 'ST', 'ԵՒ', 'ՄՆ', 'ՄԵ', 'ՄԻ', 'ՎՆ', 'ՄԽ', 'ʼN', 'Ϊ́', 'Ϋ́', 'J̌', 'H̱', 'T̈', 'W̊', 'Y̊', 'Aʾ', 'Υ̓', 'Υ̓̀', 'Υ̓́', 'Υ̓͂', 'Α͂', 'Η͂', 'Ϊ̀', 'Ϊ́', 'Ι͂', 'Ϊ͂', 'Ϋ̀', 'Ϋ́', 'Ρ̓', 'Υ͂', 'Ϋ͂', 'Ω͂']; // the subset of https://github.com/unicode-org/cldr/blob/master/common/transforms/Latin-ASCII.xml that is not in NFKD private const TRANSLIT_FROM = ['Æ', 'Ð', 'Ø', 'Þ', 'ß', 'æ', 'ð', 'ø', 'þ', 'Đ', 'đ', 'Ħ', 'ħ', 'ı', 'ĸ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'ʼn', 'Ŋ', 'ŋ', 'Œ', 'œ', 'Ŧ', 'ŧ', 'ƀ', 'Ɓ', 'Ƃ', 'ƃ', 'Ƈ', 'ƈ', 'Ɖ', 'Ɗ', 'Ƌ', 'ƌ', 'Ɛ', 'Ƒ', 'ƒ', 'Ɠ', 'ƕ', 'Ɩ', 'Ɨ', 'Ƙ', 'ƙ', 'ƚ', 'Ɲ', 'ƞ', 'Ƣ', 'ƣ', 'Ƥ', 'ƥ', 'ƫ', 'Ƭ', 'ƭ', 'Ʈ', 'Ʋ', 'Ƴ', 'ƴ', 'Ƶ', 'ƶ', 'DŽ', 'Dž', 'dž', 'Ǥ', 'ǥ', 'ȡ', 'Ȥ', 'ȥ', 'ȴ', 'ȵ', 'ȶ', 'ȷ', 'ȸ', 'ȹ', 'Ⱥ', 'Ȼ', 'ȼ', 'Ƚ', 'Ⱦ', 'ȿ', 'ɀ', 'Ƀ', 'Ʉ', 'Ɇ', 'ɇ', 'Ɉ', 'ɉ', 'Ɍ', 'ɍ', 'Ɏ', 'ɏ', 'ɓ', 'ɕ', 'ɖ', 'ɗ', 'ɛ', 'ɟ', 'ɠ', 'ɡ', 'ɢ', 'ɦ', 'ɧ', 'ɨ', 'ɪ', 'ɫ', 'ɬ', 'ɭ', 'ɱ', 'ɲ', 'ɳ', 'ɴ', 'ɶ', 'ɼ', 'ɽ', 'ɾ', 'ʀ', 'ʂ', 'ʈ', 'ʉ', 'ʋ', 'ʏ', 'ʐ', 'ʑ', 'ʙ', 'ʛ', 'ʜ', 'ʝ', 'ʟ', 'ʠ', 'ʣ', 'ʥ', 'ʦ', 'ʪ', 'ʫ', 'ᴀ', 'ᴁ', 'ᴃ', 'ᴄ', 'ᴅ', 'ᴆ', 'ᴇ', 'ᴊ', 'ᴋ', 'ᴌ', 'ᴍ', 'ᴏ', 'ᴘ', 'ᴛ', 'ᴜ', 'ᴠ', 'ᴡ', 'ᴢ', 'ᵫ', 'ᵬ', 'ᵭ', 'ᵮ', 'ᵯ', 'ᵰ', 'ᵱ', 'ᵲ', 'ᵳ', 'ᵴ', 'ᵵ', 'ᵶ', 'ᵺ', 'ᵻ', 'ᵽ', 'ᵾ', 'ᶀ', 'ᶁ', 'ᶂ', 'ᶃ', 'ᶄ', 'ᶅ', 'ᶆ', 'ᶇ', 'ᶈ', 'ᶉ', 'ᶊ', 'ᶌ', 'ᶍ', 'ᶎ', 'ᶏ', 'ᶑ', 'ᶒ', 'ᶓ', 'ᶖ', 'ᶙ', 'ẚ', 'ẜ', 'ẝ', 'ẞ', 'Ỻ', 'ỻ', 'Ỽ', 'ỽ', 'Ỿ', 'ỿ', '©', '®', '₠', '₢', '₣', '₤', '₧', '₺', '₹', 'ℌ', '℞', '㎧', '㎮', '㏆', '㏗', '㏞', '㏟', '¼', '½', '¾', '⅓', '⅔', '⅕', '⅖', '⅗', '⅘', '⅙', '⅚', '⅛', '⅜', '⅝', '⅞', '⅟', '〇', '‘', '’', '‚', '‛', '“', '”', '„', '‟', '′', '″', '〝', '〞', '«', '»', '‹', '›', '‐', '‑', '‒', '–', '—', '―', '︱', '︲', '﹘', '‖', '⁄', '⁅', '⁆', '⁎', '、', '。', '〈', '〉', '《', '》', '〔', '〕', '〘', '〙', '〚', '〛', '︑', '︒', '︹', '︺', '︽', '︾', '︿', '﹀', '﹑', '﹝', '﹞', '⦅', '⦆', '。', '、', '×', '÷', '−', '∕', '∖', '∣', '∥', '≪', '≫', '⦅', '⦆']; private const TRANSLIT_TO = ['AE', 'D', 'O', 'TH', 'ss', 'ae', 'd', 'o', 'th', 'D', 'd', 'H', 'h', 'i', 'q', 'L', 'l', 'L', 'l', '\'n', 'N', 'n', 'OE', 'oe', 'T', 't', 'b', 'B', 'B', 'b', 'C', 'c', 'D', 'D', 'D', 'd', 'E', 'F', 'f', 'G', 'hv', 'I', 'I', 'K', 'k', 'l', 'N', 'n', 'OI', 'oi', 'P', 'p', 't', 'T', 't', 'T', 'V', 'Y', 'y', 'Z', 'z', 'DZ', 'Dz', 'dz', 'G', 'g', 'd', 'Z', 'z', 'l', 'n', 't', 'j', 'db', 'qp', 'A', 'C', 'c', 'L', 'T', 's', 'z', 'B', 'U', 'E', 'e', 'J', 'j', 'R', 'r', 'Y', 'y', 'b', 'c', 'd', 'd', 'e', 'j', 'g', 'g', 'G', 'h', 'h', 'i', 'I', 'l', 'l', 'l', 'm', 'n', 'n', 'N', 'OE', 'r', 'r', 'r', 'R', 's', 't', 'u', 'v', 'Y', 'z', 'z', 'B', 'G', 'H', 'j', 'L', 'q', 'dz', 'dz', 'ts', 'ls', 'lz', 'A', 'AE', 'B', 'C', 'D', 'D', 'E', 'J', 'K', 'L', 'M', 'O', 'P', 'T', 'U', 'V', 'W', 'Z', 'ue', 'b', 'd', 'f', 'm', 'n', 'p', 'r', 'r', 's', 't', 'z', 'th', 'I', 'p', 'U', 'b', 'd', 'f', 'g', 'k', 'l', 'm', 'n', 'p', 'r', 's', 'v', 'x', 'z', 'a', 'd', 'e', 'e', 'i', 'u', 'a', 's', 's', 'SS', 'LL', 'll', 'V', 'v', 'Y', 'y', '(C)', '(R)', 'CE', 'Cr', 'Fr.', 'L.', 'Pts', 'TL', 'Rs', 'x', 'Rx', 'm/s', 'rad/s', 'C/kg', 'pH', 'V/m', 'A/m', ' 1/4', ' 1/2', ' 3/4', ' 1/3', ' 2/3', ' 1/5', ' 2/5', ' 3/5', ' 4/5', ' 1/6', ' 5/6', ' 1/8', ' 3/8', ' 5/8', ' 7/8', ' 1/', '0', '\'', '\'', ',', '\'', '"', '"', ',,', '"', '\'', '"', '"', '"', '<<', '>>', '<', '>', '-', '-', '-', '-', '-', '-', '-', '-', '-', '||', '/', '[', ']', '*', ',', '.', '<', '>', '<<', '>>', '[', ']', '[', ']', '[', ']', ',', '.', '[', ']', '<<', '>>', '<', '>', ',', '[', ']', '((', '))', '.', ',', '*', '/', '-', '/', '\\', '|', '||', '<<', '>>', '((', '))']; private static $transliterators = []; private static $tableZero; private static $tableWide; public static function fromCodePoints(int ...$codes): static { $string = ''; foreach ($codes as $code) { if (0x80 > $code %= 0x200000) { $string .= \chr($code); } elseif (0x800 > $code) { $string .= \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); } elseif (0x10000 > $code) { $string .= \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } else { $string .= \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } } return new static($string); } /** * Generic UTF-8 to ASCII transliteration. * * Install the intl extension for best results. * * @param string[]|\Transliterator[]|\Closure[] $rules See "*-Latin" rules from Transliterator::listIDs() */ public function ascii(array $rules = []): self { $str = clone $this; $s = $str->string; $str->string = ''; array_unshift($rules, 'nfd'); $rules[] = 'latin-ascii'; if (\function_exists('transliterator_transliterate')) { $rules[] = 'any-latin/bgn'; } $rules[] = 'nfkd'; $rules[] = '[:nonspacing mark:] remove'; while (\strlen($s) - 1 > $i = strspn($s, self::ASCII)) { if (0 < --$i) { $str->string .= substr($s, 0, $i); $s = substr($s, $i); } if (!$rule = array_shift($rules)) { $rules = []; // An empty rule interrupts the next ones } if ($rule instanceof \Transliterator) { $s = $rule->transliterate($s); } elseif ($rule instanceof \Closure) { $s = $rule($s); } elseif ($rule) { if ('nfd' === $rule = strtolower($rule)) { normalizer_is_normalized($s, self::NFD) ?: $s = normalizer_normalize($s, self::NFD); } elseif ('nfkd' === $rule) { normalizer_is_normalized($s, self::NFKD) ?: $s = normalizer_normalize($s, self::NFKD); } elseif ('[:nonspacing mark:] remove' === $rule) { $s = preg_replace('/\p{Mn}++/u', '', $s); } elseif ('latin-ascii' === $rule) { $s = str_replace(self::TRANSLIT_FROM, self::TRANSLIT_TO, $s); } elseif ('de-ascii' === $rule) { $s = preg_replace("/([AUO])\u{0308}(?=\p{Ll})/u", '$1e', $s); $s = str_replace(["a\u{0308}", "o\u{0308}", "u\u{0308}", "A\u{0308}", "O\u{0308}", "U\u{0308}"], ['ae', 'oe', 'ue', 'AE', 'OE', 'UE'], $s); } elseif (\function_exists('transliterator_transliterate')) { if (null === $transliterator = self::$transliterators[$rule] ?? self::$transliterators[$rule] = \Transliterator::create($rule)) { if ('any-latin/bgn' === $rule) { $rule = 'any-latin'; $transliterator = self::$transliterators[$rule] ?? self::$transliterators[$rule] = \Transliterator::create($rule); } if (null === $transliterator) { throw new InvalidArgumentException(sprintf('Unknown transliteration rule "%s".', $rule)); } self::$transliterators['any-latin/bgn'] = $transliterator; } $s = $transliterator->transliterate($s); } } elseif (!\function_exists('iconv')) { $s = preg_replace('/[^\x00-\x7F]/u', '?', $s); } else { $s = @preg_replace_callback('/[^\x00-\x7F]/u', static function ($c) { $c = (string) iconv('UTF-8', 'ASCII//TRANSLIT', $c[0]); if ('' === $c && '' === iconv('UTF-8', 'ASCII//TRANSLIT', '²')) { throw new \LogicException(sprintf('"%s" requires a translit-able iconv implementation, try installing "gnu-libiconv" if you\'re using Alpine Linux.', static::class)); } return 1 < \strlen($c) ? ltrim($c, '\'`"^~') : ('' !== $c ? $c : '?'); }, $s); } } $str->string .= $s; return $str; } public function camel(): static { $str = clone $this; $str->string = str_replace(' ', '', preg_replace_callback('/\b.(?![A-Z]{2,})/u', static function ($m) use (&$i) { return 1 === ++$i ? ('İ' === $m[0] ? 'i̇' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'); }, preg_replace('/[^\pL0-9]++/u', ' ', $this->string))); return $str; } /** * @return int[] */ public function codePointsAt(int $offset): array { $str = $this->slice($offset, 1); if ('' === $str->string) { return []; } $codePoints = []; foreach (preg_split('//u', $str->string, -1, \PREG_SPLIT_NO_EMPTY) as $c) { $codePoints[] = mb_ord($c, 'UTF-8'); } return $codePoints; } public function folded(bool $compat = true): static { $str = clone $this; if (!$compat || !\defined('Normalizer::NFKC_CF')) { $str->string = normalizer_normalize($str->string, $compat ? \Normalizer::NFKC : \Normalizer::NFC); $str->string = mb_strtolower(str_replace(self::FOLD_FROM, self::FOLD_TO, $this->string), 'UTF-8'); } else { $str->string = normalizer_normalize($str->string, \Normalizer::NFKC_CF); } return $str; } public function join(array $strings, string $lastGlue = null): static { $str = clone $this; $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : ''; $str->string = implode($this->string, $strings).$tail; if (!preg_match('//u', $str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function lower(): static { $str = clone $this; $str->string = mb_strtolower(str_replace('İ', 'i̇', $str->string), 'UTF-8'); return $str; } public function match(string $regexp, int $flags = 0, int $offset = 0): array { $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; if ($this->ignoreCase) { $regexp .= 'i'; } set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); try { if (false === $match($regexp.'u', $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) { $lastError = preg_last_error(); foreach (get_defined_constants(true)['pcre'] as $k => $v) { if ($lastError === $v && '_ERROR' === substr($k, -6)) { throw new RuntimeException('Matching failed with '.$k.'.'); } } throw new RuntimeException('Matching failed with unknown error code.'); } } finally { restore_error_handler(); } return $matches; } public function normalize(int $form = self::NFC): static { if (!\in_array($form, [self::NFC, self::NFD, self::NFKC, self::NFKD])) { throw new InvalidArgumentException('Unsupported normalization form.'); } $str = clone $this; normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form); return $str; } public function padBoth(int $length, string $padStr = ' '): static { if ('' === $padStr || !preg_match('//u', $padStr)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $pad = clone $this; $pad->string = $padStr; return $this->pad($length, $pad, \STR_PAD_BOTH); } public function padEnd(int $length, string $padStr = ' '): static { if ('' === $padStr || !preg_match('//u', $padStr)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $pad = clone $this; $pad->string = $padStr; return $this->pad($length, $pad, \STR_PAD_RIGHT); } public function padStart(int $length, string $padStr = ' '): static { if ('' === $padStr || !preg_match('//u', $padStr)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $pad = clone $this; $pad->string = $padStr; return $this->pad($length, $pad, \STR_PAD_LEFT); } public function replaceMatches(string $fromRegexp, string|callable $to): static { if ($this->ignoreCase) { $fromRegexp .= 'i'; } if (\is_array($to) || $to instanceof \Closure) { $replace = 'preg_replace_callback'; $to = static function (array $m) use ($to): string { $to = $to($m); if ('' !== $to && (!\is_string($to) || !preg_match('//u', $to))) { throw new InvalidArgumentException('Replace callback must return a valid UTF-8 string.'); } return $to; }; } elseif ('' !== $to && !preg_match('//u', $to)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } else { $replace = 'preg_replace'; } set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); try { if (null === $string = $replace($fromRegexp.'u', $to, $this->string)) { $lastError = preg_last_error(); foreach (get_defined_constants(true)['pcre'] as $k => $v) { if ($lastError === $v && '_ERROR' === substr($k, -6)) { throw new RuntimeException('Matching failed with '.$k.'.'); } } throw new RuntimeException('Matching failed with unknown error code.'); } } finally { restore_error_handler(); } $str = clone $this; $str->string = $string; return $str; } public function reverse(): static { $str = clone $this; $str->string = implode('', array_reverse(preg_split('/(\X)/u', $str->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY))); return $str; } public function snake(): static { $str = $this->camel(); $str->string = mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $str->string), 'UTF-8'); return $str; } public function title(bool $allWords = false): static { $str = clone $this; $limit = $allWords ? -1 : 1; $str->string = preg_replace_callback('/\b./u', static function (array $m): string { return mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'); }, $str->string, $limit); return $str; } public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); } $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{^[$chars]++|[$chars]++$}uD", '', $str->string); return $str; } public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); } $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{[$chars]++$}uD", '', $str->string); return $str; } public function trimPrefix($prefix): static { if (!$this->ignoreCase) { return parent::trimPrefix($prefix); } $str = clone $this; if ($prefix instanceof \Traversable) { $prefix = iterator_to_array($prefix, false); } elseif ($prefix instanceof parent) { $prefix = $prefix->string; } $prefix = implode('|', array_map('preg_quote', (array) $prefix)); $str->string = preg_replace("{^(?:$prefix)}iuD", '', $this->string); return $str; } public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); } $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{^[$chars]++}uD", '', $str->string); return $str; } public function trimSuffix($suffix): static { if (!$this->ignoreCase) { return parent::trimSuffix($suffix); } $str = clone $this; if ($suffix instanceof \Traversable) { $suffix = iterator_to_array($suffix, false); } elseif ($suffix instanceof parent) { $suffix = $suffix->string; } $suffix = implode('|', array_map('preg_quote', (array) $suffix)); $str->string = preg_replace("{(?:$suffix)$}iuD", '', $this->string); return $str; } public function upper(): static { $str = clone $this; $str->string = mb_strtoupper($str->string, 'UTF-8'); return $str; } public function width(bool $ignoreAnsiDecoration = true): int { $width = 0; $s = str_replace(["\x00", "\x05", "\x07"], '', $this->string); if (false !== strpos($s, "\r")) { $s = str_replace(["\r\n", "\r"], "\n", $s); } if (!$ignoreAnsiDecoration) { $s = preg_replace('/[\p{Cc}\x7F]++/u', '', $s); } foreach (explode("\n", $s) as $s) { if ($ignoreAnsiDecoration) { $s = preg_replace('/(?:\x1B(?: \[ [\x30-\x3F]*+ [\x20-\x2F]*+ [\x40-\x7E] | [P\]X^_] .*? \x1B\\\\ | [\x41-\x7E] )|[\p{Cc}\x7F]++)/xu', '', $s); } $lineWidth = $this->wcswidth($s); if ($lineWidth > $width) { $width = $lineWidth; } } return $width; } private function pad(int $len, self $pad, int $type): static { $sLen = $this->length(); if ($len <= $sLen) { return clone $this; } $padLen = $pad->length(); $freeLen = $len - $sLen; $len = $freeLen % $padLen; switch ($type) { case \STR_PAD_RIGHT: return $this->append(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : '')); case \STR_PAD_LEFT: return $this->prepend(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : '')); case \STR_PAD_BOTH: $freeLen /= 2; $rightLen = ceil($freeLen); $len = $rightLen % $padLen; $str = $this->append(str_repeat($pad->string, intdiv($rightLen, $padLen)).($len ? $pad->slice(0, $len) : '')); $leftLen = floor($freeLen); $len = $leftLen % $padLen; return $str->prepend(str_repeat($pad->string, intdiv($leftLen, $padLen)).($len ? $pad->slice(0, $len) : '')); default: throw new InvalidArgumentException('Invalid padding type.'); } } /** * Based on https://github.com/jquast/wcwidth, a Python implementation of https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c. */ private function wcswidth(string $string): int { $width = 0; foreach (preg_split('//u', $string, -1, \PREG_SPLIT_NO_EMPTY) as $c) { $codePoint = mb_ord($c, 'UTF-8'); if (0 === $codePoint // NULL || 0x034F === $codePoint // COMBINING GRAPHEME JOINER || (0x200B <= $codePoint && 0x200F >= $codePoint) // ZERO WIDTH SPACE to RIGHT-TO-LEFT MARK || 0x2028 === $codePoint // LINE SEPARATOR || 0x2029 === $codePoint // PARAGRAPH SEPARATOR || (0x202A <= $codePoint && 0x202E >= $codePoint) // LEFT-TO-RIGHT EMBEDDING to RIGHT-TO-LEFT OVERRIDE || (0x2060 <= $codePoint && 0x2063 >= $codePoint) // WORD JOINER to INVISIBLE SEPARATOR ) { continue; } // Non printable characters if (32 > $codePoint // C0 control characters || (0x07F <= $codePoint && 0x0A0 > $codePoint) // C1 control characters and DEL ) { return -1; } if (null === self::$tableZero) { self::$tableZero = require __DIR__.'/Resources/data/wcswidth_table_zero.php'; } if ($codePoint >= self::$tableZero[0][0] && $codePoint <= self::$tableZero[$ubound = \count(self::$tableZero) - 1][1]) { $lbound = 0; while ($ubound >= $lbound) { $mid = floor(($lbound + $ubound) / 2); if ($codePoint > self::$tableZero[$mid][1]) { $lbound = $mid + 1; } elseif ($codePoint < self::$tableZero[$mid][0]) { $ubound = $mid - 1; } else { continue 2; } } } if (null === self::$tableWide) { self::$tableWide = require __DIR__.'/Resources/data/wcswidth_table_wide.php'; } if ($codePoint >= self::$tableWide[0][0] && $codePoint <= self::$tableWide[$ubound = \count(self::$tableWide) - 1][1]) { $lbound = 0; while ($ubound >= $lbound) { $mid = floor(($lbound + $ubound) / 2); if ($codePoint > self::$tableWide[$mid][1]) { $lbound = $mid + 1; } elseif ($codePoint < self::$tableWide[$mid][0]) { $ubound = $mid - 1; } else { $width += 2; continue 2; } } } ++$width; } return $width; } } string/Inflector/EnglishInflector.php 0000644 00000036302 15025017654 0013760 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Inflector; final class EnglishInflector implements InflectorInterface { /** * Map English plural to singular suffixes. * * @see http://english-zone.com/spelling/plurals.html */ private const PLURAL_MAP = [ // First entry: plural suffix, reversed // Second entry: length of plural suffix // Third entry: Whether the suffix may succeed a vocal // Fourth entry: Whether the suffix may succeed a consonant // Fifth entry: singular suffix, normal // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) ['a', 1, true, true, ['on', 'um']], // nebulae (nebula) ['ea', 2, true, true, 'a'], // services (service) ['secivres', 8, true, true, 'service'], // mice (mouse), lice (louse) ['eci', 3, false, true, 'ouse'], // geese (goose) ['esee', 4, false, true, 'oose'], // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) ['i', 1, true, true, 'us'], // men (man), women (woman) ['nem', 3, true, true, 'man'], // children (child) ['nerdlihc', 8, true, true, 'child'], // oxen (ox) ['nexo', 4, false, false, 'ox'], // indices (index), appendices (appendix), prices (price) ['seci', 4, false, true, ['ex', 'ix', 'ice']], // selfies (selfie) ['seifles', 7, true, true, 'selfie'], // zombies (zombie) ['seibmoz', 7, true, true, 'zombie'], // movies (movie) ['seivom', 6, true, true, 'movie'], // conspectuses (conspectus), prospectuses (prospectus) ['sesutcep', 8, true, true, 'pectus'], // feet (foot) ['teef', 4, true, true, 'foot'], // geese (goose) ['eseeg', 5, true, true, 'goose'], // teeth (tooth) ['hteet', 5, true, true, 'tooth'], // news (news) ['swen', 4, true, true, 'news'], // series (series) ['seires', 6, true, true, 'series'], // babies (baby) ['sei', 3, false, true, 'y'], // accesses (access), addresses (address), kisses (kiss) ['sess', 4, true, false, 'ss'], // analyses (analysis), ellipses (ellipsis), fungi (fungus), // neuroses (neurosis), theses (thesis), emphases (emphasis), // oases (oasis), crises (crisis), houses (house), bases (base), // atlases (atlas) ['ses', 3, true, true, ['s', 'se', 'sis']], // objectives (objective), alternative (alternatives) ['sevit', 5, true, true, 'tive'], // drives (drive) ['sevird', 6, false, true, 'drive'], // lives (life), wives (wife) ['sevi', 4, false, true, 'ife'], // moves (move) ['sevom', 5, true, true, 'move'], // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff) ['sev', 3, true, true, ['f', 've', 'ff']], // axes (axis), axes (ax), axes (axe) ['sexa', 4, false, false, ['ax', 'axe', 'axis']], // indexes (index), matrixes (matrix) ['sex', 3, true, false, 'x'], // quizzes (quiz) ['sezz', 4, true, false, 'z'], // bureaus (bureau) ['suae', 4, false, true, 'eau'], // fees (fee), trees (tree), employees (employee) ['see', 3, true, true, 'ee'], // edges (edge) ['segd', 4, true, true, 'dge'], // roses (rose), garages (garage), cassettes (cassette), // waltzes (waltz), heroes (hero), bushes (bush), arches (arch), // shoes (shoe) ['se', 2, true, true, ['', 'e']], // tags (tag) ['s', 1, true, true, ''], // chateaux (chateau) ['xuae', 4, false, true, 'eau'], // people (person) ['elpoep', 6, true, true, 'person'], ]; /** * Map English singular to plural suffixes. * * @see http://english-zone.com/spelling/plurals.html */ private const SINGULAR_MAP = [ // First entry: singular suffix, reversed // Second entry: length of singular suffix // Third entry: Whether the suffix may succeed a vocal // Fourth entry: Whether the suffix may succeed a consonant // Fifth entry: plural suffix, normal // criterion (criteria) ['airetirc', 8, false, false, 'criterion'], // nebulae (nebula) ['aluben', 6, false, false, 'nebulae'], // children (child) ['dlihc', 5, true, true, 'children'], // prices (price) ['eci', 3, false, true, 'ices'], // services (service) ['ecivres', 7, true, true, 'services'], // lives (life), wives (wife) ['efi', 3, false, true, 'ives'], // selfies (selfie) ['eifles', 6, true, true, 'selfies'], // movies (movie) ['eivom', 5, true, true, 'movies'], // lice (louse) ['esuol', 5, false, true, 'lice'], // mice (mouse) ['esuom', 5, false, true, 'mice'], // geese (goose) ['esoo', 4, false, true, 'eese'], // houses (house), bases (base) ['es', 2, true, true, 'ses'], // geese (goose) ['esoog', 5, true, true, 'geese'], // caves (cave) ['ev', 2, true, true, 'ves'], // drives (drive) ['evird', 5, false, true, 'drives'], // objectives (objective), alternative (alternatives) ['evit', 4, true, true, 'tives'], // moves (move) ['evom', 4, true, true, 'moves'], // staves (staff) ['ffats', 5, true, true, 'staves'], // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) ['ff', 2, true, true, 'ffs'], // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) ['f', 1, true, true, ['fs', 'ves']], // arches (arch) ['hc', 2, true, true, 'ches'], // bushes (bush) ['hs', 2, true, true, 'shes'], // teeth (tooth) ['htoot', 5, true, true, 'teeth'], // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) ['mu', 2, true, true, 'a'], // men (man), women (woman) ['nam', 3, true, true, 'men'], // people (person) ['nosrep', 6, true, true, ['persons', 'people']], // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) ['noi', 3, true, true, 'ions'], // coupon (coupons) ['nop', 3, true, true, 'pons'], // seasons (season), treasons (treason), poisons (poison), lessons (lesson) ['nos', 3, true, true, 'sons'], // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) ['no', 2, true, true, 'a'], // echoes (echo) ['ohce', 4, true, true, 'echoes'], // heroes (hero) ['oreh', 4, true, true, 'heroes'], // atlases (atlas) ['salta', 5, true, true, 'atlases'], // irises (iris) ['siri', 4, true, true, 'irises'], // analyses (analysis), ellipses (ellipsis), neuroses (neurosis) // theses (thesis), emphases (emphasis), oases (oasis), // crises (crisis) ['sis', 3, true, true, 'ses'], // accesses (access), addresses (address), kisses (kiss) ['ss', 2, true, false, 'sses'], // syllabi (syllabus) ['suballys', 8, true, true, 'syllabi'], // buses (bus) ['sub', 3, true, true, 'buses'], // circuses (circus) ['suc', 3, true, true, 'cuses'], // conspectuses (conspectus), prospectuses (prospectus) ['sutcep', 6, true, true, 'pectuses'], // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) ['su', 2, true, true, 'i'], // news (news) ['swen', 4, true, true, 'news'], // feet (foot) ['toof', 4, true, true, 'feet'], // chateaux (chateau), bureaus (bureau) ['uae', 3, false, true, ['eaus', 'eaux']], // oxen (ox) ['xo', 2, false, false, 'oxen'], // hoaxes (hoax) ['xaoh', 4, true, false, 'hoaxes'], // indices (index) ['xedni', 5, false, true, ['indicies', 'indexes']], // boxes (box) ['xo', 2, false, true, 'oxes'], // indexes (index), matrixes (matrix) ['x', 1, true, false, ['cies', 'xes']], // appendices (appendix) ['xi', 2, false, true, 'ices'], // babies (baby) ['y', 1, false, true, 'ies'], // quizzes (quiz) ['ziuq', 4, true, false, 'quizzes'], // waltzes (waltz) ['z', 1, true, true, 'zes'], ]; /** * A list of words which should not be inflected, reversed. */ private const UNINFLECTED = [ '', // data 'atad', // deer 'reed', // feedback 'kcabdeef', // fish 'hsif', // info 'ofni', // moose 'esoom', // series 'seires', // sheep 'peehs', // species 'seiceps', ]; /** * {@inheritdoc} */ public function singularize(string $plural): array { $pluralRev = strrev($plural); $lowerPluralRev = strtolower($pluralRev); $pluralLength = \strlen($lowerPluralRev); // Check if the word is one which is not inflected, return early if so if (\in_array($lowerPluralRev, self::UNINFLECTED, true)) { return [$plural]; } // The outer loop iterates over the entries of the plural table // The inner loop $j iterates over the characters of the plural suffix // in the plural table to compare them with the characters of the actual // given plural suffix foreach (self::PLURAL_MAP as $map) { $suffix = $map[0]; $suffixLength = $map[1]; $j = 0; // Compare characters in the plural table and of the suffix of the // given plural one by one while ($suffix[$j] === $lowerPluralRev[$j]) { // Let $j point to the next character ++$j; // Successfully compared the last character // Add an entry with the singular suffix to the singular array if ($j === $suffixLength) { // Is there any character preceding the suffix in the plural string? if ($j < $pluralLength) { $nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]); if (!$map[2] && $nextIsVocal) { // suffix may not succeed a vocal but next char is one break; } if (!$map[3] && !$nextIsVocal) { // suffix may not succeed a consonant but next char is one break; } } $newBase = substr($plural, 0, $pluralLength - $suffixLength); $newSuffix = $map[4]; // Check whether the first character in the plural suffix // is uppercased. If yes, uppercase the first character in // the singular suffix too $firstUpper = ctype_upper($pluralRev[$j - 1]); if (\is_array($newSuffix)) { $singulars = []; foreach ($newSuffix as $newSuffixEntry) { $singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); } return $singulars; } return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)]; } // Suffix is longer than word if ($j === $pluralLength) { break; } } } // Assume that plural and singular is identical return [$plural]; } /** * {@inheritdoc} */ public function pluralize(string $singular): array { $singularRev = strrev($singular); $lowerSingularRev = strtolower($singularRev); $singularLength = \strlen($lowerSingularRev); // Check if the word is one which is not inflected, return early if so if (\in_array($lowerSingularRev, self::UNINFLECTED, true)) { return [$singular]; } // The outer loop iterates over the entries of the singular table // The inner loop $j iterates over the characters of the singular suffix // in the singular table to compare them with the characters of the actual // given singular suffix foreach (self::SINGULAR_MAP as $map) { $suffix = $map[0]; $suffixLength = $map[1]; $j = 0; // Compare characters in the singular table and of the suffix of the // given plural one by one while ($suffix[$j] === $lowerSingularRev[$j]) { // Let $j point to the next character ++$j; // Successfully compared the last character // Add an entry with the plural suffix to the plural array if ($j === $suffixLength) { // Is there any character preceding the suffix in the plural string? if ($j < $singularLength) { $nextIsVocal = false !== strpos('aeiou', $lowerSingularRev[$j]); if (!$map[2] && $nextIsVocal) { // suffix may not succeed a vocal but next char is one break; } if (!$map[3] && !$nextIsVocal) { // suffix may not succeed a consonant but next char is one break; } } $newBase = substr($singular, 0, $singularLength - $suffixLength); $newSuffix = $map[4]; // Check whether the first character in the singular suffix // is uppercased. If yes, uppercase the first character in // the singular suffix too $firstUpper = ctype_upper($singularRev[$j - 1]); if (\is_array($newSuffix)) { $plurals = []; foreach ($newSuffix as $newSuffixEntry) { $plurals[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); } return $plurals; } return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)]; } // Suffix is longer than word if ($j === $singularLength) { break; } } } // Assume that plural is singular with a trailing `s` return [$singular.'s']; } } string/Inflector/FrenchInflector.php 0000644 00000013625 15025017654 0013577 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Inflector; /** * French inflector. * * This class does only inflect nouns; not adjectives nor composed words like "soixante-dix". */ final class FrenchInflector implements InflectorInterface { /** * A list of all rules for pluralise. * * @see https://la-conjugaison.nouvelobs.com/regles/grammaire/le-pluriel-des-noms-121.php */ private const PLURALIZE_REGEXP = [ // First entry: regexp // Second entry: replacement // Words finishing with "s", "x" or "z" are invariables // Les mots finissant par "s", "x" ou "z" sont invariables ['/(s|x|z)$/i', '\1'], // Words finishing with "eau" are pluralized with a "x" // Les mots finissant par "eau" prennent tous un "x" au pluriel ['/(eau)$/i', '\1x'], // Words finishing with "au" are pluralized with a "x" excepted "landau" // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" ['/^(landau)$/i', '\1s'], ['/(au)$/i', '\1x'], // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" ['/^(pneu|bleu|émeu)$/i', '\1s'], ['/(eu)$/i', '\1x'], // Words finishing with "al" are pluralized with a "aux" excepted // Les mots finissant en "al" se terminent en "aux" sauf ['/^(bal|carnaval|caracal|chacal|choral|corral|étal|festival|récital|val)$/i', '\1s'], ['/al$/i', '\1aux'], // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux ['/^(aspir|b|cor|ém|ferm|soupir|trav|vant|vitr)ail$/i', '\1aux'], // Bijou, caillou, chou, genou, hibou, joujou et pou qui prennent un x au pluriel ['/^(bij|caill|ch|gen|hib|jouj|p)ou$/i', '\1oux'], // Invariable words ['/^(cinquante|soixante|mille)$/i', '\1'], // French titles ['/^(mon|ma)(sieur|dame|demoiselle|seigneur)$/', 'mes\2s'], ['/^(Mon|Ma)(sieur|dame|demoiselle|seigneur)$/', 'Mes\2s'], ]; /** * A list of all rules for singularize. */ private const SINGULARIZE_REGEXP = [ // First entry: regexp // Second entry: replacement // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux ['/((aspir|b|cor|ém|ferm|soupir|trav|vant|vitr))aux$/i', '\1ail'], // Words finishing with "eau" are pluralized with a "x" // Les mots finissant par "eau" prennent tous un "x" au pluriel ['/(eau)x$/i', '\1'], // Words finishing with "al" are pluralized with a "aux" expected // Les mots finissant en "al" se terminent en "aux" sauf ['/(amir|anim|arsen|boc|can|capit|capor|chev|crist|génér|hopit|hôpit|idé|journ|littor|loc|m|mét|minér|princip|radic|termin)aux$/i', '\1al'], // Words finishing with "au" are pluralized with a "x" excepted "landau" // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" ['/(au)x$/i', '\1'], // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" ['/(eu)x$/i', '\1'], // Words finishing with "ou" are pluralized with a "s" excepted bijou, caillou, chou, genou, hibou, joujou, pou // Les mots finissant par "ou" prennent un "s" sauf bijou, caillou, chou, genou, hibou, joujou, pou ['/(bij|caill|ch|gen|hib|jouj|p)oux$/i', '\1ou'], // French titles ['/^mes(dame|demoiselle)s$/', 'ma\1'], ['/^Mes(dame|demoiselle)s$/', 'Ma\1'], ['/^mes(sieur|seigneur)s$/', 'mon\1'], ['/^Mes(sieur|seigneur)s$/', 'Mon\1'], // Default rule ['/s$/i', ''], ]; /** * A list of words which should not be inflected. * This list is only used by singularize. */ private const UNINFLECTED = '/^(abcès|accès|abus|albatros|anchois|anglais|autobus|bois|brebis|carquois|cas|chas|colis|concours|corps|cours|cyprès|décès|devis|discours|dos|embarras|engrais|entrelacs|excès|fils|fois|gâchis|gars|glas|héros|intrus|jars|jus|kermès|lacis|legs|lilas|marais|mars|matelas|mépris|mets|mois|mors|obus|os|palais|paradis|parcours|pardessus|pays|plusieurs|poids|pois|pouls|printemps|processus|progrès|puits|pus|rabais|radis|recors|recours|refus|relais|remords|remous|rictus|rhinocéros|repas|rubis|sans|sas|secours|sens|souris|succès|talus|tapis|tas|taudis|temps|tiers|univers|velours|verglas|vernis|virus)$/i'; /** * {@inheritdoc} */ public function singularize(string $plural): array { if ($this->isInflectedWord($plural)) { return [$plural]; } foreach (self::SINGULARIZE_REGEXP as $rule) { [$regexp, $replace] = $rule; if (1 === preg_match($regexp, $plural)) { return [preg_replace($regexp, $replace, $plural)]; } } return [$plural]; } /** * {@inheritdoc} */ public function pluralize(string $singular): array { if ($this->isInflectedWord($singular)) { return [$singular]; } foreach (self::PLURALIZE_REGEXP as $rule) { [$regexp, $replace] = $rule; if (1 === preg_match($regexp, $singular)) { return [preg_replace($regexp, $replace, $singular)]; } } return [$singular.'s']; } private function isInflectedWord(string $word): bool { return 1 === preg_match(self::UNINFLECTED, $word); } } string/Inflector/InflectorInterface.php 0000644 00000001503 15025017654 0014262 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Inflector; interface InflectorInterface { /** * Returns the singular forms of a string. * * If the method can't determine the form with certainty, several possible singulars are returned. * * @return string[] */ public function singularize(string $plural): array; /** * Returns the plural forms of a string. * * If the method can't determine the form with certainty, several possible plurals are returned. * * @return string[] */ public function pluralize(string $singular): array; } string/Exception/InvalidArgumentException.php 0000644 00000000600 15025017654 0015472 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Exception; class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } string/Exception/ExceptionInterface.php 0000644 00000000521 15025017654 0014303 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Exception; interface ExceptionInterface extends \Throwable { } string/Exception/RuntimeException.php 0000644 00000000560 15025017654 0014031 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Exception; class RuntimeException extends \RuntimeException implements ExceptionInterface { } string/README.md 0000644 00000001053 15025017654 0007335 0 ustar 00 String Component ================ The String component provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way. Resources --------- * [Documentation](https://symfony.com/doc/current/components/string.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) string/Slugger/SluggerInterface.php 0000644 00000001313 15025017654 0013427 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Slugger; use Symfony\Component\String\AbstractUnicodeString; /** * Creates a URL-friendly slug from a given string. * * @author Titouan Galopin <galopintitouan@gmail.com> */ interface SluggerInterface { /** * Creates a slug for the given string and locale, using appropriate transliteration when needed. */ public function slug(string $string, string $separator = '-', string $locale = null): AbstractUnicodeString; } string/Slugger/AsciiSlugger.php 0000644 00000013351 15025017654 0012564 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Slugger; use Symfony\Component\String\AbstractUnicodeString; use Symfony\Component\String\UnicodeString; use Symfony\Contracts\Translation\LocaleAwareInterface; if (!interface_exists(LocaleAwareInterface::class)) { throw new \LogicException('You cannot use the "Symfony\Component\String\Slugger\AsciiSlugger" as the "symfony/translation-contracts" package is not installed. Try running "composer require symfony/translation-contracts".'); } /** * @author Titouan Galopin <galopintitouan@gmail.com> */ class AsciiSlugger implements SluggerInterface, LocaleAwareInterface { private const LOCALE_TO_TRANSLITERATOR_ID = [ 'am' => 'Amharic-Latin', 'ar' => 'Arabic-Latin', 'az' => 'Azerbaijani-Latin', 'be' => 'Belarusian-Latin', 'bg' => 'Bulgarian-Latin', 'bn' => 'Bengali-Latin', 'de' => 'de-ASCII', 'el' => 'Greek-Latin', 'fa' => 'Persian-Latin', 'he' => 'Hebrew-Latin', 'hy' => 'Armenian-Latin', 'ka' => 'Georgian-Latin', 'kk' => 'Kazakh-Latin', 'ky' => 'Kirghiz-Latin', 'ko' => 'Korean-Latin', 'mk' => 'Macedonian-Latin', 'mn' => 'Mongolian-Latin', 'or' => 'Oriya-Latin', 'ps' => 'Pashto-Latin', 'ru' => 'Russian-Latin', 'sr' => 'Serbian-Latin', 'sr_Cyrl' => 'Serbian-Latin', 'th' => 'Thai-Latin', 'tk' => 'Turkmen-Latin', 'uk' => 'Ukrainian-Latin', 'uz' => 'Uzbek-Latin', 'zh' => 'Han-Latin', ]; private ?string $defaultLocale; private \Closure|array $symbolsMap = [ 'en' => ['@' => 'at', '&' => 'and'], ]; /** * Cache of transliterators per locale. * * @var \Transliterator[] */ private array $transliterators = []; public function __construct(string $defaultLocale = null, array|\Closure $symbolsMap = null) { $this->defaultLocale = $defaultLocale; $this->symbolsMap = $symbolsMap ?? $this->symbolsMap; } /** * {@inheritdoc} */ public function setLocale(string $locale) { $this->defaultLocale = $locale; } /** * {@inheritdoc} */ public function getLocale(): string { return $this->defaultLocale; } /** * {@inheritdoc} */ public function slug(string $string, string $separator = '-', string $locale = null): AbstractUnicodeString { $locale = $locale ?? $this->defaultLocale; $transliterator = []; if ($locale && ('de' === $locale || 0 === strpos($locale, 'de_'))) { // Use the shortcut for German in UnicodeString::ascii() if possible (faster and no requirement on intl) $transliterator = ['de-ASCII']; } elseif (\function_exists('transliterator_transliterate') && $locale) { $transliterator = (array) $this->createTransliterator($locale); } if ($this->symbolsMap instanceof \Closure) { // If the symbols map is passed as a closure, there is no need to fallback to the parent locale // as the closure can just provide substitutions for all locales of interest. $symbolsMap = $this->symbolsMap; array_unshift($transliterator, static function ($s) use ($symbolsMap, $locale) { return $symbolsMap($s, $locale); }); } $unicodeString = (new UnicodeString($string))->ascii($transliterator); if (\is_array($this->symbolsMap)) { $map = null; if (isset($this->symbolsMap[$locale])) { $map = $this->symbolsMap[$locale]; } else { $parent = self::getParentLocale($locale); if ($parent && isset($this->symbolsMap[$parent])) { $map = $this->symbolsMap[$parent]; } } if ($map) { foreach ($map as $char => $replace) { $unicodeString = $unicodeString->replace($char, ' '.$replace.' '); } } } return $unicodeString ->replaceMatches('/[^A-Za-z0-9]++/', $separator) ->trim($separator) ; } private function createTransliterator(string $locale): ?\Transliterator { if (\array_key_exists($locale, $this->transliterators)) { return $this->transliterators[$locale]; } // Exact locale supported, cache and return if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$locale] ?? null) { return $this->transliterators[$locale] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id); } // Locale not supported and no parent, fallback to any-latin if (!$parent = self::getParentLocale($locale)) { return $this->transliterators[$locale] = null; } // Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) { $transliterator = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id); } return $this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator ?? null; } private static function getParentLocale(?string $locale): ?string { if (!$locale) { return null; } if (false === $str = strrchr($locale, '_')) { // no parent locale return null; } return substr($locale, 0, -\strlen($str)); } } string/LICENSE 0000644 00000002051 15025017654 0007062 0 ustar 00 Copyright (c) 2019-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. string/AbstractString.php 0000644 00000045546 15025017654 0011540 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; use Symfony\Component\String\Exception\RuntimeException; /** * Represents a string of abstract characters. * * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters). * This class is the abstract type to use as a type-hint when the logic you want to * implement doesn't care about the exact variant it deals with. * * @author Nicolas Grekas <p@tchwork.com> * @author Hugo Hamon <hugohamon@neuf.fr> * * @throws ExceptionInterface */ abstract class AbstractString implements \Stringable, \JsonSerializable { public const PREG_PATTERN_ORDER = \PREG_PATTERN_ORDER; public const PREG_SET_ORDER = \PREG_SET_ORDER; public const PREG_OFFSET_CAPTURE = \PREG_OFFSET_CAPTURE; public const PREG_UNMATCHED_AS_NULL = \PREG_UNMATCHED_AS_NULL; public const PREG_SPLIT = 0; public const PREG_SPLIT_NO_EMPTY = \PREG_SPLIT_NO_EMPTY; public const PREG_SPLIT_DELIM_CAPTURE = \PREG_SPLIT_DELIM_CAPTURE; public const PREG_SPLIT_OFFSET_CAPTURE = \PREG_SPLIT_OFFSET_CAPTURE; protected $string = ''; protected $ignoreCase = false; abstract public function __construct(string $string = ''); /** * Unwraps instances of AbstractString back to strings. * * @return string[]|array */ public static function unwrap(array $values): array { foreach ($values as $k => $v) { if ($v instanceof self) { $values[$k] = $v->__toString(); } elseif (\is_array($v) && $values[$k] !== $v = static::unwrap($v)) { $values[$k] = $v; } } return $values; } /** * Wraps (and normalizes) strings in instances of AbstractString. * * @return static[]|array */ public static function wrap(array $values): array { $i = 0; $keys = null; foreach ($values as $k => $v) { if (\is_string($k) && '' !== $k && $k !== $j = (string) new static($k)) { $keys = $keys ?? array_keys($values); $keys[$i] = $j; } if (\is_string($v)) { $values[$k] = new static($v); } elseif (\is_array($v) && $values[$k] !== $v = static::wrap($v)) { $values[$k] = $v; } ++$i; } return null !== $keys ? array_combine($keys, $values) : $values; } /** * @param string|string[] $needle */ public function after(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = \PHP_INT_MAX; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOf($n, $offset); if (null !== $j && $j < $i) { $i = $j; $str->string = $n; } } if (\PHP_INT_MAX === $i) { return $str; } if (!$includeNeedle) { $i += $str->length(); } return $this->slice($i); } /** * @param string|string[] $needle */ public function afterLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = null; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOfLast($n, $offset); if (null !== $j && $j >= $i) { $i = $offset = $j; $str->string = $n; } } if (null === $i) { return $str; } if (!$includeNeedle) { $i += $str->length(); } return $this->slice($i); } abstract public function append(string ...$suffix): static; /** * @param string|string[] $needle */ public function before(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = \PHP_INT_MAX; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOf($n, $offset); if (null !== $j && $j < $i) { $i = $j; $str->string = $n; } } if (\PHP_INT_MAX === $i) { return $str; } if ($includeNeedle) { $i += $str->length(); } return $this->slice(0, $i); } /** * @param string|string[] $needle */ public function beforeLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = null; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOfLast($n, $offset); if (null !== $j && $j >= $i) { $i = $offset = $j; $str->string = $n; } } if (null === $i) { return $str; } if ($includeNeedle) { $i += $str->length(); } return $this->slice(0, $i); } /** * @return int[] */ public function bytesAt(int $offset): array { $str = $this->slice($offset, 1); return '' === $str->string ? [] : array_values(unpack('C*', $str->string)); } abstract public function camel(): static; /** * @return static[] */ abstract public function chunk(int $length = 1): array; public function collapseWhitespace(): static { $str = clone $this; $str->string = trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $str->string), " \n\r\t\x0C"); return $str; } /** * @param string|string[] $needle */ public function containsAny(string|iterable $needle): bool { return null !== $this->indexOf($needle); } /** * @param string|string[] $suffix */ public function endsWith(string|iterable $suffix): bool { if (\is_string($suffix)) { throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($suffix as $s) { if ($this->endsWith((string) $s)) { return true; } } return false; } public function ensureEnd(string $suffix): static { if (!$this->endsWith($suffix)) { return $this->append($suffix); } $suffix = preg_quote($suffix); $regex = '{('.$suffix.')(?:'.$suffix.')++$}D'; return $this->replaceMatches($regex.($this->ignoreCase ? 'i' : ''), '$1'); } public function ensureStart(string $prefix): static { $prefix = new static($prefix); if (!$this->startsWith($prefix)) { return $this->prepend($prefix); } $str = clone $this; $i = $prefixLen = $prefix->length(); while ($this->indexOf($prefix, $i) === $i) { $str = $str->slice($prefixLen); $i += $prefixLen; } return $str; } /** * @param string|string[] $string */ public function equalsTo(string|iterable $string): bool { if (\is_string($string)) { throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($string as $s) { if ($this->equalsTo((string) $s)) { return true; } } return false; } abstract public function folded(): static; public function ignoreCase(): static { $str = clone $this; $str->ignoreCase = true; return $str; } /** * @param string|string[] $needle */ public function indexOf(string|iterable $needle, int $offset = 0): ?int { if (\is_string($needle)) { throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } $i = \PHP_INT_MAX; foreach ($needle as $n) { $j = $this->indexOf((string) $n, $offset); if (null !== $j && $j < $i) { $i = $j; } } return \PHP_INT_MAX === $i ? null : $i; } /** * @param string|string[] $needle */ public function indexOfLast(string|iterable $needle, int $offset = 0): ?int { if (\is_string($needle)) { throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } $i = null; foreach ($needle as $n) { $j = $this->indexOfLast((string) $n, $offset); if (null !== $j && $j >= $i) { $i = $offset = $j; } } return $i; } public function isEmpty(): bool { return '' === $this->string; } abstract public function join(array $strings, string $lastGlue = null): static; public function jsonSerialize(): string { return $this->string; } abstract public function length(): int; abstract public function lower(): static; /** * Matches the string using a regular expression. * * Pass PREG_PATTERN_ORDER or PREG_SET_ORDER as $flags to get all occurrences matching the regular expression. * * @return array All matches in a multi-dimensional array ordered according to flags */ abstract public function match(string $regexp, int $flags = 0, int $offset = 0): array; abstract public function padBoth(int $length, string $padStr = ' '): static; abstract public function padEnd(int $length, string $padStr = ' '): static; abstract public function padStart(int $length, string $padStr = ' '): static; abstract public function prepend(string ...$prefix): static; public function repeat(int $multiplier): static { if (0 > $multiplier) { throw new InvalidArgumentException(sprintf('Multiplier must be positive, %d given.', $multiplier)); } $str = clone $this; $str->string = str_repeat($str->string, $multiplier); return $str; } abstract public function replace(string $from, string $to): static; abstract public function replaceMatches(string $fromRegexp, string|callable $to): static; abstract public function reverse(): static; abstract public function slice(int $start = 0, int $length = null): static; abstract public function snake(): static; abstract public function splice(string $replacement, int $start = 0, int $length = null): static; /** * @return static[] */ public function split(string $delimiter, int $limit = null, int $flags = null): array { if (null === $flags) { throw new \TypeError('Split behavior when $flags is null must be implemented by child classes.'); } if ($this->ignoreCase) { $delimiter .= 'i'; } set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); try { if (false === $chunks = preg_split($delimiter, $this->string, $limit, $flags)) { $lastError = preg_last_error(); foreach (get_defined_constants(true)['pcre'] as $k => $v) { if ($lastError === $v && '_ERROR' === substr($k, -6)) { throw new RuntimeException('Splitting failed with '.$k.'.'); } } throw new RuntimeException('Splitting failed with unknown error code.'); } } finally { restore_error_handler(); } $str = clone $this; if (self::PREG_SPLIT_OFFSET_CAPTURE & $flags) { foreach ($chunks as &$chunk) { $str->string = $chunk[0]; $chunk[0] = clone $str; } } else { foreach ($chunks as &$chunk) { $str->string = $chunk; $chunk = clone $str; } } return $chunks; } /** * @param string|string[] $prefix */ public function startsWith(string|iterable $prefix): bool { if (\is_string($prefix)) { throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($prefix as $prefix) { if ($this->startsWith((string) $prefix)) { return true; } } return false; } abstract public function title(bool $allWords = false): static; public function toByteString(string $toEncoding = null): ByteString { $b = new ByteString(); $toEncoding = \in_array($toEncoding, ['utf8', 'utf-8', 'UTF8'], true) ? 'UTF-8' : $toEncoding; if (null === $toEncoding || $toEncoding === $fromEncoding = $this instanceof AbstractUnicodeString || preg_match('//u', $b->string) ? 'UTF-8' : 'Windows-1252') { $b->string = $this->string; return $b; } set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); try { try { $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8'); } catch (InvalidArgumentException $e) { if (!\function_exists('iconv')) { throw $e; } $b->string = iconv('UTF-8', $toEncoding, $this->string); } } finally { restore_error_handler(); } return $b; } public function toCodePointString(): CodePointString { return new CodePointString($this->string); } public function toString(): string { return $this->string; } public function toUnicodeString(): UnicodeString { return new UnicodeString($this->string); } abstract public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; abstract public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; /** * @param string|string[] $prefix */ public function trimPrefix($prefix): static { if (\is_array($prefix) || $prefix instanceof \Traversable) { foreach ($prefix as $s) { $t = $this->trimPrefix($s); if ($t->string !== $this->string) { return $t; } } return clone $this; } $str = clone $this; if ($prefix instanceof self) { $prefix = $prefix->string; } else { $prefix = (string) $prefix; } if ('' !== $prefix && \strlen($this->string) >= \strlen($prefix) && 0 === substr_compare($this->string, $prefix, 0, \strlen($prefix), $this->ignoreCase)) { $str->string = substr($this->string, \strlen($prefix)); } return $str; } abstract public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; /** * @param string|string[] $suffix */ public function trimSuffix($suffix): static { if (\is_array($suffix) || $suffix instanceof \Traversable) { foreach ($suffix as $s) { $t = $this->trimSuffix($s); if ($t->string !== $this->string) { return $t; } } return clone $this; } $str = clone $this; if ($suffix instanceof self) { $suffix = $suffix->string; } else { $suffix = (string) $suffix; } if ('' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase)) { $str->string = substr($this->string, 0, -\strlen($suffix)); } return $str; } public function truncate(int $length, string $ellipsis = '', bool $cut = true): static { $stringLength = $this->length(); if ($stringLength <= $length) { return clone $this; } $ellipsisLength = '' !== $ellipsis ? (new static($ellipsis))->length() : 0; if ($length < $ellipsisLength) { $ellipsisLength = 0; } if (!$cut) { if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) { return clone $this; } $length += $ellipsisLength; } $str = $this->slice(0, $length - $ellipsisLength); return $ellipsisLength ? $str->trimEnd()->append($ellipsis) : $str; } abstract public function upper(): static; /** * Returns the printable length on a terminal. */ abstract public function width(bool $ignoreAnsiDecoration = true): int; public function wordwrap(int $width = 75, string $break = "\n", bool $cut = false): static { $lines = '' !== $break ? $this->split($break) : [clone $this]; $chars = []; $mask = ''; if (1 === \count($lines) && '' === $lines[0]->string) { return $lines[0]; } foreach ($lines as $i => $line) { if ($i) { $chars[] = $break; $mask .= '#'; } foreach ($line->chunk() as $char) { $chars[] = $char->string; $mask .= ' ' === $char->string ? ' ' : '?'; } } $string = ''; $j = 0; $b = $i = -1; $mask = wordwrap($mask, $width, '#', $cut); while (false !== $b = strpos($mask, '#', $b + 1)) { for (++$i; $i < $b; ++$i) { $string .= $chars[$j]; unset($chars[$j++]); } if ($break === $chars[$j] || ' ' === $chars[$j]) { unset($chars[$j++]); } $string .= $break; } $str = clone $this; $str->string = $string.implode('', $chars); return $str; } public function __sleep(): array { return ['string']; } public function __clone() { $this->ignoreCase = false; } public function __toString(): string { return $this->string; } } string/ByteString.php 0000644 00000035233 15025017654 0010670 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; use Symfony\Component\String\Exception\RuntimeException; /** * Represents a binary-safe string of bytes. * * @author Nicolas Grekas <p@tchwork.com> * @author Hugo Hamon <hugohamon@neuf.fr> * * @throws ExceptionInterface */ class ByteString extends AbstractString { private const ALPHABET_ALPHANUMERIC = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; public function __construct(string $string = '') { $this->string = $string; } /* * The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03) * * https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16 * * Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE). * * Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/) */ public static function fromRandom(int $length = 16, string $alphabet = null): self { if ($length <= 0) { throw new InvalidArgumentException(sprintf('A strictly positive length is expected, "%d" given.', $length)); } $alphabet = $alphabet ?? self::ALPHABET_ALPHANUMERIC; $alphabetSize = \strlen($alphabet); $bits = (int) ceil(log($alphabetSize, 2.0)); if ($bits <= 0 || $bits > 56) { throw new InvalidArgumentException('The length of the alphabet must in the [2^1, 2^56] range.'); } $ret = ''; while ($length > 0) { $urandomLength = (int) ceil(2 * $length * $bits / 8.0); $data = random_bytes($urandomLength); $unpackedData = 0; $unpackedBits = 0; for ($i = 0; $i < $urandomLength && $length > 0; ++$i) { // Unpack 8 bits $unpackedData = ($unpackedData << 8) | \ord($data[$i]); $unpackedBits += 8; // While we have enough bits to select a character from the alphabet, keep // consuming the random data for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) { $index = ($unpackedData & ((1 << $bits) - 1)); $unpackedData >>= $bits; // Unfortunately, the alphabet size is not necessarily a power of two. // Worst case, it is 2^k + 1, which means we need (k+1) bits and we // have around a 50% chance of missing as k gets larger if ($index < $alphabetSize) { $ret .= $alphabet[$index]; --$length; } } } } return new static($ret); } public function bytesAt(int $offset): array { $str = $this->string[$offset] ?? ''; return '' === $str ? [] : [\ord($str)]; } public function append(string ...$suffix): static { $str = clone $this; $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix); return $str; } public function camel(): static { $str = clone $this; $parts = explode(' ', trim(ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->string)))); $parts[0] = 1 !== \strlen($parts[0]) && ctype_upper($parts[0]) ? $parts[0] : lcfirst($parts[0]); $str->string = implode('', $parts); return $str; } public function chunk(int $length = 1): array { if (1 > $length) { throw new InvalidArgumentException('The chunk length must be greater than zero.'); } if ('' === $this->string) { return []; } $str = clone $this; $chunks = []; foreach (str_split($this->string, $length) as $chunk) { $str->string = $chunk; $chunks[] = clone $str; } return $chunks; } public function endsWith(string|iterable|AbstractString $suffix): bool { if ($suffix instanceof AbstractString) { $suffix = $suffix->string; } elseif (!\is_string($suffix)) { return parent::endsWith($suffix); } return '' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase); } public function equalsTo(string|iterable|AbstractString $string): bool { if ($string instanceof AbstractString) { $string = $string->string; } elseif (!\is_string($string)) { return parent::equalsTo($string); } if ('' !== $string && $this->ignoreCase) { return 0 === strcasecmp($string, $this->string); } return $string === $this->string; } public function folded(): static { $str = clone $this; $str->string = strtolower($str->string); return $str; } public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOf($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset); return false === $i ? null : $i; } public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOfLast($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset); return false === $i ? null : $i; } public function isUtf8(): bool { return '' === $this->string || preg_match('//u', $this->string); } public function join(array $strings, string $lastGlue = null): static { $str = clone $this; $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : ''; $str->string = implode($this->string, $strings).$tail; return $str; } public function length(): int { return \strlen($this->string); } public function lower(): static { $str = clone $this; $str->string = strtolower($str->string); return $str; } public function match(string $regexp, int $flags = 0, int $offset = 0): array { $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; if ($this->ignoreCase) { $regexp .= 'i'; } set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); try { if (false === $match($regexp, $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) { $lastError = preg_last_error(); foreach (get_defined_constants(true)['pcre'] as $k => $v) { if ($lastError === $v && '_ERROR' === substr($k, -6)) { throw new RuntimeException('Matching failed with '.$k.'.'); } } throw new RuntimeException('Matching failed with unknown error code.'); } } finally { restore_error_handler(); } return $matches; } public function padBoth(int $length, string $padStr = ' '): static { $str = clone $this; $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_BOTH); return $str; } public function padEnd(int $length, string $padStr = ' '): static { $str = clone $this; $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_RIGHT); return $str; } public function padStart(int $length, string $padStr = ' '): static { $str = clone $this; $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_LEFT); return $str; } public function prepend(string ...$prefix): static { $str = clone $this; $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$str->string; return $str; } public function replace(string $from, string $to): static { $str = clone $this; if ('' !== $from) { $str->string = $this->ignoreCase ? str_ireplace($from, $to, $this->string) : str_replace($from, $to, $this->string); } return $str; } public function replaceMatches(string $fromRegexp, string|callable $to): static { if ($this->ignoreCase) { $fromRegexp .= 'i'; } $replace = \is_array($to) || $to instanceof \Closure ? 'preg_replace_callback' : 'preg_replace'; set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); try { if (null === $string = $replace($fromRegexp, $to, $this->string)) { $lastError = preg_last_error(); foreach (get_defined_constants(true)['pcre'] as $k => $v) { if ($lastError === $v && '_ERROR' === substr($k, -6)) { throw new RuntimeException('Matching failed with '.$k.'.'); } } throw new RuntimeException('Matching failed with unknown error code.'); } } finally { restore_error_handler(); } $str = clone $this; $str->string = $string; return $str; } public function reverse(): static { $str = clone $this; $str->string = strrev($str->string); return $str; } public function slice(int $start = 0, int $length = null): static { $str = clone $this; $str->string = (string) substr($this->string, $start, $length ?? \PHP_INT_MAX); return $str; } public function snake(): static { $str = $this->camel(); $str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string)); return $str; } public function splice(string $replacement, int $start = 0, int $length = null): static { $str = clone $this; $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); return $str; } public function split(string $delimiter, int $limit = null, int $flags = null): array { if (1 > $limit = $limit ?? \PHP_INT_MAX) { throw new InvalidArgumentException('Split limit must be a positive integer.'); } if ('' === $delimiter) { throw new InvalidArgumentException('Split delimiter is empty.'); } if (null !== $flags) { return parent::split($delimiter, $limit, $flags); } $str = clone $this; $chunks = $this->ignoreCase ? preg_split('{'.preg_quote($delimiter).'}iD', $this->string, $limit) : explode($delimiter, $this->string, $limit); foreach ($chunks as &$chunk) { $str->string = $chunk; $chunk = clone $str; } return $chunks; } public function startsWith(string|iterable|AbstractString $prefix): bool { if ($prefix instanceof AbstractString) { $prefix = $prefix->string; } elseif (!\is_string($prefix)) { return parent::startsWith($prefix); } return '' !== $prefix && 0 === ($this->ignoreCase ? strncasecmp($this->string, $prefix, \strlen($prefix)) : strncmp($this->string, $prefix, \strlen($prefix))); } public function title(bool $allWords = false): static { $str = clone $this; $str->string = $allWords ? ucwords($str->string) : ucfirst($str->string); return $str; } public function toUnicodeString(string $fromEncoding = null): UnicodeString { return new UnicodeString($this->toCodePointString($fromEncoding)->string); } public function toCodePointString(string $fromEncoding = null): CodePointString { $u = new CodePointString(); if (\in_array($fromEncoding, [null, 'utf8', 'utf-8', 'UTF8', 'UTF-8'], true) && preg_match('//u', $this->string)) { $u->string = $this->string; return $u; } set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); try { try { $validEncoding = false !== mb_detect_encoding($this->string, $fromEncoding ?? 'Windows-1252', true); } catch (InvalidArgumentException $e) { if (!\function_exists('iconv')) { throw $e; } $u->string = iconv($fromEncoding ?? 'Windows-1252', 'UTF-8', $this->string); return $u; } } finally { restore_error_handler(); } if (!$validEncoding) { throw new InvalidArgumentException(sprintf('Invalid "%s" string.', $fromEncoding ?? 'Windows-1252')); } $u->string = mb_convert_encoding($this->string, 'UTF-8', $fromEncoding ?? 'Windows-1252'); return $u; } public function trim(string $chars = " \t\n\r\0\x0B\x0C"): static { $str = clone $this; $str->string = trim($str->string, $chars); return $str; } public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C"): static { $str = clone $this; $str->string = rtrim($str->string, $chars); return $str; } public function trimStart(string $chars = " \t\n\r\0\x0B\x0C"): static { $str = clone $this; $str->string = ltrim($str->string, $chars); return $str; } public function upper(): static { $str = clone $this; $str->string = strtoupper($str->string); return $str; } public function width(bool $ignoreAnsiDecoration = true): int { $string = preg_match('//u', $this->string) ? $this->string : preg_replace('/[\x80-\xFF]/', '?', $this->string); return (new CodePointString($string))->width($ignoreAnsiDecoration); } } string/Resources/data/wcswidth_table_wide.php 0000644 00000030251 15025017654 0015467 0 ustar 00 <?php /* * This file has been auto-generated by the Symfony String Component for internal use. * * Unicode version: 15.0.0 * Date: 2022-10-05T17:16:36+02:00 */ return [ [ 4352, 4447, ], [ 8986, 8987, ], [ 9001, 9001, ], [ 9002, 9002, ], [ 9193, 9196, ], [ 9200, 9200, ], [ 9203, 9203, ], [ 9725, 9726, ], [ 9748, 9749, ], [ 9800, 9811, ], [ 9855, 9855, ], [ 9875, 9875, ], [ 9889, 9889, ], [ 9898, 9899, ], [ 9917, 9918, ], [ 9924, 9925, ], [ 9934, 9934, ], [ 9940, 9940, ], [ 9962, 9962, ], [ 9970, 9971, ], [ 9973, 9973, ], [ 9978, 9978, ], [ 9981, 9981, ], [ 9989, 9989, ], [ 9994, 9995, ], [ 10024, 10024, ], [ 10060, 10060, ], [ 10062, 10062, ], [ 10067, 10069, ], [ 10071, 10071, ], [ 10133, 10135, ], [ 10160, 10160, ], [ 10175, 10175, ], [ 11035, 11036, ], [ 11088, 11088, ], [ 11093, 11093, ], [ 11904, 11929, ], [ 11931, 12019, ], [ 12032, 12245, ], [ 12272, 12283, ], [ 12288, 12288, ], [ 12289, 12291, ], [ 12292, 12292, ], [ 12293, 12293, ], [ 12294, 12294, ], [ 12295, 12295, ], [ 12296, 12296, ], [ 12297, 12297, ], [ 12298, 12298, ], [ 12299, 12299, ], [ 12300, 12300, ], [ 12301, 12301, ], [ 12302, 12302, ], [ 12303, 12303, ], [ 12304, 12304, ], [ 12305, 12305, ], [ 12306, 12307, ], [ 12308, 12308, ], [ 12309, 12309, ], [ 12310, 12310, ], [ 12311, 12311, ], [ 12312, 12312, ], [ 12313, 12313, ], [ 12314, 12314, ], [ 12315, 12315, ], [ 12316, 12316, ], [ 12317, 12317, ], [ 12318, 12319, ], [ 12320, 12320, ], [ 12321, 12329, ], [ 12330, 12333, ], [ 12334, 12335, ], [ 12336, 12336, ], [ 12337, 12341, ], [ 12342, 12343, ], [ 12344, 12346, ], [ 12347, 12347, ], [ 12348, 12348, ], [ 12349, 12349, ], [ 12350, 12350, ], [ 12353, 12438, ], [ 12441, 12442, ], [ 12443, 12444, ], [ 12445, 12446, ], [ 12447, 12447, ], [ 12448, 12448, ], [ 12449, 12538, ], [ 12539, 12539, ], [ 12540, 12542, ], [ 12543, 12543, ], [ 12549, 12591, ], [ 12593, 12686, ], [ 12688, 12689, ], [ 12690, 12693, ], [ 12694, 12703, ], [ 12704, 12735, ], [ 12736, 12771, ], [ 12784, 12799, ], [ 12800, 12830, ], [ 12832, 12841, ], [ 12842, 12871, ], [ 12880, 12880, ], [ 12881, 12895, ], [ 12896, 12927, ], [ 12928, 12937, ], [ 12938, 12976, ], [ 12977, 12991, ], [ 12992, 13055, ], [ 13056, 13311, ], [ 13312, 19903, ], [ 19968, 40959, ], [ 40960, 40980, ], [ 40981, 40981, ], [ 40982, 42124, ], [ 42128, 42182, ], [ 43360, 43388, ], [ 44032, 55203, ], [ 63744, 64109, ], [ 64110, 64111, ], [ 64112, 64217, ], [ 64218, 64255, ], [ 65040, 65046, ], [ 65047, 65047, ], [ 65048, 65048, ], [ 65049, 65049, ], [ 65072, 65072, ], [ 65073, 65074, ], [ 65075, 65076, ], [ 65077, 65077, ], [ 65078, 65078, ], [ 65079, 65079, ], [ 65080, 65080, ], [ 65081, 65081, ], [ 65082, 65082, ], [ 65083, 65083, ], [ 65084, 65084, ], [ 65085, 65085, ], [ 65086, 65086, ], [ 65087, 65087, ], [ 65088, 65088, ], [ 65089, 65089, ], [ 65090, 65090, ], [ 65091, 65091, ], [ 65092, 65092, ], [ 65093, 65094, ], [ 65095, 65095, ], [ 65096, 65096, ], [ 65097, 65100, ], [ 65101, 65103, ], [ 65104, 65106, ], [ 65108, 65111, ], [ 65112, 65112, ], [ 65113, 65113, ], [ 65114, 65114, ], [ 65115, 65115, ], [ 65116, 65116, ], [ 65117, 65117, ], [ 65118, 65118, ], [ 65119, 65121, ], [ 65122, 65122, ], [ 65123, 65123, ], [ 65124, 65126, ], [ 65128, 65128, ], [ 65129, 65129, ], [ 65130, 65131, ], [ 65281, 65283, ], [ 65284, 65284, ], [ 65285, 65287, ], [ 65288, 65288, ], [ 65289, 65289, ], [ 65290, 65290, ], [ 65291, 65291, ], [ 65292, 65292, ], [ 65293, 65293, ], [ 65294, 65295, ], [ 65296, 65305, ], [ 65306, 65307, ], [ 65308, 65310, ], [ 65311, 65312, ], [ 65313, 65338, ], [ 65339, 65339, ], [ 65340, 65340, ], [ 65341, 65341, ], [ 65342, 65342, ], [ 65343, 65343, ], [ 65344, 65344, ], [ 65345, 65370, ], [ 65371, 65371, ], [ 65372, 65372, ], [ 65373, 65373, ], [ 65374, 65374, ], [ 65375, 65375, ], [ 65376, 65376, ], [ 65504, 65505, ], [ 65506, 65506, ], [ 65507, 65507, ], [ 65508, 65508, ], [ 65509, 65510, ], [ 94176, 94177, ], [ 94178, 94178, ], [ 94179, 94179, ], [ 94180, 94180, ], [ 94192, 94193, ], [ 94208, 100343, ], [ 100352, 101119, ], [ 101120, 101589, ], [ 101632, 101640, ], [ 110576, 110579, ], [ 110581, 110587, ], [ 110589, 110590, ], [ 110592, 110847, ], [ 110848, 110882, ], [ 110898, 110898, ], [ 110928, 110930, ], [ 110933, 110933, ], [ 110948, 110951, ], [ 110960, 111355, ], [ 126980, 126980, ], [ 127183, 127183, ], [ 127374, 127374, ], [ 127377, 127386, ], [ 127488, 127490, ], [ 127504, 127547, ], [ 127552, 127560, ], [ 127568, 127569, ], [ 127584, 127589, ], [ 127744, 127776, ], [ 127789, 127797, ], [ 127799, 127868, ], [ 127870, 127891, ], [ 127904, 127946, ], [ 127951, 127955, ], [ 127968, 127984, ], [ 127988, 127988, ], [ 127992, 127994, ], [ 127995, 127999, ], [ 128000, 128062, ], [ 128064, 128064, ], [ 128066, 128252, ], [ 128255, 128317, ], [ 128331, 128334, ], [ 128336, 128359, ], [ 128378, 128378, ], [ 128405, 128406, ], [ 128420, 128420, ], [ 128507, 128511, ], [ 128512, 128591, ], [ 128640, 128709, ], [ 128716, 128716, ], [ 128720, 128722, ], [ 128725, 128727, ], [ 128732, 128735, ], [ 128747, 128748, ], [ 128756, 128764, ], [ 128992, 129003, ], [ 129008, 129008, ], [ 129292, 129338, ], [ 129340, 129349, ], [ 129351, 129535, ], [ 129648, 129660, ], [ 129664, 129672, ], [ 129680, 129725, ], [ 129727, 129733, ], [ 129742, 129755, ], [ 129760, 129768, ], [ 129776, 129784, ], [ 131072, 173791, ], [ 173792, 173823, ], [ 173824, 177977, ], [ 177978, 177983, ], [ 177984, 178205, ], [ 178206, 178207, ], [ 178208, 183969, ], [ 183970, 183983, ], [ 183984, 191456, ], [ 191457, 194559, ], [ 194560, 195101, ], [ 195102, 195103, ], [ 195104, 196605, ], [ 196608, 201546, ], [ 201547, 201551, ], [ 201552, 205743, ], [ 205744, 262141, ], ]; string/Resources/data/wcswidth_table_zero.php 0000644 00000035202 15025017654 0015517 0 ustar 00 <?php /* * This file has been auto-generated by the Symfony String Component for internal use. * * Unicode version: 15.0.0 * Date: 2022-10-05T17:16:37+02:00 */ return [ [ 768, 879, ], [ 1155, 1159, ], [ 1160, 1161, ], [ 1425, 1469, ], [ 1471, 1471, ], [ 1473, 1474, ], [ 1476, 1477, ], [ 1479, 1479, ], [ 1552, 1562, ], [ 1611, 1631, ], [ 1648, 1648, ], [ 1750, 1756, ], [ 1759, 1764, ], [ 1767, 1768, ], [ 1770, 1773, ], [ 1809, 1809, ], [ 1840, 1866, ], [ 1958, 1968, ], [ 2027, 2035, ], [ 2045, 2045, ], [ 2070, 2073, ], [ 2075, 2083, ], [ 2085, 2087, ], [ 2089, 2093, ], [ 2137, 2139, ], [ 2200, 2207, ], [ 2250, 2273, ], [ 2275, 2306, ], [ 2362, 2362, ], [ 2364, 2364, ], [ 2369, 2376, ], [ 2381, 2381, ], [ 2385, 2391, ], [ 2402, 2403, ], [ 2433, 2433, ], [ 2492, 2492, ], [ 2497, 2500, ], [ 2509, 2509, ], [ 2530, 2531, ], [ 2558, 2558, ], [ 2561, 2562, ], [ 2620, 2620, ], [ 2625, 2626, ], [ 2631, 2632, ], [ 2635, 2637, ], [ 2641, 2641, ], [ 2672, 2673, ], [ 2677, 2677, ], [ 2689, 2690, ], [ 2748, 2748, ], [ 2753, 2757, ], [ 2759, 2760, ], [ 2765, 2765, ], [ 2786, 2787, ], [ 2810, 2815, ], [ 2817, 2817, ], [ 2876, 2876, ], [ 2879, 2879, ], [ 2881, 2884, ], [ 2893, 2893, ], [ 2901, 2902, ], [ 2914, 2915, ], [ 2946, 2946, ], [ 3008, 3008, ], [ 3021, 3021, ], [ 3072, 3072, ], [ 3076, 3076, ], [ 3132, 3132, ], [ 3134, 3136, ], [ 3142, 3144, ], [ 3146, 3149, ], [ 3157, 3158, ], [ 3170, 3171, ], [ 3201, 3201, ], [ 3260, 3260, ], [ 3263, 3263, ], [ 3270, 3270, ], [ 3276, 3277, ], [ 3298, 3299, ], [ 3328, 3329, ], [ 3387, 3388, ], [ 3393, 3396, ], [ 3405, 3405, ], [ 3426, 3427, ], [ 3457, 3457, ], [ 3530, 3530, ], [ 3538, 3540, ], [ 3542, 3542, ], [ 3633, 3633, ], [ 3636, 3642, ], [ 3655, 3662, ], [ 3761, 3761, ], [ 3764, 3772, ], [ 3784, 3790, ], [ 3864, 3865, ], [ 3893, 3893, ], [ 3895, 3895, ], [ 3897, 3897, ], [ 3953, 3966, ], [ 3968, 3972, ], [ 3974, 3975, ], [ 3981, 3991, ], [ 3993, 4028, ], [ 4038, 4038, ], [ 4141, 4144, ], [ 4146, 4151, ], [ 4153, 4154, ], [ 4157, 4158, ], [ 4184, 4185, ], [ 4190, 4192, ], [ 4209, 4212, ], [ 4226, 4226, ], [ 4229, 4230, ], [ 4237, 4237, ], [ 4253, 4253, ], [ 4957, 4959, ], [ 5906, 5908, ], [ 5938, 5939, ], [ 5970, 5971, ], [ 6002, 6003, ], [ 6068, 6069, ], [ 6071, 6077, ], [ 6086, 6086, ], [ 6089, 6099, ], [ 6109, 6109, ], [ 6155, 6157, ], [ 6159, 6159, ], [ 6277, 6278, ], [ 6313, 6313, ], [ 6432, 6434, ], [ 6439, 6440, ], [ 6450, 6450, ], [ 6457, 6459, ], [ 6679, 6680, ], [ 6683, 6683, ], [ 6742, 6742, ], [ 6744, 6750, ], [ 6752, 6752, ], [ 6754, 6754, ], [ 6757, 6764, ], [ 6771, 6780, ], [ 6783, 6783, ], [ 6832, 6845, ], [ 6846, 6846, ], [ 6847, 6862, ], [ 6912, 6915, ], [ 6964, 6964, ], [ 6966, 6970, ], [ 6972, 6972, ], [ 6978, 6978, ], [ 7019, 7027, ], [ 7040, 7041, ], [ 7074, 7077, ], [ 7080, 7081, ], [ 7083, 7085, ], [ 7142, 7142, ], [ 7144, 7145, ], [ 7149, 7149, ], [ 7151, 7153, ], [ 7212, 7219, ], [ 7222, 7223, ], [ 7376, 7378, ], [ 7380, 7392, ], [ 7394, 7400, ], [ 7405, 7405, ], [ 7412, 7412, ], [ 7416, 7417, ], [ 7616, 7679, ], [ 8400, 8412, ], [ 8413, 8416, ], [ 8417, 8417, ], [ 8418, 8420, ], [ 8421, 8432, ], [ 11503, 11505, ], [ 11647, 11647, ], [ 11744, 11775, ], [ 12330, 12333, ], [ 12441, 12442, ], [ 42607, 42607, ], [ 42608, 42610, ], [ 42612, 42621, ], [ 42654, 42655, ], [ 42736, 42737, ], [ 43010, 43010, ], [ 43014, 43014, ], [ 43019, 43019, ], [ 43045, 43046, ], [ 43052, 43052, ], [ 43204, 43205, ], [ 43232, 43249, ], [ 43263, 43263, ], [ 43302, 43309, ], [ 43335, 43345, ], [ 43392, 43394, ], [ 43443, 43443, ], [ 43446, 43449, ], [ 43452, 43453, ], [ 43493, 43493, ], [ 43561, 43566, ], [ 43569, 43570, ], [ 43573, 43574, ], [ 43587, 43587, ], [ 43596, 43596, ], [ 43644, 43644, ], [ 43696, 43696, ], [ 43698, 43700, ], [ 43703, 43704, ], [ 43710, 43711, ], [ 43713, 43713, ], [ 43756, 43757, ], [ 43766, 43766, ], [ 44005, 44005, ], [ 44008, 44008, ], [ 44013, 44013, ], [ 64286, 64286, ], [ 65024, 65039, ], [ 65056, 65071, ], [ 66045, 66045, ], [ 66272, 66272, ], [ 66422, 66426, ], [ 68097, 68099, ], [ 68101, 68102, ], [ 68108, 68111, ], [ 68152, 68154, ], [ 68159, 68159, ], [ 68325, 68326, ], [ 68900, 68903, ], [ 69291, 69292, ], [ 69373, 69375, ], [ 69446, 69456, ], [ 69506, 69509, ], [ 69633, 69633, ], [ 69688, 69702, ], [ 69744, 69744, ], [ 69747, 69748, ], [ 69759, 69761, ], [ 69811, 69814, ], [ 69817, 69818, ], [ 69826, 69826, ], [ 69888, 69890, ], [ 69927, 69931, ], [ 69933, 69940, ], [ 70003, 70003, ], [ 70016, 70017, ], [ 70070, 70078, ], [ 70089, 70092, ], [ 70095, 70095, ], [ 70191, 70193, ], [ 70196, 70196, ], [ 70198, 70199, ], [ 70206, 70206, ], [ 70209, 70209, ], [ 70367, 70367, ], [ 70371, 70378, ], [ 70400, 70401, ], [ 70459, 70460, ], [ 70464, 70464, ], [ 70502, 70508, ], [ 70512, 70516, ], [ 70712, 70719, ], [ 70722, 70724, ], [ 70726, 70726, ], [ 70750, 70750, ], [ 70835, 70840, ], [ 70842, 70842, ], [ 70847, 70848, ], [ 70850, 70851, ], [ 71090, 71093, ], [ 71100, 71101, ], [ 71103, 71104, ], [ 71132, 71133, ], [ 71219, 71226, ], [ 71229, 71229, ], [ 71231, 71232, ], [ 71339, 71339, ], [ 71341, 71341, ], [ 71344, 71349, ], [ 71351, 71351, ], [ 71453, 71455, ], [ 71458, 71461, ], [ 71463, 71467, ], [ 71727, 71735, ], [ 71737, 71738, ], [ 71995, 71996, ], [ 71998, 71998, ], [ 72003, 72003, ], [ 72148, 72151, ], [ 72154, 72155, ], [ 72160, 72160, ], [ 72193, 72202, ], [ 72243, 72248, ], [ 72251, 72254, ], [ 72263, 72263, ], [ 72273, 72278, ], [ 72281, 72283, ], [ 72330, 72342, ], [ 72344, 72345, ], [ 72752, 72758, ], [ 72760, 72765, ], [ 72767, 72767, ], [ 72850, 72871, ], [ 72874, 72880, ], [ 72882, 72883, ], [ 72885, 72886, ], [ 73009, 73014, ], [ 73018, 73018, ], [ 73020, 73021, ], [ 73023, 73029, ], [ 73031, 73031, ], [ 73104, 73105, ], [ 73109, 73109, ], [ 73111, 73111, ], [ 73459, 73460, ], [ 73472, 73473, ], [ 73526, 73530, ], [ 73536, 73536, ], [ 73538, 73538, ], [ 78912, 78912, ], [ 78919, 78933, ], [ 92912, 92916, ], [ 92976, 92982, ], [ 94031, 94031, ], [ 94095, 94098, ], [ 94180, 94180, ], [ 113821, 113822, ], [ 118528, 118573, ], [ 118576, 118598, ], [ 119143, 119145, ], [ 119163, 119170, ], [ 119173, 119179, ], [ 119210, 119213, ], [ 119362, 119364, ], [ 121344, 121398, ], [ 121403, 121452, ], [ 121461, 121461, ], [ 121476, 121476, ], [ 121499, 121503, ], [ 121505, 121519, ], [ 122880, 122886, ], [ 122888, 122904, ], [ 122907, 122913, ], [ 122915, 122916, ], [ 122918, 122922, ], [ 123023, 123023, ], [ 123184, 123190, ], [ 123566, 123566, ], [ 123628, 123631, ], [ 124140, 124143, ], [ 125136, 125142, ], [ 125252, 125258, ], [ 917760, 917999, ], ]; string/Resources/functions.php 0000644 00000001535 15025017654 0012556 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; if (!\function_exists(u::class)) { function u(?string $string = ''): UnicodeString { return new UnicodeString($string ?? ''); } } if (!\function_exists(b::class)) { function b(?string $string = ''): ByteString { return new ByteString($string ?? ''); } } if (!\function_exists(s::class)) { /** * @return UnicodeString|ByteString */ function s(?string $string = ''): AbstractString { $string = $string ?? ''; return preg_match('//u', $string) ? new UnicodeString($string) : new ByteString($string); } } var-dumper/Caster/SplCaster.php 0000644 00000016373 15025017654 0012477 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts SPL related classes to array representation. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class SplCaster { private const SPL_FILE_OBJECT_FLAGS = [ \SplFileObject::DROP_NEW_LINE => 'DROP_NEW_LINE', \SplFileObject::READ_AHEAD => 'READ_AHEAD', \SplFileObject::SKIP_EMPTY => 'SKIP_EMPTY', \SplFileObject::READ_CSV => 'READ_CSV', ]; public static function castArrayObject(\ArrayObject $c, array $a, Stub $stub, bool $isNested) { return self::castSplArray($c, $a, $stub, $isNested); } public static function castArrayIterator(\ArrayIterator $c, array $a, Stub $stub, bool $isNested) { return self::castSplArray($c, $a, $stub, $isNested); } public static function castHeap(\Iterator $c, array $a, Stub $stub, bool $isNested) { $a += [ Caster::PREFIX_VIRTUAL.'heap' => iterator_to_array(clone $c), ]; return $a; } public static function castDoublyLinkedList(\SplDoublyLinkedList $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $mode = $c->getIteratorMode(); $c->setIteratorMode(\SplDoublyLinkedList::IT_MODE_KEEP | $mode & ~\SplDoublyLinkedList::IT_MODE_DELETE); $a += [ $prefix.'mode' => new ConstStub((($mode & \SplDoublyLinkedList::IT_MODE_LIFO) ? 'IT_MODE_LIFO' : 'IT_MODE_FIFO').' | '.(($mode & \SplDoublyLinkedList::IT_MODE_DELETE) ? 'IT_MODE_DELETE' : 'IT_MODE_KEEP'), $mode), $prefix.'dllist' => iterator_to_array($c), ]; $c->setIteratorMode($mode); return $a; } public static function castFileInfo(\SplFileInfo $c, array $a, Stub $stub, bool $isNested) { static $map = [ 'path' => 'getPath', 'filename' => 'getFilename', 'basename' => 'getBasename', 'pathname' => 'getPathname', 'extension' => 'getExtension', 'realPath' => 'getRealPath', 'aTime' => 'getATime', 'mTime' => 'getMTime', 'cTime' => 'getCTime', 'inode' => 'getInode', 'size' => 'getSize', 'perms' => 'getPerms', 'owner' => 'getOwner', 'group' => 'getGroup', 'type' => 'getType', 'writable' => 'isWritable', 'readable' => 'isReadable', 'executable' => 'isExecutable', 'file' => 'isFile', 'dir' => 'isDir', 'link' => 'isLink', 'linkTarget' => 'getLinkTarget', ]; $prefix = Caster::PREFIX_VIRTUAL; unset($a["\0SplFileInfo\0fileName"]); unset($a["\0SplFileInfo\0pathName"]); try { $c->isReadable(); } catch (\RuntimeException $e) { if ('Object not initialized' !== $e->getMessage()) { throw $e; } $a[$prefix.'⚠'] = 'The parent constructor was not called: the object is in an invalid state'; return $a; } catch (\Error $e) { if ('Object not initialized' !== $e->getMessage()) { throw $e; } $a[$prefix.'⚠'] = 'The parent constructor was not called: the object is in an invalid state'; return $a; } foreach ($map as $key => $accessor) { try { $a[$prefix.$key] = $c->$accessor(); } catch (\Exception $e) { } } if ($a[$prefix.'realPath'] ?? false) { $a[$prefix.'realPath'] = new LinkStub($a[$prefix.'realPath']); } if (isset($a[$prefix.'perms'])) { $a[$prefix.'perms'] = new ConstStub(sprintf('0%o', $a[$prefix.'perms']), $a[$prefix.'perms']); } static $mapDate = ['aTime', 'mTime', 'cTime']; foreach ($mapDate as $key) { if (isset($a[$prefix.$key])) { $a[$prefix.$key] = new ConstStub(date('Y-m-d H:i:s', $a[$prefix.$key]), $a[$prefix.$key]); } } return $a; } public static function castFileObject(\SplFileObject $c, array $a, Stub $stub, bool $isNested) { static $map = [ 'csvControl' => 'getCsvControl', 'flags' => 'getFlags', 'maxLineLen' => 'getMaxLineLen', 'fstat' => 'fstat', 'eof' => 'eof', 'key' => 'key', ]; $prefix = Caster::PREFIX_VIRTUAL; foreach ($map as $key => $accessor) { try { $a[$prefix.$key] = $c->$accessor(); } catch (\Exception $e) { } } if (isset($a[$prefix.'flags'])) { $flagsArray = []; foreach (self::SPL_FILE_OBJECT_FLAGS as $value => $name) { if ($a[$prefix.'flags'] & $value) { $flagsArray[] = $name; } } $a[$prefix.'flags'] = new ConstStub(implode('|', $flagsArray), $a[$prefix.'flags']); } if (isset($a[$prefix.'fstat'])) { $a[$prefix.'fstat'] = new CutArrayStub($a[$prefix.'fstat'], ['dev', 'ino', 'nlink', 'rdev', 'blksize', 'blocks']); } return $a; } public static function castObjectStorage(\SplObjectStorage $c, array $a, Stub $stub, bool $isNested) { $storage = []; unset($a[Caster::PREFIX_DYNAMIC."\0gcdata"]); // Don't hit https://bugs.php.net/65967 unset($a["\0SplObjectStorage\0storage"]); $clone = clone $c; foreach ($clone as $obj) { $storage[] = [ 'object' => $obj, 'info' => $clone->getInfo(), ]; } $a += [ Caster::PREFIX_VIRTUAL.'storage' => $storage, ]; return $a; } public static function castOuterIterator(\OuterIterator $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'innerIterator'] = $c->getInnerIterator(); return $a; } public static function castWeakReference(\WeakReference $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'object'] = $c->get(); return $a; } private static function castSplArray(\ArrayObject|\ArrayIterator $c, array $a, Stub $stub, bool $isNested): array { $prefix = Caster::PREFIX_VIRTUAL; $flags = $c->getFlags(); if (!($flags & \ArrayObject::STD_PROP_LIST)) { $c->setFlags(\ArrayObject::STD_PROP_LIST); $a = Caster::castObject($c, \get_class($c), method_exists($c, '__debugInfo'), $stub->class); $c->setFlags($flags); } $a += [ $prefix.'flag::STD_PROP_LIST' => (bool) ($flags & \ArrayObject::STD_PROP_LIST), $prefix.'flag::ARRAY_AS_PROPS' => (bool) ($flags & \ArrayObject::ARRAY_AS_PROPS), ]; if ($c instanceof \ArrayObject) { $a[$prefix.'iteratorClass'] = new ClassStub($c->getIteratorClass()); } return $a; } } var-dumper/Caster/ReflectionCaster.php 0000644 00000035264 15025017654 0014033 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts Reflector related classes to array representation. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class ReflectionCaster { public const UNSET_CLOSURE_FILE_INFO = ['Closure' => __CLASS__.'::unsetClosureFileInfo']; private const EXTRA_MAP = [ 'docComment' => 'getDocComment', 'extension' => 'getExtensionName', 'isDisabled' => 'isDisabled', 'isDeprecated' => 'isDeprecated', 'isInternal' => 'isInternal', 'isUserDefined' => 'isUserDefined', 'isGenerator' => 'isGenerator', 'isVariadic' => 'isVariadic', ]; public static function castClosure(\Closure $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; $c = new \ReflectionFunction($c); $a = static::castFunctionAbstract($c, $a, $stub, $isNested, $filter); if (!str_contains($c->name, '{closure}')) { $stub->class = isset($a[$prefix.'class']) ? $a[$prefix.'class']->value.'::'.$c->name : $c->name; unset($a[$prefix.'class']); } unset($a[$prefix.'extra']); $stub->class .= self::getSignature($a); if ($f = $c->getFileName()) { $stub->attr['file'] = $f; $stub->attr['line'] = $c->getStartLine(); } unset($a[$prefix.'parameters']); if ($filter & Caster::EXCLUDE_VERBOSE) { $stub->cut += ($c->getFileName() ? 2 : 0) + \count($a); return []; } if ($f) { $a[$prefix.'file'] = new LinkStub($f, $c->getStartLine()); $a[$prefix.'line'] = $c->getStartLine().' to '.$c->getEndLine(); } return $a; } public static function unsetClosureFileInfo(\Closure $c, array $a) { unset($a[Caster::PREFIX_VIRTUAL.'file'], $a[Caster::PREFIX_VIRTUAL.'line']); return $a; } public static function castGenerator(\Generator $c, array $a, Stub $stub, bool $isNested) { // Cannot create ReflectionGenerator based on a terminated Generator try { $reflectionGenerator = new \ReflectionGenerator($c); } catch (\Exception $e) { $a[Caster::PREFIX_VIRTUAL.'closed'] = true; return $a; } return self::castReflectionGenerator($reflectionGenerator, $a, $stub, $isNested); } public static function castType(\ReflectionType $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; if ($c instanceof \ReflectionNamedType) { $a += [ $prefix.'name' => $c instanceof \ReflectionNamedType ? $c->getName() : (string) $c, $prefix.'allowsNull' => $c->allowsNull(), $prefix.'isBuiltin' => $c->isBuiltin(), ]; } elseif ($c instanceof \ReflectionUnionType || $c instanceof \ReflectionIntersectionType) { $a[$prefix.'allowsNull'] = $c->allowsNull(); self::addMap($a, $c, [ 'types' => 'getTypes', ]); } else { $a[$prefix.'allowsNull'] = $c->allowsNull(); } return $a; } public static function castAttribute(\ReflectionAttribute $c, array $a, Stub $stub, bool $isNested) { self::addMap($a, $c, [ 'name' => 'getName', 'arguments' => 'getArguments', ]); return $a; } public static function castReflectionGenerator(\ReflectionGenerator $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; if ($c->getThis()) { $a[$prefix.'this'] = new CutStub($c->getThis()); } $function = $c->getFunction(); $frame = [ 'class' => $function->class ?? null, 'type' => isset($function->class) ? ($function->isStatic() ? '::' : '->') : null, 'function' => $function->name, 'file' => $c->getExecutingFile(), 'line' => $c->getExecutingLine(), ]; if ($trace = $c->getTrace(\DEBUG_BACKTRACE_IGNORE_ARGS)) { $function = new \ReflectionGenerator($c->getExecutingGenerator()); array_unshift($trace, [ 'function' => 'yield', 'file' => $function->getExecutingFile(), 'line' => $function->getExecutingLine() - (int) (\PHP_VERSION_ID < 80100), ]); $trace[] = $frame; $a[$prefix.'trace'] = new TraceStub($trace, false, 0, -1, -1); } else { $function = new FrameStub($frame, false, true); $function = ExceptionCaster::castFrameStub($function, [], $function, true); $a[$prefix.'executing'] = $function[$prefix.'src']; } $a[Caster::PREFIX_VIRTUAL.'closed'] = false; return $a; } public static function castClass(\ReflectionClass $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; if ($n = \Reflection::getModifierNames($c->getModifiers())) { $a[$prefix.'modifiers'] = implode(' ', $n); } self::addMap($a, $c, [ 'extends' => 'getParentClass', 'implements' => 'getInterfaceNames', 'constants' => 'getReflectionConstants', ]); foreach ($c->getProperties() as $n) { $a[$prefix.'properties'][$n->name] = $n; } foreach ($c->getMethods() as $n) { $a[$prefix.'methods'][$n->name] = $n; } self::addAttributes($a, $c, $prefix); if (!($filter & Caster::EXCLUDE_VERBOSE) && !$isNested) { self::addExtra($a, $c); } return $a; } public static function castFunctionAbstract(\ReflectionFunctionAbstract $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; self::addMap($a, $c, [ 'returnsReference' => 'returnsReference', 'returnType' => 'getReturnType', 'class' => \PHP_VERSION_ID >= 80111 ? 'getClosureCalledClass' : 'getClosureScopeClass', 'this' => 'getClosureThis', ]); if (isset($a[$prefix.'returnType'])) { $v = $a[$prefix.'returnType']; $v = $v instanceof \ReflectionNamedType ? $v->getName() : (string) $v; $a[$prefix.'returnType'] = new ClassStub($a[$prefix.'returnType'] instanceof \ReflectionNamedType && $a[$prefix.'returnType']->allowsNull() && 'mixed' !== $v ? '?'.$v : $v, [class_exists($v, false) || interface_exists($v, false) || trait_exists($v, false) ? $v : '', '']); } if (isset($a[$prefix.'class'])) { $a[$prefix.'class'] = new ClassStub($a[$prefix.'class']); } if (isset($a[$prefix.'this'])) { $a[$prefix.'this'] = new CutStub($a[$prefix.'this']); } foreach ($c->getParameters() as $v) { $k = '$'.$v->name; if ($v->isVariadic()) { $k = '...'.$k; } if ($v->isPassedByReference()) { $k = '&'.$k; } $a[$prefix.'parameters'][$k] = $v; } if (isset($a[$prefix.'parameters'])) { $a[$prefix.'parameters'] = new EnumStub($a[$prefix.'parameters']); } self::addAttributes($a, $c, $prefix); if (!($filter & Caster::EXCLUDE_VERBOSE) && $v = $c->getStaticVariables()) { foreach ($v as $k => &$v) { if (\is_object($v)) { $a[$prefix.'use']['$'.$k] = new CutStub($v); } else { $a[$prefix.'use']['$'.$k] = &$v; } } unset($v); $a[$prefix.'use'] = new EnumStub($a[$prefix.'use']); } if (!($filter & Caster::EXCLUDE_VERBOSE) && !$isNested) { self::addExtra($a, $c); } return $a; } public static function castClassConstant(\ReflectionClassConstant $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers())); $a[Caster::PREFIX_VIRTUAL.'value'] = $c->getValue(); self::addAttributes($a, $c); return $a; } public static function castMethod(\ReflectionMethod $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers())); return $a; } public static function castParameter(\ReflectionParameter $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; self::addMap($a, $c, [ 'position' => 'getPosition', 'isVariadic' => 'isVariadic', 'byReference' => 'isPassedByReference', 'allowsNull' => 'allowsNull', ]); self::addAttributes($a, $c, $prefix); if ($v = $c->getType()) { $a[$prefix.'typeHint'] = $v instanceof \ReflectionNamedType ? $v->getName() : (string) $v; } if (isset($a[$prefix.'typeHint'])) { $v = $a[$prefix.'typeHint']; $a[$prefix.'typeHint'] = new ClassStub($v, [class_exists($v, false) || interface_exists($v, false) || trait_exists($v, false) ? $v : '', '']); } else { unset($a[$prefix.'allowsNull']); } if ($c->isOptional()) { try { $a[$prefix.'default'] = $v = $c->getDefaultValue(); if ($c->isDefaultValueConstant()) { $a[$prefix.'default'] = new ConstStub($c->getDefaultValueConstantName(), $v); } if (null === $v) { unset($a[$prefix.'allowsNull']); } } catch (\ReflectionException $e) { } } return $a; } public static function castProperty(\ReflectionProperty $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers())); self::addAttributes($a, $c); self::addExtra($a, $c); return $a; } public static function castReference(\ReflectionReference $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'id'] = $c->getId(); return $a; } public static function castExtension(\ReflectionExtension $c, array $a, Stub $stub, bool $isNested) { self::addMap($a, $c, [ 'version' => 'getVersion', 'dependencies' => 'getDependencies', 'iniEntries' => 'getIniEntries', 'isPersistent' => 'isPersistent', 'isTemporary' => 'isTemporary', 'constants' => 'getConstants', 'functions' => 'getFunctions', 'classes' => 'getClasses', ]); return $a; } public static function castZendExtension(\ReflectionZendExtension $c, array $a, Stub $stub, bool $isNested) { self::addMap($a, $c, [ 'version' => 'getVersion', 'author' => 'getAuthor', 'copyright' => 'getCopyright', 'url' => 'getURL', ]); return $a; } public static function getSignature(array $a) { $prefix = Caster::PREFIX_VIRTUAL; $signature = ''; if (isset($a[$prefix.'parameters'])) { foreach ($a[$prefix.'parameters']->value as $k => $param) { $signature .= ', '; if ($type = $param->getType()) { if (!$type instanceof \ReflectionNamedType) { $signature .= $type.' '; } else { if (!$param->isOptional() && $param->allowsNull() && 'mixed' !== $type->getName()) { $signature .= '?'; } $signature .= substr(strrchr('\\'.$type->getName(), '\\'), 1).' '; } } $signature .= $k; if (!$param->isDefaultValueAvailable()) { continue; } $v = $param->getDefaultValue(); $signature .= ' = '; if ($param->isDefaultValueConstant()) { $signature .= substr(strrchr('\\'.$param->getDefaultValueConstantName(), '\\'), 1); } elseif (null === $v) { $signature .= 'null'; } elseif (\is_array($v)) { $signature .= $v ? '[…'.\count($v).']' : '[]'; } elseif (\is_string($v)) { $signature .= 10 > \strlen($v) && !str_contains($v, '\\') ? "'{$v}'" : "'…".\strlen($v)."'"; } elseif (\is_bool($v)) { $signature .= $v ? 'true' : 'false'; } elseif (\is_object($v)) { $signature .= 'new '.substr(strrchr('\\'.get_debug_type($v), '\\'), 1); } else { $signature .= $v; } } } $signature = (empty($a[$prefix.'returnsReference']) ? '' : '&').'('.substr($signature, 2).')'; if (isset($a[$prefix.'returnType'])) { $signature .= ': '.substr(strrchr('\\'.$a[$prefix.'returnType'], '\\'), 1); } return $signature; } private static function addExtra(array &$a, \Reflector $c) { $x = isset($a[Caster::PREFIX_VIRTUAL.'extra']) ? $a[Caster::PREFIX_VIRTUAL.'extra']->value : []; if (method_exists($c, 'getFileName') && $m = $c->getFileName()) { $x['file'] = new LinkStub($m, $c->getStartLine()); $x['line'] = $c->getStartLine().' to '.$c->getEndLine(); } self::addMap($x, $c, self::EXTRA_MAP, ''); if ($x) { $a[Caster::PREFIX_VIRTUAL.'extra'] = new EnumStub($x); } } private static function addMap(array &$a, object $c, array $map, string $prefix = Caster::PREFIX_VIRTUAL) { foreach ($map as $k => $m) { if ('isDisabled' === $k) { continue; } if (method_exists($c, $m) && false !== ($m = $c->$m()) && null !== $m) { $a[$prefix.$k] = $m instanceof \Reflector ? $m->name : $m; } } } private static function addAttributes(array &$a, \Reflector $c, string $prefix = Caster::PREFIX_VIRTUAL): void { foreach ($c->getAttributes() as $n) { $a[$prefix.'attributes'][] = $n; } } } var-dumper/Caster/ClassStub.php 0000644 00000007425 15025017654 0012500 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Represents a PHP class identifier. * * @author Nicolas Grekas <p@tchwork.com> */ class ClassStub extends ConstStub { /** * @param string $identifier A PHP identifier, e.g. a class, method, interface, etc. name * @param callable $callable The callable targeted by the identifier when it is ambiguous or not a real PHP identifier */ public function __construct(string $identifier, callable|array|string $callable = null) { $this->value = $identifier; try { if (null !== $callable) { if ($callable instanceof \Closure) { $r = new \ReflectionFunction($callable); } elseif (\is_object($callable)) { $r = [$callable, '__invoke']; } elseif (\is_array($callable)) { $r = $callable; } elseif (false !== $i = strpos($callable, '::')) { $r = [substr($callable, 0, $i), substr($callable, 2 + $i)]; } else { $r = new \ReflectionFunction($callable); } } elseif (0 < $i = strpos($identifier, '::') ?: strpos($identifier, '->')) { $r = [substr($identifier, 0, $i), substr($identifier, 2 + $i)]; } else { $r = new \ReflectionClass($identifier); } if (\is_array($r)) { try { $r = new \ReflectionMethod($r[0], $r[1]); } catch (\ReflectionException $e) { $r = new \ReflectionClass($r[0]); } } if (str_contains($identifier, "@anonymous\0")) { $this->value = $identifier = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) { return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0]; }, $identifier); } if (null !== $callable && $r instanceof \ReflectionFunctionAbstract) { $s = ReflectionCaster::castFunctionAbstract($r, [], new Stub(), true, Caster::EXCLUDE_VERBOSE); $s = ReflectionCaster::getSignature($s); if (str_ends_with($identifier, '()')) { $this->value = substr_replace($identifier, $s, -2); } else { $this->value .= $s; } } } catch (\ReflectionException $e) { return; } finally { if (0 < $i = strrpos($this->value, '\\')) { $this->attr['ellipsis'] = \strlen($this->value) - $i; $this->attr['ellipsis-type'] = 'class'; $this->attr['ellipsis-tail'] = 1; } } if ($f = $r->getFileName()) { $this->attr['file'] = $f; $this->attr['line'] = $r->getStartLine(); } } public static function wrapCallable(mixed $callable) { if (\is_object($callable) || !\is_callable($callable)) { return $callable; } if (!\is_array($callable)) { $callable = new static($callable, $callable); } elseif (\is_string($callable[0])) { $callable[0] = new static($callable[0], $callable); } else { $callable[1] = new static($callable[1], $callable); } return $callable; } } var-dumper/Caster/PgSqlCaster.php 0000644 00000012554 15025017654 0012764 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts pqsql resources to array representation. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class PgSqlCaster { private const PARAM_CODES = [ 'server_encoding', 'client_encoding', 'is_superuser', 'session_authorization', 'DateStyle', 'TimeZone', 'IntervalStyle', 'integer_datetimes', 'application_name', 'standard_conforming_strings', ]; private const TRANSACTION_STATUS = [ \PGSQL_TRANSACTION_IDLE => 'PGSQL_TRANSACTION_IDLE', \PGSQL_TRANSACTION_ACTIVE => 'PGSQL_TRANSACTION_ACTIVE', \PGSQL_TRANSACTION_INTRANS => 'PGSQL_TRANSACTION_INTRANS', \PGSQL_TRANSACTION_INERROR => 'PGSQL_TRANSACTION_INERROR', \PGSQL_TRANSACTION_UNKNOWN => 'PGSQL_TRANSACTION_UNKNOWN', ]; private const RESULT_STATUS = [ \PGSQL_EMPTY_QUERY => 'PGSQL_EMPTY_QUERY', \PGSQL_COMMAND_OK => 'PGSQL_COMMAND_OK', \PGSQL_TUPLES_OK => 'PGSQL_TUPLES_OK', \PGSQL_COPY_OUT => 'PGSQL_COPY_OUT', \PGSQL_COPY_IN => 'PGSQL_COPY_IN', \PGSQL_BAD_RESPONSE => 'PGSQL_BAD_RESPONSE', \PGSQL_NONFATAL_ERROR => 'PGSQL_NONFATAL_ERROR', \PGSQL_FATAL_ERROR => 'PGSQL_FATAL_ERROR', ]; private const DIAG_CODES = [ 'severity' => \PGSQL_DIAG_SEVERITY, 'sqlstate' => \PGSQL_DIAG_SQLSTATE, 'message' => \PGSQL_DIAG_MESSAGE_PRIMARY, 'detail' => \PGSQL_DIAG_MESSAGE_DETAIL, 'hint' => \PGSQL_DIAG_MESSAGE_HINT, 'statement position' => \PGSQL_DIAG_STATEMENT_POSITION, 'internal position' => \PGSQL_DIAG_INTERNAL_POSITION, 'internal query' => \PGSQL_DIAG_INTERNAL_QUERY, 'context' => \PGSQL_DIAG_CONTEXT, 'file' => \PGSQL_DIAG_SOURCE_FILE, 'line' => \PGSQL_DIAG_SOURCE_LINE, 'function' => \PGSQL_DIAG_SOURCE_FUNCTION, ]; public static function castLargeObject($lo, array $a, Stub $stub, bool $isNested) { $a['seek position'] = pg_lo_tell($lo); return $a; } public static function castLink($link, array $a, Stub $stub, bool $isNested) { $a['status'] = pg_connection_status($link); $a['status'] = new ConstStub(\PGSQL_CONNECTION_OK === $a['status'] ? 'PGSQL_CONNECTION_OK' : 'PGSQL_CONNECTION_BAD', $a['status']); $a['busy'] = pg_connection_busy($link); $a['transaction'] = pg_transaction_status($link); if (isset(self::TRANSACTION_STATUS[$a['transaction']])) { $a['transaction'] = new ConstStub(self::TRANSACTION_STATUS[$a['transaction']], $a['transaction']); } $a['pid'] = pg_get_pid($link); $a['last error'] = pg_last_error($link); $a['last notice'] = pg_last_notice($link); $a['host'] = pg_host($link); $a['port'] = pg_port($link); $a['dbname'] = pg_dbname($link); $a['options'] = pg_options($link); $a['version'] = pg_version($link); foreach (self::PARAM_CODES as $v) { if (false !== $s = pg_parameter_status($link, $v)) { $a['param'][$v] = $s; } } $a['param']['client_encoding'] = pg_client_encoding($link); $a['param'] = new EnumStub($a['param']); return $a; } public static function castResult($result, array $a, Stub $stub, bool $isNested) { $a['num rows'] = pg_num_rows($result); $a['status'] = pg_result_status($result); if (isset(self::RESULT_STATUS[$a['status']])) { $a['status'] = new ConstStub(self::RESULT_STATUS[$a['status']], $a['status']); } $a['command-completion tag'] = pg_result_status($result, \PGSQL_STATUS_STRING); if (-1 === $a['num rows']) { foreach (self::DIAG_CODES as $k => $v) { $a['error'][$k] = pg_result_error_field($result, $v); } } $a['affected rows'] = pg_affected_rows($result); $a['last OID'] = pg_last_oid($result); $fields = pg_num_fields($result); for ($i = 0; $i < $fields; ++$i) { $field = [ 'name' => pg_field_name($result, $i), 'table' => sprintf('%s (OID: %s)', pg_field_table($result, $i), pg_field_table($result, $i, true)), 'type' => sprintf('%s (OID: %s)', pg_field_type($result, $i), pg_field_type_oid($result, $i)), 'nullable' => (bool) pg_field_is_null($result, $i), 'storage' => pg_field_size($result, $i).' bytes', 'display' => pg_field_prtlen($result, $i).' chars', ]; if (' (OID: )' === $field['table']) { $field['table'] = null; } if ('-1 bytes' === $field['storage']) { $field['storage'] = 'variable size'; } elseif ('1 bytes' === $field['storage']) { $field['storage'] = '1 byte'; } if ('1 chars' === $field['display']) { $field['display'] = '1 char'; } $a['fields'][] = new EnumStub($field); } return $a; } } var-dumper/Caster/XmlResourceCaster.php 0000644 00000004761 15025017654 0014207 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts XML resources to array representation. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class XmlResourceCaster { private const XML_ERRORS = [ \XML_ERROR_NONE => 'XML_ERROR_NONE', \XML_ERROR_NO_MEMORY => 'XML_ERROR_NO_MEMORY', \XML_ERROR_SYNTAX => 'XML_ERROR_SYNTAX', \XML_ERROR_NO_ELEMENTS => 'XML_ERROR_NO_ELEMENTS', \XML_ERROR_INVALID_TOKEN => 'XML_ERROR_INVALID_TOKEN', \XML_ERROR_UNCLOSED_TOKEN => 'XML_ERROR_UNCLOSED_TOKEN', \XML_ERROR_PARTIAL_CHAR => 'XML_ERROR_PARTIAL_CHAR', \XML_ERROR_TAG_MISMATCH => 'XML_ERROR_TAG_MISMATCH', \XML_ERROR_DUPLICATE_ATTRIBUTE => 'XML_ERROR_DUPLICATE_ATTRIBUTE', \XML_ERROR_JUNK_AFTER_DOC_ELEMENT => 'XML_ERROR_JUNK_AFTER_DOC_ELEMENT', \XML_ERROR_PARAM_ENTITY_REF => 'XML_ERROR_PARAM_ENTITY_REF', \XML_ERROR_UNDEFINED_ENTITY => 'XML_ERROR_UNDEFINED_ENTITY', \XML_ERROR_RECURSIVE_ENTITY_REF => 'XML_ERROR_RECURSIVE_ENTITY_REF', \XML_ERROR_ASYNC_ENTITY => 'XML_ERROR_ASYNC_ENTITY', \XML_ERROR_BAD_CHAR_REF => 'XML_ERROR_BAD_CHAR_REF', \XML_ERROR_BINARY_ENTITY_REF => 'XML_ERROR_BINARY_ENTITY_REF', \XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF => 'XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF', \XML_ERROR_MISPLACED_XML_PI => 'XML_ERROR_MISPLACED_XML_PI', \XML_ERROR_UNKNOWN_ENCODING => 'XML_ERROR_UNKNOWN_ENCODING', \XML_ERROR_INCORRECT_ENCODING => 'XML_ERROR_INCORRECT_ENCODING', \XML_ERROR_UNCLOSED_CDATA_SECTION => 'XML_ERROR_UNCLOSED_CDATA_SECTION', \XML_ERROR_EXTERNAL_ENTITY_HANDLING => 'XML_ERROR_EXTERNAL_ENTITY_HANDLING', ]; public static function castXml($h, array $a, Stub $stub, bool $isNested) { $a['current_byte_index'] = xml_get_current_byte_index($h); $a['current_column_number'] = xml_get_current_column_number($h); $a['current_line_number'] = xml_get_current_line_number($h); $a['error_code'] = xml_get_error_code($h); if (isset(self::XML_ERRORS[$a['error_code']])) { $a['error_code'] = new ConstStub(self::XML_ERRORS[$a['error_code']], $a['error_code']); } return $a; } } var-dumper/Caster/FiberCaster.php 0000644 00000002046 15025017654 0012760 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts Fiber related classes to array representation. * * @author Grégoire Pineau <lyrixx@lyrixx.info> */ final class FiberCaster { public static function castFiber(\Fiber $fiber, array $a, Stub $stub, bool $isNested, int $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; if ($fiber->isTerminated()) { $status = 'terminated'; } elseif ($fiber->isRunning()) { $status = 'running'; } elseif ($fiber->isSuspended()) { $status = 'suspended'; } elseif ($fiber->isStarted()) { $status = 'started'; } else { $status = 'not started'; } $a[$prefix.'status'] = $status; return $a; } } var-dumper/Caster/EnumStub.php 0000644 00000001175 15025017654 0012333 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Represents an enumeration of values. * * @author Nicolas Grekas <p@tchwork.com> */ class EnumStub extends Stub { public $dumpKeys = true; public function __construct(array $values, bool $dumpKeys = true) { $this->value = $values; $this->dumpKeys = $dumpKeys; } } var-dumper/Caster/UuidCaster.php 0000644 00000001234 15025017654 0012635 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Ramsey\Uuid\UuidInterface; use Symfony\Component\VarDumper\Cloner\Stub; /** * @author Grégoire Pineau <lyrixx@lyrixx.info> */ final class UuidCaster { public static function castRamseyUuid(UuidInterface $c, array $a, Stub $stub, bool $isNested): array { $a += [ Caster::PREFIX_VIRTUAL.'uuid' => (string) $c, ]; return $a; } } var-dumper/Caster/ConstStub.php 0000644 00000001337 15025017654 0012515 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Represents a PHP constant and its value. * * @author Nicolas Grekas <p@tchwork.com> */ class ConstStub extends Stub { public function __construct(string $name, string|int|float $value = null) { $this->class = $name; $this->value = 1 < \func_num_args() ? $value : $name; } public function __toString(): string { return (string) $this->value; } } var-dumper/Caster/StubCaster.php 0000644 00000004134 15025017654 0012646 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts a caster's Stub. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class StubCaster { public static function castStub(Stub $c, array $a, Stub $stub, bool $isNested) { if ($isNested) { $stub->type = $c->type; $stub->class = $c->class; $stub->value = $c->value; $stub->handle = $c->handle; $stub->cut = $c->cut; $stub->attr = $c->attr; if (Stub::TYPE_REF === $c->type && !$c->class && \is_string($c->value) && !preg_match('//u', $c->value)) { $stub->type = Stub::TYPE_STRING; $stub->class = Stub::STRING_BINARY; } $a = []; } return $a; } public static function castCutArray(CutArrayStub $c, array $a, Stub $stub, bool $isNested) { return $isNested ? $c->preservedSubset : $a; } public static function cutInternals($obj, array $a, Stub $stub, bool $isNested) { if ($isNested) { $stub->cut += \count($a); return []; } return $a; } public static function castEnum(EnumStub $c, array $a, Stub $stub, bool $isNested) { if ($isNested) { $stub->class = $c->dumpKeys ? '' : null; $stub->handle = 0; $stub->value = null; $stub->cut = $c->cut; $stub->attr = $c->attr; $a = []; if ($c->value) { foreach (array_keys($c->value) as $k) { $keys[] = !isset($k[0]) || "\0" !== $k[0] ? Caster::PREFIX_VIRTUAL.$k : $k; } // Preserve references with array_combine() $a = array_combine($keys, $c->value); } } return $a; } } var-dumper/Caster/MysqliCaster.php 0000644 00000001263 15025017654 0013207 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * @author Nicolas Grekas <p@tchwork.com> * * @internal */ final class MysqliCaster { public static function castMysqliDriver(\mysqli_driver $c, array $a, Stub $stub, bool $isNested): array { foreach ($a as $k => $v) { if (isset($c->$k)) { $a[$k] = $c->$k; } } return $a; } } var-dumper/Caster/DateCaster.php 0000644 00000011377 15025017654 0012615 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts DateTimeInterface related classes to array representation. * * @author Dany Maillard <danymaillard93b@gmail.com> * * @final */ class DateCaster { private const PERIOD_LIMIT = 3; public static function castDateTime(\DateTimeInterface $d, array $a, Stub $stub, bool $isNested, int $filter) { $prefix = Caster::PREFIX_VIRTUAL; $location = $d->getTimezone()->getLocation(); $fromNow = (new \DateTime())->diff($d); $title = $d->format('l, F j, Y') ."\n".self::formatInterval($fromNow).' from now' .($location ? ($d->format('I') ? "\nDST On" : "\nDST Off") : '') ; unset( $a[Caster::PREFIX_DYNAMIC.'date'], $a[Caster::PREFIX_DYNAMIC.'timezone'], $a[Caster::PREFIX_DYNAMIC.'timezone_type'] ); $a[$prefix.'date'] = new ConstStub(self::formatDateTime($d, $location ? ' e (P)' : ' P'), $title); $stub->class .= $d->format(' @U'); return $a; } public static function castInterval(\DateInterval $interval, array $a, Stub $stub, bool $isNested, int $filter) { $now = new \DateTimeImmutable('@0', new \DateTimeZone('UTC')); $numberOfSeconds = $now->add($interval)->getTimestamp() - $now->getTimestamp(); $title = number_format($numberOfSeconds, 0, '.', ' ').'s'; $i = [Caster::PREFIX_VIRTUAL.'interval' => new ConstStub(self::formatInterval($interval), $title)]; return $filter & Caster::EXCLUDE_VERBOSE ? $i : $i + $a; } private static function formatInterval(\DateInterval $i): string { $format = '%R '; if (0 === $i->y && 0 === $i->m && ($i->h >= 24 || $i->i >= 60 || $i->s >= 60)) { $d = new \DateTimeImmutable('@0', new \DateTimeZone('UTC')); $i = $d->diff($d->add($i)); // recalculate carry over points $format .= 0 < $i->days ? '%ad ' : ''; } else { $format .= ($i->y ? '%yy ' : '').($i->m ? '%mm ' : '').($i->d ? '%dd ' : ''); } $format .= $i->h || $i->i || $i->s || $i->f ? '%H:%I:'.self::formatSeconds($i->s, substr($i->f, 2)) : ''; $format = '%R ' === $format ? '0s' : $format; return $i->format(rtrim($format)); } public static function castTimeZone(\DateTimeZone $timeZone, array $a, Stub $stub, bool $isNested, int $filter) { $location = $timeZone->getLocation(); $formatted = (new \DateTime('now', $timeZone))->format($location ? 'e (P)' : 'P'); $title = $location && \extension_loaded('intl') ? \Locale::getDisplayRegion('-'.$location['country_code']) : ''; $z = [Caster::PREFIX_VIRTUAL.'timezone' => new ConstStub($formatted, $title)]; return $filter & Caster::EXCLUDE_VERBOSE ? $z : $z + $a; } public static function castPeriod(\DatePeriod $p, array $a, Stub $stub, bool $isNested, int $filter) { $dates = []; foreach (clone $p as $i => $d) { if (self::PERIOD_LIMIT === $i) { $now = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); $dates[] = sprintf('%s more', ($end = $p->getEndDate()) ? ceil(($end->format('U.u') - $d->format('U.u')) / ((int) $now->add($p->getDateInterval())->format('U.u') - (int) $now->format('U.u'))) : $p->recurrences - $i ); break; } $dates[] = sprintf('%s) %s', $i + 1, self::formatDateTime($d)); } $period = sprintf( 'every %s, from %s%s %s', self::formatInterval($p->getDateInterval()), $p->include_start_date ? '[' : ']', self::formatDateTime($p->getStartDate()), ($end = $p->getEndDate()) ? 'to '.self::formatDateTime($end).(\PHP_VERSION_ID >= 80200 && $p->include_end_date ? ']' : '[') : 'recurring '.$p->recurrences.' time/s' ); $p = [Caster::PREFIX_VIRTUAL.'period' => new ConstStub($period, implode("\n", $dates))]; return $filter & Caster::EXCLUDE_VERBOSE ? $p : $p + $a; } private static function formatDateTime(\DateTimeInterface $d, string $extra = ''): string { return $d->format('Y-m-d H:i:'.self::formatSeconds($d->format('s'), $d->format('u')).$extra); } private static function formatSeconds(string $s, string $us): string { return sprintf('%02d.%s', $s, 0 === ($len = \strlen($t = rtrim($us, '0'))) ? '0' : ($len <= 3 ? str_pad($t, 3, '0') : $us)); } } var-dumper/Caster/DoctrineCaster.php 0000644 00000003210 15025017654 0013472 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Doctrine\Common\Proxy\Proxy as CommonProxy; use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Proxy\Proxy as OrmProxy; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts Doctrine related classes to array representation. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class DoctrineCaster { public static function castCommonProxy(CommonProxy $proxy, array $a, Stub $stub, bool $isNested) { foreach (['__cloner__', '__initializer__'] as $k) { if (\array_key_exists($k, $a)) { unset($a[$k]); ++$stub->cut; } } return $a; } public static function castOrmProxy(OrmProxy $proxy, array $a, Stub $stub, bool $isNested) { foreach (['_entityPersister', '_identifier'] as $k) { if (\array_key_exists($k = "\0Doctrine\\ORM\\Proxy\\Proxy\0".$k, $a)) { unset($a[$k]); ++$stub->cut; } } return $a; } public static function castPersistentCollection(PersistentCollection $coll, array $a, Stub $stub, bool $isNested) { foreach (['snapshot', 'association', 'typeClass'] as $k) { if (\array_key_exists($k = "\0Doctrine\\ORM\\PersistentCollection\0".$k, $a)) { $a[$k] = new CutStub($a[$k]); } } return $a; } } var-dumper/Caster/ArgsStub.php 0000644 00000004375 15025017654 0012330 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Represents a list of function arguments. * * @author Nicolas Grekas <p@tchwork.com> */ class ArgsStub extends EnumStub { private static array $parameters = []; public function __construct(array $args, string $function, ?string $class) { [$variadic, $params] = self::getParameters($function, $class); $values = []; foreach ($args as $k => $v) { $values[$k] = !\is_scalar($v) && !$v instanceof Stub ? new CutStub($v) : $v; } if (null === $params) { parent::__construct($values, false); return; } if (\count($values) < \count($params)) { $params = \array_slice($params, 0, \count($values)); } elseif (\count($values) > \count($params)) { $values[] = new EnumStub(array_splice($values, \count($params)), false); $params[] = $variadic; } if (['...'] === $params) { $this->dumpKeys = false; $this->value = $values[0]->value; } else { $this->value = array_combine($params, $values); } } private static function getParameters(string $function, ?string $class): array { if (isset(self::$parameters[$k = $class.'::'.$function])) { return self::$parameters[$k]; } try { $r = null !== $class ? new \ReflectionMethod($class, $function) : new \ReflectionFunction($function); } catch (\ReflectionException $e) { return [null, null]; } $variadic = '...'; $params = []; foreach ($r->getParameters() as $v) { $k = '$'.$v->name; if ($v->isPassedByReference()) { $k = '&'.$k; } if ($v->isVariadic()) { $variadic .= $k; } else { $params[] = $k; } } return self::$parameters[$k] = [$variadic, $params]; } } var-dumper/Caster/FrameStub.php 0000644 00000001344 15025017654 0012457 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; /** * Represents a single backtrace frame as returned by debug_backtrace() or Exception->getTrace(). * * @author Nicolas Grekas <p@tchwork.com> */ class FrameStub extends EnumStub { public $keepArgs; public $inTraceStub; public function __construct(array $frame, bool $keepArgs = true, bool $inTraceStub = false) { $this->value = $frame; $this->keepArgs = $keepArgs; $this->inTraceStub = $inTraceStub; } } var-dumper/Caster/ImagineCaster.php 0000644 00000001665 15025017654 0013310 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Imagine\Image\ImageInterface; use Symfony\Component\VarDumper\Cloner\Stub; /** * @author Grégoire Pineau <lyrixx@lyrixx.info> */ final class ImagineCaster { public static function castImage(ImageInterface $c, array $a, Stub $stub, bool $isNested): array { $imgData = $c->get('png'); if (\strlen($imgData) > 1 * 1000 * 1000) { $a += [ Caster::PREFIX_VIRTUAL.'image' => new ConstStub($c->getSize()), ]; } else { $a += [ Caster::PREFIX_VIRTUAL.'image' => new ImgStub($imgData, 'image/png', $c->getSize()), ]; } return $a; } } var-dumper/Caster/XmlReaderCaster.php 0000644 00000006351 15025017654 0013617 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts XmlReader class to array representation. * * @author Baptiste Clavié <clavie.b@gmail.com> * * @final */ class XmlReaderCaster { private const NODE_TYPES = [ \XMLReader::NONE => 'NONE', \XMLReader::ELEMENT => 'ELEMENT', \XMLReader::ATTRIBUTE => 'ATTRIBUTE', \XMLReader::TEXT => 'TEXT', \XMLReader::CDATA => 'CDATA', \XMLReader::ENTITY_REF => 'ENTITY_REF', \XMLReader::ENTITY => 'ENTITY', \XMLReader::PI => 'PI (Processing Instruction)', \XMLReader::COMMENT => 'COMMENT', \XMLReader::DOC => 'DOC', \XMLReader::DOC_TYPE => 'DOC_TYPE', \XMLReader::DOC_FRAGMENT => 'DOC_FRAGMENT', \XMLReader::NOTATION => 'NOTATION', \XMLReader::WHITESPACE => 'WHITESPACE', \XMLReader::SIGNIFICANT_WHITESPACE => 'SIGNIFICANT_WHITESPACE', \XMLReader::END_ELEMENT => 'END_ELEMENT', \XMLReader::END_ENTITY => 'END_ENTITY', \XMLReader::XML_DECLARATION => 'XML_DECLARATION', ]; public static function castXmlReader(\XMLReader $reader, array $a, Stub $stub, bool $isNested) { try { $properties = [ 'LOADDTD' => @$reader->getParserProperty(\XMLReader::LOADDTD), 'DEFAULTATTRS' => @$reader->getParserProperty(\XMLReader::DEFAULTATTRS), 'VALIDATE' => @$reader->getParserProperty(\XMLReader::VALIDATE), 'SUBST_ENTITIES' => @$reader->getParserProperty(\XMLReader::SUBST_ENTITIES), ]; } catch (\Error $e) { $properties = [ 'LOADDTD' => false, 'DEFAULTATTRS' => false, 'VALIDATE' => false, 'SUBST_ENTITIES' => false, ]; } $props = Caster::PREFIX_VIRTUAL.'parserProperties'; $info = [ 'localName' => $reader->localName, 'prefix' => $reader->prefix, 'nodeType' => new ConstStub(self::NODE_TYPES[$reader->nodeType], $reader->nodeType), 'depth' => $reader->depth, 'isDefault' => $reader->isDefault, 'isEmptyElement' => \XMLReader::NONE === $reader->nodeType ? null : $reader->isEmptyElement, 'xmlLang' => $reader->xmlLang, 'attributeCount' => $reader->attributeCount, 'value' => $reader->value, 'namespaceURI' => $reader->namespaceURI, 'baseURI' => $reader->baseURI ? new LinkStub($reader->baseURI) : $reader->baseURI, $props => $properties, ]; if ($info[$props] = Caster::filter($info[$props], Caster::EXCLUDE_EMPTY, [], $count)) { $info[$props] = new EnumStub($info[$props]); $info[$props]->cut = $count; } $info = Caster::filter($info, Caster::EXCLUDE_EMPTY, [], $count); // +2 because hasValue and hasAttributes are always filtered $stub->cut += $count + 2; return $a + $info; } } var-dumper/Caster/ProxyManagerCaster.php 0000644 00000001321 15025017654 0014340 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use ProxyManager\Proxy\ProxyInterface; use Symfony\Component\VarDumper\Cloner\Stub; /** * @author Nicolas Grekas <p@tchwork.com> * * @final */ class ProxyManagerCaster { public static function castProxy(ProxyInterface $c, array $a, Stub $stub, bool $isNested) { if ($parent = get_parent_class($c)) { $stub->class .= ' - '.$parent; } $stub->class .= '@proxy'; return $a; } } var-dumper/Caster/ResourceCaster.php 0000644 00000006207 15025017654 0013523 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts common resource types to array representation. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class ResourceCaster { public static function castCurl(\CurlHandle $h, array $a, Stub $stub, bool $isNested): array { return curl_getinfo($h); } public static function castDba($dba, array $a, Stub $stub, bool $isNested) { $list = dba_list(); $a['file'] = $list[(int) $dba]; return $a; } public static function castProcess($process, array $a, Stub $stub, bool $isNested) { return proc_get_status($process); } public static function castStream($stream, array $a, Stub $stub, bool $isNested) { $a = stream_get_meta_data($stream) + static::castStreamContext($stream, $a, $stub, $isNested); if ($a['uri'] ?? false) { $a['uri'] = new LinkStub($a['uri']); } return $a; } public static function castStreamContext($stream, array $a, Stub $stub, bool $isNested) { return @stream_context_get_params($stream) ?: $a; } public static function castGd($gd, array $a, Stub $stub, bool $isNested) { $a['size'] = imagesx($gd).'x'.imagesy($gd); $a['trueColor'] = imageistruecolor($gd); return $a; } public static function castMysqlLink($h, array $a, Stub $stub, bool $isNested) { $a['host'] = mysql_get_host_info($h); $a['protocol'] = mysql_get_proto_info($h); $a['server'] = mysql_get_server_info($h); return $a; } public static function castOpensslX509($h, array $a, Stub $stub, bool $isNested) { $stub->cut = -1; $info = openssl_x509_parse($h, false); $pin = openssl_pkey_get_public($h); $pin = openssl_pkey_get_details($pin)['key']; $pin = \array_slice(explode("\n", $pin), 1, -2); $pin = base64_decode(implode('', $pin)); $pin = base64_encode(hash('sha256', $pin, true)); $a += [ 'subject' => new EnumStub(array_intersect_key($info['subject'], ['organizationName' => true, 'commonName' => true])), 'issuer' => new EnumStub(array_intersect_key($info['issuer'], ['organizationName' => true, 'commonName' => true])), 'expiry' => new ConstStub(date(\DateTime::ISO8601, $info['validTo_time_t']), $info['validTo_time_t']), 'fingerprint' => new EnumStub([ 'md5' => new ConstStub(wordwrap(strtoupper(openssl_x509_fingerprint($h, 'md5')), 2, ':', true)), 'sha1' => new ConstStub(wordwrap(strtoupper(openssl_x509_fingerprint($h, 'sha1')), 2, ':', true)), 'sha256' => new ConstStub(wordwrap(strtoupper(openssl_x509_fingerprint($h, 'sha256')), 2, ':', true)), 'pin-sha256' => new ConstStub($pin), ]), ]; return $a; } } var-dumper/Caster/CutArrayStub.php 0000644 00000001270 15025017654 0013155 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; /** * Represents a cut array. * * @author Nicolas Grekas <p@tchwork.com> */ class CutArrayStub extends CutStub { public $preservedSubset; public function __construct(array $value, array $preservedKeys) { parent::__construct($value); $this->preservedSubset = array_intersect_key($value, array_flip($preservedKeys)); $this->cut -= \count($this->preservedSubset); } } var-dumper/Caster/TraceStub.php 0000644 00000001703 15025017654 0012462 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Represents a backtrace as returned by debug_backtrace() or Exception->getTrace(). * * @author Nicolas Grekas <p@tchwork.com> */ class TraceStub extends Stub { public $keepArgs; public $sliceOffset; public $sliceLength; public $numberingOffset; public function __construct(array $trace, bool $keepArgs = true, int $sliceOffset = 0, int $sliceLength = null, int $numberingOffset = 0) { $this->value = $trace; $this->keepArgs = $keepArgs; $this->sliceOffset = $sliceOffset; $this->sliceLength = $sliceLength; $this->numberingOffset = $numberingOffset; } } var-dumper/Caster/GmpCaster.php 0000644 00000001355 15025017654 0012456 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts GMP objects to array representation. * * @author Hamza Amrouche <hamza.simperfit@gmail.com> * @author Nicolas Grekas <p@tchwork.com> * * @final */ class GmpCaster { public static function castGmp(\GMP $gmp, array $a, Stub $stub, bool $isNested, int $filter): array { $a[Caster::PREFIX_VIRTUAL.'value'] = new ConstStub(gmp_strval($gmp), gmp_strval($gmp)); return $a; } } var-dumper/Caster/ExceptionCaster.php 0000644 00000037616 15025017654 0013702 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\VarDumper\Cloner\Stub; use Symfony\Component\VarDumper\Exception\ThrowingCasterException; /** * Casts common Exception classes to array representation. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class ExceptionCaster { public static int $srcContext = 1; public static bool $traceArgs = true; public static array $errorTypes = [ \E_DEPRECATED => 'E_DEPRECATED', \E_USER_DEPRECATED => 'E_USER_DEPRECATED', \E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', \E_ERROR => 'E_ERROR', \E_WARNING => 'E_WARNING', \E_PARSE => 'E_PARSE', \E_NOTICE => 'E_NOTICE', \E_CORE_ERROR => 'E_CORE_ERROR', \E_CORE_WARNING => 'E_CORE_WARNING', \E_COMPILE_ERROR => 'E_COMPILE_ERROR', \E_COMPILE_WARNING => 'E_COMPILE_WARNING', \E_USER_ERROR => 'E_USER_ERROR', \E_USER_WARNING => 'E_USER_WARNING', \E_USER_NOTICE => 'E_USER_NOTICE', \E_STRICT => 'E_STRICT', ]; private static array $framesCache = []; public static function castError(\Error $e, array $a, Stub $stub, bool $isNested, int $filter = 0) { return self::filterExceptionArray($stub->class, $a, "\0Error\0", $filter); } public static function castException(\Exception $e, array $a, Stub $stub, bool $isNested, int $filter = 0) { return self::filterExceptionArray($stub->class, $a, "\0Exception\0", $filter); } public static function castErrorException(\ErrorException $e, array $a, Stub $stub, bool $isNested) { if (isset($a[$s = Caster::PREFIX_PROTECTED.'severity'], self::$errorTypes[$a[$s]])) { $a[$s] = new ConstStub(self::$errorTypes[$a[$s]], $a[$s]); } return $a; } public static function castThrowingCasterException(ThrowingCasterException $e, array $a, Stub $stub, bool $isNested) { $trace = Caster::PREFIX_VIRTUAL.'trace'; $prefix = Caster::PREFIX_PROTECTED; $xPrefix = "\0Exception\0"; if (isset($a[$xPrefix.'previous'], $a[$trace]) && $a[$xPrefix.'previous'] instanceof \Exception) { $b = (array) $a[$xPrefix.'previous']; $class = get_debug_type($a[$xPrefix.'previous']); self::traceUnshift($b[$xPrefix.'trace'], $class, $b[$prefix.'file'], $b[$prefix.'line']); $a[$trace] = new TraceStub($b[$xPrefix.'trace'], false, 0, -\count($a[$trace]->value)); } unset($a[$xPrefix.'previous'], $a[$prefix.'code'], $a[$prefix.'file'], $a[$prefix.'line']); return $a; } public static function castSilencedErrorContext(SilencedErrorContext $e, array $a, Stub $stub, bool $isNested) { $sPrefix = "\0".SilencedErrorContext::class."\0"; if (!isset($a[$s = $sPrefix.'severity'])) { return $a; } if (isset(self::$errorTypes[$a[$s]])) { $a[$s] = new ConstStub(self::$errorTypes[$a[$s]], $a[$s]); } $trace = [[ 'file' => $a[$sPrefix.'file'], 'line' => $a[$sPrefix.'line'], ]]; if (isset($a[$sPrefix.'trace'])) { $trace = array_merge($trace, $a[$sPrefix.'trace']); } unset($a[$sPrefix.'file'], $a[$sPrefix.'line'], $a[$sPrefix.'trace']); $a[Caster::PREFIX_VIRTUAL.'trace'] = new TraceStub($trace, self::$traceArgs); return $a; } public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, bool $isNested) { if (!$isNested) { return $a; } $stub->class = ''; $stub->handle = 0; $frames = $trace->value; $prefix = Caster::PREFIX_VIRTUAL; $a = []; $j = \count($frames); if (0 > $i = $trace->sliceOffset) { $i = max(0, $j + $i); } if (!isset($trace->value[$i])) { return []; } $lastCall = isset($frames[$i]['function']) ? (isset($frames[$i]['class']) ? $frames[0]['class'].$frames[$i]['type'] : '').$frames[$i]['function'].'()' : ''; $frames[] = ['function' => '']; $collapse = false; for ($j += $trace->numberingOffset - $i++; isset($frames[$i]); ++$i, --$j) { $f = $frames[$i]; $call = isset($f['function']) ? (isset($f['class']) ? $f['class'].$f['type'] : '').$f['function'] : '???'; $frame = new FrameStub( [ 'object' => $f['object'] ?? null, 'class' => $f['class'] ?? null, 'type' => $f['type'] ?? null, 'function' => $f['function'] ?? null, ] + $frames[$i - 1], false, true ); $f = self::castFrameStub($frame, [], $frame, true); if (isset($f[$prefix.'src'])) { foreach ($f[$prefix.'src']->value as $label => $frame) { if (str_starts_with($label, "\0~collapse=0")) { if ($collapse) { $label = substr_replace($label, '1', 11, 1); } else { $collapse = true; } } $label = substr_replace($label, "title=Stack level $j.&", 2, 0); } $f = $frames[$i - 1]; if ($trace->keepArgs && !empty($f['args']) && $frame instanceof EnumStub) { $frame->value['arguments'] = new ArgsStub($f['args'], $f['function'] ?? null, $f['class'] ?? null); } } elseif ('???' !== $lastCall) { $label = new ClassStub($lastCall); if (isset($label->attr['ellipsis'])) { $label->attr['ellipsis'] += 2; $label = substr_replace($prefix, "ellipsis-type=class&ellipsis={$label->attr['ellipsis']}&ellipsis-tail=1&title=Stack level $j.", 2, 0).$label->value.'()'; } else { $label = substr_replace($prefix, "title=Stack level $j.", 2, 0).$label->value.'()'; } } else { $label = substr_replace($prefix, "title=Stack level $j.", 2, 0).$lastCall; } $a[substr_replace($label, sprintf('separator=%s&', $frame instanceof EnumStub ? ' ' : ':'), 2, 0)] = $frame; $lastCall = $call; } if (null !== $trace->sliceLength) { $a = \array_slice($a, 0, $trace->sliceLength, true); } return $a; } public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, bool $isNested) { if (!$isNested) { return $a; } $f = $frame->value; $prefix = Caster::PREFIX_VIRTUAL; if (isset($f['file'], $f['line'])) { $cacheKey = $f; unset($cacheKey['object'], $cacheKey['args']); $cacheKey[] = self::$srcContext; $cacheKey = implode('-', $cacheKey); if (isset(self::$framesCache[$cacheKey])) { $a[$prefix.'src'] = self::$framesCache[$cacheKey]; } else { if (preg_match('/\((\d+)\)(?:\([\da-f]{32}\))? : (?:eval\(\)\'d code|runtime-created function)$/', $f['file'], $match)) { $f['file'] = substr($f['file'], 0, -\strlen($match[0])); $f['line'] = (int) $match[1]; } $src = $f['line']; $srcKey = $f['file']; $ellipsis = new LinkStub($srcKey, 0); $srcAttr = 'collapse='.(int) $ellipsis->inVendor; $ellipsisTail = $ellipsis->attr['ellipsis-tail'] ?? 0; $ellipsis = $ellipsis->attr['ellipsis'] ?? 0; if (is_file($f['file']) && 0 <= self::$srcContext) { if (!empty($f['class']) && (is_subclass_of($f['class'], 'Twig\Template') || is_subclass_of($f['class'], 'Twig_Template')) && method_exists($f['class'], 'getDebugInfo')) { $template = null; if (isset($f['object'])) { $template = $f['object']; } elseif ((new \ReflectionClass($f['class']))->isInstantiable()) { $template = unserialize(sprintf('O:%d:"%s":0:{}', \strlen($f['class']), $f['class'])); } if (null !== $template) { $ellipsis = 0; $templateSrc = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getCode() : (method_exists($template, 'getSource') ? $template->getSource() : ''); $templateInfo = $template->getDebugInfo(); if (isset($templateInfo[$f['line']])) { if (!method_exists($template, 'getSourceContext') || !is_file($templatePath = $template->getSourceContext()->getPath())) { $templatePath = null; } if ($templateSrc) { $src = self::extractSource($templateSrc, $templateInfo[$f['line']], self::$srcContext, 'twig', $templatePath, $f); $srcKey = ($templatePath ?: $template->getTemplateName()).':'.$templateInfo[$f['line']]; } } } } if ($srcKey == $f['file']) { $src = self::extractSource(file_get_contents($f['file']), $f['line'], self::$srcContext, 'php', $f['file'], $f); $srcKey .= ':'.$f['line']; if ($ellipsis) { $ellipsis += 1 + \strlen($f['line']); } } $srcAttr .= sprintf('&separator= &file=%s&line=%d', rawurlencode($f['file']), $f['line']); } else { $srcAttr .= '&separator=:'; } $srcAttr .= $ellipsis ? '&ellipsis-type=path&ellipsis='.$ellipsis.'&ellipsis-tail='.$ellipsisTail : ''; self::$framesCache[$cacheKey] = $a[$prefix.'src'] = new EnumStub(["\0~$srcAttr\0$srcKey" => $src]); } } unset($a[$prefix.'args'], $a[$prefix.'line'], $a[$prefix.'file']); if ($frame->inTraceStub) { unset($a[$prefix.'class'], $a[$prefix.'type'], $a[$prefix.'function']); } foreach ($a as $k => $v) { if (!$v) { unset($a[$k]); } } if ($frame->keepArgs && !empty($f['args'])) { $a[$prefix.'arguments'] = new ArgsStub($f['args'], $f['function'], $f['class']); } return $a; } private static function filterExceptionArray(string $xClass, array $a, string $xPrefix, int $filter): array { if (isset($a[$xPrefix.'trace'])) { $trace = $a[$xPrefix.'trace']; unset($a[$xPrefix.'trace']); // Ensures the trace is always last } else { $trace = []; } if (!($filter & Caster::EXCLUDE_VERBOSE) && $trace) { if (isset($a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line'])) { self::traceUnshift($trace, $xClass, $a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line']); } $a[Caster::PREFIX_VIRTUAL.'trace'] = new TraceStub($trace, self::$traceArgs); } if (empty($a[$xPrefix.'previous'])) { unset($a[$xPrefix.'previous']); } unset($a[$xPrefix.'string'], $a[Caster::PREFIX_DYNAMIC.'xdebug_message'], $a[Caster::PREFIX_DYNAMIC.'__destructorException']); if (isset($a[Caster::PREFIX_PROTECTED.'message']) && str_contains($a[Caster::PREFIX_PROTECTED.'message'], "@anonymous\0")) { $a[Caster::PREFIX_PROTECTED.'message'] = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) { return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0]; }, $a[Caster::PREFIX_PROTECTED.'message']); } if (isset($a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line'])) { $a[Caster::PREFIX_PROTECTED.'file'] = new LinkStub($a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line']); } return $a; } private static function traceUnshift(array &$trace, ?string $class, string $file, int $line): void { if (isset($trace[0]['file'], $trace[0]['line']) && $trace[0]['file'] === $file && $trace[0]['line'] === $line) { return; } array_unshift($trace, [ 'function' => $class ? 'new '.$class : null, 'file' => $file, 'line' => $line, ]); } private static function extractSource(string $srcLines, int $line, int $srcContext, string $lang, ?string $file, array $frame): EnumStub { $srcLines = explode("\n", $srcLines); $src = []; for ($i = $line - 1 - $srcContext; $i <= $line - 1 + $srcContext; ++$i) { $src[] = ($srcLines[$i] ?? '')."\n"; } if ($frame['function'] ?? false) { $stub = new CutStub(new \stdClass()); $stub->class = (isset($frame['class']) ? $frame['class'].$frame['type'] : '').$frame['function']; $stub->type = Stub::TYPE_OBJECT; $stub->attr['cut_hash'] = true; $stub->attr['file'] = $frame['file']; $stub->attr['line'] = $frame['line']; try { $caller = isset($frame['class']) ? new \ReflectionMethod($frame['class'], $frame['function']) : new \ReflectionFunction($frame['function']); $stub->class .= ReflectionCaster::getSignature(ReflectionCaster::castFunctionAbstract($caller, [], $stub, true, Caster::EXCLUDE_VERBOSE)); if ($f = $caller->getFileName()) { $stub->attr['file'] = $f; $stub->attr['line'] = $caller->getStartLine(); } } catch (\ReflectionException $e) { // ignore fake class/function } $srcLines = ["\0~separator=\0" => $stub]; } else { $stub = null; $srcLines = []; } $ltrim = 0; do { $pad = null; for ($i = $srcContext << 1; $i >= 0; --$i) { if (isset($src[$i][$ltrim]) && "\r" !== ($c = $src[$i][$ltrim]) && "\n" !== $c) { if (null === $pad) { $pad = $c; } if ((' ' !== $c && "\t" !== $c) || $pad !== $c) { break; } } } ++$ltrim; } while (0 > $i && null !== $pad); --$ltrim; foreach ($src as $i => $c) { if ($ltrim) { $c = isset($c[$ltrim]) && "\r" !== $c[$ltrim] ? substr($c, $ltrim) : ltrim($c, " \t"); } $c = substr($c, 0, -1); if ($i !== $srcContext) { $c = new ConstStub('default', $c); } else { $c = new ConstStub($c, $stub ? 'in '.$stub->class : ''); if (null !== $file) { $c->attr['file'] = $file; $c->attr['line'] = $line; } } $c->attr['lang'] = $lang; $srcLines[sprintf("\0~separator=› &%d\0", $i + $line - $srcContext)] = $c; } return new EnumStub($srcLines); } } var-dumper/Caster/CutStub.php 0000644 00000003617 15025017654 0012165 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Represents the main properties of a PHP variable, pre-casted by a caster. * * @author Nicolas Grekas <p@tchwork.com> */ class CutStub extends Stub { public function __construct(mixed $value) { $this->value = $value; switch (\gettype($value)) { case 'object': $this->type = self::TYPE_OBJECT; $this->class = \get_class($value); if ($value instanceof \Closure) { ReflectionCaster::castClosure($value, [], $this, true, Caster::EXCLUDE_VERBOSE); } $this->cut = -1; break; case 'array': $this->type = self::TYPE_ARRAY; $this->class = self::ARRAY_ASSOC; $this->cut = $this->value = \count($value); break; case 'resource': case 'unknown type': case 'resource (closed)': $this->type = self::TYPE_RESOURCE; $this->handle = (int) $value; if ('Unknown' === $this->class = @get_resource_type($value)) { $this->class = 'Closed'; } $this->cut = -1; break; case 'string': $this->type = self::TYPE_STRING; $this->class = preg_match('//u', $value) ? self::STRING_UTF8 : self::STRING_BINARY; $this->cut = self::STRING_BINARY === $this->class ? \strlen($value) : mb_strlen($value, 'UTF-8'); $this->value = ''; break; } } } var-dumper/Caster/RdKafkaCaster.php 0000644 00000011053 15025017654 0013232 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use RdKafka\Conf; use RdKafka\Exception as RdKafkaException; use RdKafka\KafkaConsumer; use RdKafka\Message; use RdKafka\Metadata\Broker as BrokerMetadata; use RdKafka\Metadata\Collection as CollectionMetadata; use RdKafka\Metadata\Partition as PartitionMetadata; use RdKafka\Metadata\Topic as TopicMetadata; use RdKafka\Topic; use RdKafka\TopicConf; use RdKafka\TopicPartition; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts RdKafka related classes to array representation. * * @author Romain Neutron <imprec@gmail.com> */ class RdKafkaCaster { public static function castKafkaConsumer(KafkaConsumer $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; try { $assignment = $c->getAssignment(); } catch (RdKafkaException $e) { $assignment = []; } $a += [ $prefix.'subscription' => $c->getSubscription(), $prefix.'assignment' => $assignment, ]; $a += self::extractMetadata($c); return $a; } public static function castTopic(Topic $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'name' => $c->getName(), ]; return $a; } public static function castTopicPartition(TopicPartition $c, array $a) { $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'offset' => $c->getOffset(), $prefix.'partition' => $c->getPartition(), $prefix.'topic' => $c->getTopic(), ]; return $a; } public static function castMessage(Message $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'errstr' => $c->errstr(), ]; return $a; } public static function castConf(Conf $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; foreach ($c->dump() as $key => $value) { $a[$prefix.$key] = $value; } return $a; } public static function castTopicConf(TopicConf $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; foreach ($c->dump() as $key => $value) { $a[$prefix.$key] = $value; } return $a; } public static function castRdKafka(\RdKafka $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'out_q_len' => $c->getOutQLen(), ]; $a += self::extractMetadata($c); return $a; } public static function castCollectionMetadata(CollectionMetadata $c, array $a, Stub $stub, bool $isNested) { $a += iterator_to_array($c); return $a; } public static function castTopicMetadata(TopicMetadata $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'name' => $c->getTopic(), $prefix.'partitions' => $c->getPartitions(), ]; return $a; } public static function castPartitionMetadata(PartitionMetadata $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'id' => $c->getId(), $prefix.'err' => $c->getErr(), $prefix.'leader' => $c->getLeader(), ]; return $a; } public static function castBrokerMetadata(BrokerMetadata $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'id' => $c->getId(), $prefix.'host' => $c->getHost(), $prefix.'port' => $c->getPort(), ]; return $a; } private static function extractMetadata(KafkaConsumer|\RdKafka $c) { $prefix = Caster::PREFIX_VIRTUAL; try { $m = $c->getMetadata(true, null, 500); } catch (RdKafkaException $e) { return []; } return [ $prefix.'orig_broker_id' => $m->getOrigBrokerId(), $prefix.'orig_broker_name' => $m->getOrigBrokerName(), $prefix.'brokers' => $m->getBrokers(), $prefix.'topics' => $m->getTopics(), ]; } } var-dumper/Caster/IntlCaster.php 0000644 00000021344 15025017654 0012641 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * @author Nicolas Grekas <p@tchwork.com> * @author Jan Schädlich <jan.schaedlich@sensiolabs.de> * * @final */ class IntlCaster { public static function castMessageFormatter(\MessageFormatter $c, array $a, Stub $stub, bool $isNested) { $a += [ Caster::PREFIX_VIRTUAL.'locale' => $c->getLocale(), Caster::PREFIX_VIRTUAL.'pattern' => $c->getPattern(), ]; return self::castError($c, $a); } public static function castNumberFormatter(\NumberFormatter $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $a += [ Caster::PREFIX_VIRTUAL.'locale' => $c->getLocale(), Caster::PREFIX_VIRTUAL.'pattern' => $c->getPattern(), ]; if ($filter & Caster::EXCLUDE_VERBOSE) { $stub->cut += 3; return self::castError($c, $a); } $a += [ Caster::PREFIX_VIRTUAL.'attributes' => new EnumStub( [ 'PARSE_INT_ONLY' => $c->getAttribute(\NumberFormatter::PARSE_INT_ONLY), 'GROUPING_USED' => $c->getAttribute(\NumberFormatter::GROUPING_USED), 'DECIMAL_ALWAYS_SHOWN' => $c->getAttribute(\NumberFormatter::DECIMAL_ALWAYS_SHOWN), 'MAX_INTEGER_DIGITS' => $c->getAttribute(\NumberFormatter::MAX_INTEGER_DIGITS), 'MIN_INTEGER_DIGITS' => $c->getAttribute(\NumberFormatter::MIN_INTEGER_DIGITS), 'INTEGER_DIGITS' => $c->getAttribute(\NumberFormatter::INTEGER_DIGITS), 'MAX_FRACTION_DIGITS' => $c->getAttribute(\NumberFormatter::MAX_FRACTION_DIGITS), 'MIN_FRACTION_DIGITS' => $c->getAttribute(\NumberFormatter::MIN_FRACTION_DIGITS), 'FRACTION_DIGITS' => $c->getAttribute(\NumberFormatter::FRACTION_DIGITS), 'MULTIPLIER' => $c->getAttribute(\NumberFormatter::MULTIPLIER), 'GROUPING_SIZE' => $c->getAttribute(\NumberFormatter::GROUPING_SIZE), 'ROUNDING_MODE' => $c->getAttribute(\NumberFormatter::ROUNDING_MODE), 'ROUNDING_INCREMENT' => $c->getAttribute(\NumberFormatter::ROUNDING_INCREMENT), 'FORMAT_WIDTH' => $c->getAttribute(\NumberFormatter::FORMAT_WIDTH), 'PADDING_POSITION' => $c->getAttribute(\NumberFormatter::PADDING_POSITION), 'SECONDARY_GROUPING_SIZE' => $c->getAttribute(\NumberFormatter::SECONDARY_GROUPING_SIZE), 'SIGNIFICANT_DIGITS_USED' => $c->getAttribute(\NumberFormatter::SIGNIFICANT_DIGITS_USED), 'MIN_SIGNIFICANT_DIGITS' => $c->getAttribute(\NumberFormatter::MIN_SIGNIFICANT_DIGITS), 'MAX_SIGNIFICANT_DIGITS' => $c->getAttribute(\NumberFormatter::MAX_SIGNIFICANT_DIGITS), 'LENIENT_PARSE' => $c->getAttribute(\NumberFormatter::LENIENT_PARSE), ] ), Caster::PREFIX_VIRTUAL.'text_attributes' => new EnumStub( [ 'POSITIVE_PREFIX' => $c->getTextAttribute(\NumberFormatter::POSITIVE_PREFIX), 'POSITIVE_SUFFIX' => $c->getTextAttribute(\NumberFormatter::POSITIVE_SUFFIX), 'NEGATIVE_PREFIX' => $c->getTextAttribute(\NumberFormatter::NEGATIVE_PREFIX), 'NEGATIVE_SUFFIX' => $c->getTextAttribute(\NumberFormatter::NEGATIVE_SUFFIX), 'PADDING_CHARACTER' => $c->getTextAttribute(\NumberFormatter::PADDING_CHARACTER), 'CURRENCY_CODE' => $c->getTextAttribute(\NumberFormatter::CURRENCY_CODE), 'DEFAULT_RULESET' => $c->getTextAttribute(\NumberFormatter::DEFAULT_RULESET), 'PUBLIC_RULESETS' => $c->getTextAttribute(\NumberFormatter::PUBLIC_RULESETS), ] ), Caster::PREFIX_VIRTUAL.'symbols' => new EnumStub( [ 'DECIMAL_SEPARATOR_SYMBOL' => $c->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL), 'GROUPING_SEPARATOR_SYMBOL' => $c->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL), 'PATTERN_SEPARATOR_SYMBOL' => $c->getSymbol(\NumberFormatter::PATTERN_SEPARATOR_SYMBOL), 'PERCENT_SYMBOL' => $c->getSymbol(\NumberFormatter::PERCENT_SYMBOL), 'ZERO_DIGIT_SYMBOL' => $c->getSymbol(\NumberFormatter::ZERO_DIGIT_SYMBOL), 'DIGIT_SYMBOL' => $c->getSymbol(\NumberFormatter::DIGIT_SYMBOL), 'MINUS_SIGN_SYMBOL' => $c->getSymbol(\NumberFormatter::MINUS_SIGN_SYMBOL), 'PLUS_SIGN_SYMBOL' => $c->getSymbol(\NumberFormatter::PLUS_SIGN_SYMBOL), 'CURRENCY_SYMBOL' => $c->getSymbol(\NumberFormatter::CURRENCY_SYMBOL), 'INTL_CURRENCY_SYMBOL' => $c->getSymbol(\NumberFormatter::INTL_CURRENCY_SYMBOL), 'MONETARY_SEPARATOR_SYMBOL' => $c->getSymbol(\NumberFormatter::MONETARY_SEPARATOR_SYMBOL), 'EXPONENTIAL_SYMBOL' => $c->getSymbol(\NumberFormatter::EXPONENTIAL_SYMBOL), 'PERMILL_SYMBOL' => $c->getSymbol(\NumberFormatter::PERMILL_SYMBOL), 'PAD_ESCAPE_SYMBOL' => $c->getSymbol(\NumberFormatter::PAD_ESCAPE_SYMBOL), 'INFINITY_SYMBOL' => $c->getSymbol(\NumberFormatter::INFINITY_SYMBOL), 'NAN_SYMBOL' => $c->getSymbol(\NumberFormatter::NAN_SYMBOL), 'SIGNIFICANT_DIGIT_SYMBOL' => $c->getSymbol(\NumberFormatter::SIGNIFICANT_DIGIT_SYMBOL), 'MONETARY_GROUPING_SEPARATOR_SYMBOL' => $c->getSymbol(\NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL), ] ), ]; return self::castError($c, $a); } public static function castIntlTimeZone(\IntlTimeZone $c, array $a, Stub $stub, bool $isNested) { $a += [ Caster::PREFIX_VIRTUAL.'display_name' => $c->getDisplayName(), Caster::PREFIX_VIRTUAL.'id' => $c->getID(), Caster::PREFIX_VIRTUAL.'raw_offset' => $c->getRawOffset(), ]; if ($c->useDaylightTime()) { $a += [ Caster::PREFIX_VIRTUAL.'dst_savings' => $c->getDSTSavings(), ]; } return self::castError($c, $a); } public static function castIntlCalendar(\IntlCalendar $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $a += [ Caster::PREFIX_VIRTUAL.'type' => $c->getType(), Caster::PREFIX_VIRTUAL.'first_day_of_week' => $c->getFirstDayOfWeek(), Caster::PREFIX_VIRTUAL.'minimal_days_in_first_week' => $c->getMinimalDaysInFirstWeek(), Caster::PREFIX_VIRTUAL.'repeated_wall_time_option' => $c->getRepeatedWallTimeOption(), Caster::PREFIX_VIRTUAL.'skipped_wall_time_option' => $c->getSkippedWallTimeOption(), Caster::PREFIX_VIRTUAL.'time' => $c->getTime(), Caster::PREFIX_VIRTUAL.'in_daylight_time' => $c->inDaylightTime(), Caster::PREFIX_VIRTUAL.'is_lenient' => $c->isLenient(), Caster::PREFIX_VIRTUAL.'time_zone' => ($filter & Caster::EXCLUDE_VERBOSE) ? new CutStub($c->getTimeZone()) : $c->getTimeZone(), ]; return self::castError($c, $a); } public static function castIntlDateFormatter(\IntlDateFormatter $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $a += [ Caster::PREFIX_VIRTUAL.'locale' => $c->getLocale(), Caster::PREFIX_VIRTUAL.'pattern' => $c->getPattern(), Caster::PREFIX_VIRTUAL.'calendar' => $c->getCalendar(), Caster::PREFIX_VIRTUAL.'time_zone_id' => $c->getTimeZoneId(), Caster::PREFIX_VIRTUAL.'time_type' => $c->getTimeType(), Caster::PREFIX_VIRTUAL.'date_type' => $c->getDateType(), Caster::PREFIX_VIRTUAL.'calendar_object' => ($filter & Caster::EXCLUDE_VERBOSE) ? new CutStub($c->getCalendarObject()) : $c->getCalendarObject(), Caster::PREFIX_VIRTUAL.'time_zone' => ($filter & Caster::EXCLUDE_VERBOSE) ? new CutStub($c->getTimeZone()) : $c->getTimeZone(), ]; return self::castError($c, $a); } private static function castError(object $c, array $a): array { if ($errorCode = $c->getErrorCode()) { $a += [ Caster::PREFIX_VIRTUAL.'error_code' => $errorCode, Caster::PREFIX_VIRTUAL.'error_message' => $c->getErrorMessage(), ]; } return $a; } } var-dumper/Caster/LinkStub.php 0000644 00000006520 15025017654 0012323 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; /** * Represents a file or a URL. * * @author Nicolas Grekas <p@tchwork.com> */ class LinkStub extends ConstStub { public $inVendor = false; private static array $vendorRoots; private static array $composerRoots = []; public function __construct(string $label, int $line = 0, string $href = null) { $this->value = $label; if (null === $href) { $href = $label; } if (!\is_string($href)) { return; } if (str_starts_with($href, 'file://')) { if ($href === $label) { $label = substr($label, 7); } $href = substr($href, 7); } elseif (str_contains($href, '://')) { $this->attr['href'] = $href; return; } if (!is_file($href)) { return; } if ($line) { $this->attr['line'] = $line; } if ($label !== $this->attr['file'] = realpath($href) ?: $href) { return; } if ($composerRoot = $this->getComposerRoot($href, $this->inVendor)) { $this->attr['ellipsis'] = \strlen($href) - \strlen($composerRoot) + 1; $this->attr['ellipsis-type'] = 'path'; $this->attr['ellipsis-tail'] = 1 + ($this->inVendor ? 2 + \strlen(implode('', \array_slice(explode(\DIRECTORY_SEPARATOR, substr($href, 1 - $this->attr['ellipsis'])), 0, 2))) : 0); } elseif (3 < \count($ellipsis = explode(\DIRECTORY_SEPARATOR, $href))) { $this->attr['ellipsis'] = 2 + \strlen(implode('', \array_slice($ellipsis, -2))); $this->attr['ellipsis-type'] = 'path'; $this->attr['ellipsis-tail'] = 1; } } private function getComposerRoot(string $file, bool &$inVendor) { if (!isset(self::$vendorRoots)) { self::$vendorRoots = []; foreach (get_declared_classes() as $class) { if ('C' === $class[0] && str_starts_with($class, 'ComposerAutoloaderInit')) { $r = new \ReflectionClass($class); $v = \dirname($r->getFileName(), 2); if (is_file($v.'/composer/installed.json')) { self::$vendorRoots[] = $v.\DIRECTORY_SEPARATOR; } } } } $inVendor = false; if (isset(self::$composerRoots[$dir = \dirname($file)])) { return self::$composerRoots[$dir]; } foreach (self::$vendorRoots as $root) { if ($inVendor = str_starts_with($file, $root)) { return $root; } } $parent = $dir; while (!@is_file($parent.'/composer.json')) { if (!@file_exists($parent)) { // open_basedir restriction in effect break; } if ($parent === \dirname($parent)) { return self::$composerRoots[$dir] = false; } $parent = \dirname($parent); } return self::$composerRoots[$dir] = $parent.\DIRECTORY_SEPARATOR; } } var-dumper/Caster/AmqpCaster.php 0000644 00000015046 15025017654 0012633 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts Amqp related classes to array representation. * * @author Grégoire Pineau <lyrixx@lyrixx.info> * * @final */ class AmqpCaster { private const FLAGS = [ \AMQP_DURABLE => 'AMQP_DURABLE', \AMQP_PASSIVE => 'AMQP_PASSIVE', \AMQP_EXCLUSIVE => 'AMQP_EXCLUSIVE', \AMQP_AUTODELETE => 'AMQP_AUTODELETE', \AMQP_INTERNAL => 'AMQP_INTERNAL', \AMQP_NOLOCAL => 'AMQP_NOLOCAL', \AMQP_AUTOACK => 'AMQP_AUTOACK', \AMQP_IFEMPTY => 'AMQP_IFEMPTY', \AMQP_IFUNUSED => 'AMQP_IFUNUSED', \AMQP_MANDATORY => 'AMQP_MANDATORY', \AMQP_IMMEDIATE => 'AMQP_IMMEDIATE', \AMQP_MULTIPLE => 'AMQP_MULTIPLE', \AMQP_NOWAIT => 'AMQP_NOWAIT', \AMQP_REQUEUE => 'AMQP_REQUEUE', ]; private const EXCHANGE_TYPES = [ \AMQP_EX_TYPE_DIRECT => 'AMQP_EX_TYPE_DIRECT', \AMQP_EX_TYPE_FANOUT => 'AMQP_EX_TYPE_FANOUT', \AMQP_EX_TYPE_TOPIC => 'AMQP_EX_TYPE_TOPIC', \AMQP_EX_TYPE_HEADERS => 'AMQP_EX_TYPE_HEADERS', ]; public static function castConnection(\AMQPConnection $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'is_connected' => $c->isConnected(), ]; // Recent version of the extension already expose private properties if (isset($a["\x00AMQPConnection\x00login"])) { return $a; } // BC layer in the amqp lib if (method_exists($c, 'getReadTimeout')) { $timeout = $c->getReadTimeout(); } else { $timeout = $c->getTimeout(); } $a += [ $prefix.'is_connected' => $c->isConnected(), $prefix.'login' => $c->getLogin(), $prefix.'password' => $c->getPassword(), $prefix.'host' => $c->getHost(), $prefix.'vhost' => $c->getVhost(), $prefix.'port' => $c->getPort(), $prefix.'read_timeout' => $timeout, ]; return $a; } public static function castChannel(\AMQPChannel $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'is_connected' => $c->isConnected(), $prefix.'channel_id' => $c->getChannelId(), ]; // Recent version of the extension already expose private properties if (isset($a["\x00AMQPChannel\x00connection"])) { return $a; } $a += [ $prefix.'connection' => $c->getConnection(), $prefix.'prefetch_size' => $c->getPrefetchSize(), $prefix.'prefetch_count' => $c->getPrefetchCount(), ]; return $a; } public static function castQueue(\AMQPQueue $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'flags' => self::extractFlags($c->getFlags()), ]; // Recent version of the extension already expose private properties if (isset($a["\x00AMQPQueue\x00name"])) { return $a; } $a += [ $prefix.'connection' => $c->getConnection(), $prefix.'channel' => $c->getChannel(), $prefix.'name' => $c->getName(), $prefix.'arguments' => $c->getArguments(), ]; return $a; } public static function castExchange(\AMQPExchange $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'flags' => self::extractFlags($c->getFlags()), ]; $type = isset(self::EXCHANGE_TYPES[$c->getType()]) ? new ConstStub(self::EXCHANGE_TYPES[$c->getType()], $c->getType()) : $c->getType(); // Recent version of the extension already expose private properties if (isset($a["\x00AMQPExchange\x00name"])) { $a["\x00AMQPExchange\x00type"] = $type; return $a; } $a += [ $prefix.'connection' => $c->getConnection(), $prefix.'channel' => $c->getChannel(), $prefix.'name' => $c->getName(), $prefix.'type' => $type, $prefix.'arguments' => $c->getArguments(), ]; return $a; } public static function castEnvelope(\AMQPEnvelope $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; $deliveryMode = new ConstStub($c->getDeliveryMode().(2 === $c->getDeliveryMode() ? ' (persistent)' : ' (non-persistent)'), $c->getDeliveryMode()); // Recent version of the extension already expose private properties if (isset($a["\x00AMQPEnvelope\x00body"])) { $a["\0AMQPEnvelope\0delivery_mode"] = $deliveryMode; return $a; } if (!($filter & Caster::EXCLUDE_VERBOSE)) { $a += [$prefix.'body' => $c->getBody()]; } $a += [ $prefix.'delivery_tag' => $c->getDeliveryTag(), $prefix.'is_redelivery' => $c->isRedelivery(), $prefix.'exchange_name' => $c->getExchangeName(), $prefix.'routing_key' => $c->getRoutingKey(), $prefix.'content_type' => $c->getContentType(), $prefix.'content_encoding' => $c->getContentEncoding(), $prefix.'headers' => $c->getHeaders(), $prefix.'delivery_mode' => $deliveryMode, $prefix.'priority' => $c->getPriority(), $prefix.'correlation_id' => $c->getCorrelationId(), $prefix.'reply_to' => $c->getReplyTo(), $prefix.'expiration' => $c->getExpiration(), $prefix.'message_id' => $c->getMessageId(), $prefix.'timestamp' => $c->getTimeStamp(), $prefix.'type' => $c->getType(), $prefix.'user_id' => $c->getUserId(), $prefix.'app_id' => $c->getAppId(), ]; return $a; } private static function extractFlags(int $flags): ConstStub { $flagsArray = []; foreach (self::FLAGS as $value => $name) { if ($flags & $value) { $flagsArray[] = $name; } } if (!$flagsArray) { $flagsArray = ['AMQP_NOPARAM']; } return new ConstStub(implode('|', $flagsArray), $flags); } } var-dumper/Caster/RedisCaster.php 0000644 00000012152 15025017654 0012776 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts Redis class from ext-redis to array representation. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class RedisCaster { private const SERIALIZERS = [ \Redis::SERIALIZER_NONE => 'NONE', \Redis::SERIALIZER_PHP => 'PHP', 2 => 'IGBINARY', // Optional Redis::SERIALIZER_IGBINARY ]; private const MODES = [ \Redis::ATOMIC => 'ATOMIC', \Redis::MULTI => 'MULTI', \Redis::PIPELINE => 'PIPELINE', ]; private const COMPRESSION_MODES = [ 0 => 'NONE', // Redis::COMPRESSION_NONE 1 => 'LZF', // Redis::COMPRESSION_LZF ]; private const FAILOVER_OPTIONS = [ \RedisCluster::FAILOVER_NONE => 'NONE', \RedisCluster::FAILOVER_ERROR => 'ERROR', \RedisCluster::FAILOVER_DISTRIBUTE => 'DISTRIBUTE', \RedisCluster::FAILOVER_DISTRIBUTE_SLAVES => 'DISTRIBUTE_SLAVES', ]; public static function castRedis(\Redis $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; if (!$connected = $c->isConnected()) { return $a + [ $prefix.'isConnected' => $connected, ]; } $mode = $c->getMode(); return $a + [ $prefix.'isConnected' => $connected, $prefix.'host' => $c->getHost(), $prefix.'port' => $c->getPort(), $prefix.'auth' => $c->getAuth(), $prefix.'mode' => isset(self::MODES[$mode]) ? new ConstStub(self::MODES[$mode], $mode) : $mode, $prefix.'dbNum' => $c->getDbNum(), $prefix.'timeout' => $c->getTimeout(), $prefix.'lastError' => $c->getLastError(), $prefix.'persistentId' => $c->getPersistentID(), $prefix.'options' => self::getRedisOptions($c), ]; } public static function castRedisArray(\RedisArray $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; return $a + [ $prefix.'hosts' => $c->_hosts(), $prefix.'function' => ClassStub::wrapCallable($c->_function()), $prefix.'lastError' => $c->getLastError(), $prefix.'options' => self::getRedisOptions($c), ]; } public static function castRedisCluster(\RedisCluster $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $failover = $c->getOption(\RedisCluster::OPT_SLAVE_FAILOVER); $a += [ $prefix.'_masters' => $c->_masters(), $prefix.'_redir' => $c->_redir(), $prefix.'mode' => new ConstStub($c->getMode() ? 'MULTI' : 'ATOMIC', $c->getMode()), $prefix.'lastError' => $c->getLastError(), $prefix.'options' => self::getRedisOptions($c, [ 'SLAVE_FAILOVER' => isset(self::FAILOVER_OPTIONS[$failover]) ? new ConstStub(self::FAILOVER_OPTIONS[$failover], $failover) : $failover, ]), ]; return $a; } private static function getRedisOptions(\Redis|\RedisArray|\RedisCluster $redis, array $options = []): EnumStub { $serializer = $redis->getOption(\Redis::OPT_SERIALIZER); if (\is_array($serializer)) { foreach ($serializer as &$v) { if (isset(self::SERIALIZERS[$v])) { $v = new ConstStub(self::SERIALIZERS[$v], $v); } } } elseif (isset(self::SERIALIZERS[$serializer])) { $serializer = new ConstStub(self::SERIALIZERS[$serializer], $serializer); } $compression = \defined('Redis::OPT_COMPRESSION') ? $redis->getOption(\Redis::OPT_COMPRESSION) : 0; if (\is_array($compression)) { foreach ($compression as &$v) { if (isset(self::COMPRESSION_MODES[$v])) { $v = new ConstStub(self::COMPRESSION_MODES[$v], $v); } } } elseif (isset(self::COMPRESSION_MODES[$compression])) { $compression = new ConstStub(self::COMPRESSION_MODES[$compression], $compression); } $retry = \defined('Redis::OPT_SCAN') ? $redis->getOption(\Redis::OPT_SCAN) : 0; if (\is_array($retry)) { foreach ($retry as &$v) { $v = new ConstStub($v ? 'RETRY' : 'NORETRY', $v); } } else { $retry = new ConstStub($retry ? 'RETRY' : 'NORETRY', $retry); } $options += [ 'TCP_KEEPALIVE' => \defined('Redis::OPT_TCP_KEEPALIVE') ? $redis->getOption(\Redis::OPT_TCP_KEEPALIVE) : 0, 'READ_TIMEOUT' => $redis->getOption(\Redis::OPT_READ_TIMEOUT), 'COMPRESSION' => $compression, 'SERIALIZER' => $serializer, 'PREFIX' => $redis->getOption(\Redis::OPT_PREFIX), 'SCAN' => $retry, ]; return new EnumStub($options); } } var-dumper/Caster/Caster.php 0000644 00000013565 15025017654 0012020 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Helper for filtering out properties in casters. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class Caster { public const EXCLUDE_VERBOSE = 1; public const EXCLUDE_VIRTUAL = 2; public const EXCLUDE_DYNAMIC = 4; public const EXCLUDE_PUBLIC = 8; public const EXCLUDE_PROTECTED = 16; public const EXCLUDE_PRIVATE = 32; public const EXCLUDE_NULL = 64; public const EXCLUDE_EMPTY = 128; public const EXCLUDE_NOT_IMPORTANT = 256; public const EXCLUDE_STRICT = 512; public const PREFIX_VIRTUAL = "\0~\0"; public const PREFIX_DYNAMIC = "\0+\0"; public const PREFIX_PROTECTED = "\0*\0"; /** * Casts objects to arrays and adds the dynamic property prefix. * * @param bool $hasDebugInfo Whether the __debugInfo method exists on $obj or not */ public static function castObject(object $obj, string $class, bool $hasDebugInfo = false, string $debugClass = null): array { if ($hasDebugInfo) { try { $debugInfo = $obj->__debugInfo(); } catch (\Throwable $e) { // ignore failing __debugInfo() $hasDebugInfo = false; } } $a = $obj instanceof \Closure ? [] : (array) $obj; if ($obj instanceof \__PHP_Incomplete_Class) { return $a; } if ($a) { static $publicProperties = []; $debugClass = $debugClass ?? get_debug_type($obj); $i = 0; $prefixedKeys = []; foreach ($a as $k => $v) { if ("\0" !== ($k[0] ?? '')) { if (!isset($publicProperties[$class])) { foreach ((new \ReflectionClass($class))->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { $publicProperties[$class][$prop->name] = true; } } if (!isset($publicProperties[$class][$k])) { $prefixedKeys[$i] = self::PREFIX_DYNAMIC.$k; } } elseif ($debugClass !== $class && 1 === strpos($k, $class)) { $prefixedKeys[$i] = "\0".$debugClass.strrchr($k, "\0"); } ++$i; } if ($prefixedKeys) { $keys = array_keys($a); foreach ($prefixedKeys as $i => $k) { $keys[$i] = $k; } $a = array_combine($keys, $a); } } if ($hasDebugInfo && \is_array($debugInfo)) { foreach ($debugInfo as $k => $v) { if (!isset($k[0]) || "\0" !== $k[0]) { if (\array_key_exists(self::PREFIX_DYNAMIC.$k, $a)) { continue; } $k = self::PREFIX_VIRTUAL.$k; } unset($a[$k]); $a[$k] = $v; } } return $a; } /** * Filters out the specified properties. * * By default, a single match in the $filter bit field filters properties out, following an "or" logic. * When EXCLUDE_STRICT is set, an "and" logic is applied: all bits must match for a property to be removed. * * @param array $a The array containing the properties to filter * @param int $filter A bit field of Caster::EXCLUDE_* constants specifying which properties to filter out * @param string[] $listedProperties List of properties to exclude when Caster::EXCLUDE_VERBOSE is set, and to preserve when Caster::EXCLUDE_NOT_IMPORTANT is set * @param int &$count Set to the number of removed properties */ public static function filter(array $a, int $filter, array $listedProperties = [], ?int &$count = 0): array { $count = 0; foreach ($a as $k => $v) { $type = self::EXCLUDE_STRICT & $filter; if (null === $v) { $type |= self::EXCLUDE_NULL & $filter; $type |= self::EXCLUDE_EMPTY & $filter; } elseif (false === $v || '' === $v || '0' === $v || 0 === $v || 0.0 === $v || [] === $v) { $type |= self::EXCLUDE_EMPTY & $filter; } if ((self::EXCLUDE_NOT_IMPORTANT & $filter) && !\in_array($k, $listedProperties, true)) { $type |= self::EXCLUDE_NOT_IMPORTANT; } if ((self::EXCLUDE_VERBOSE & $filter) && \in_array($k, $listedProperties, true)) { $type |= self::EXCLUDE_VERBOSE; } if (!isset($k[1]) || "\0" !== $k[0]) { $type |= self::EXCLUDE_PUBLIC & $filter; } elseif ('~' === $k[1]) { $type |= self::EXCLUDE_VIRTUAL & $filter; } elseif ('+' === $k[1]) { $type |= self::EXCLUDE_DYNAMIC & $filter; } elseif ('*' === $k[1]) { $type |= self::EXCLUDE_PROTECTED & $filter; } else { $type |= self::EXCLUDE_PRIVATE & $filter; } if ((self::EXCLUDE_STRICT & $filter) ? $type === $filter : $type) { unset($a[$k]); ++$count; } } return $a; } public static function castPhpIncompleteClass(\__PHP_Incomplete_Class $c, array $a, Stub $stub, bool $isNested): array { if (isset($a['__PHP_Incomplete_Class_Name'])) { $stub->class .= '('.$a['__PHP_Incomplete_Class_Name'].')'; unset($a['__PHP_Incomplete_Class_Name']); } return $a; } } var-dumper/Caster/DsCaster.php 0000644 00000003066 15025017654 0012302 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Ds\Collection; use Ds\Map; use Ds\Pair; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts Ds extension classes to array representation. * * @author Jáchym Toušek <enumag@gmail.com> * * @final */ class DsCaster { public static function castCollection(Collection $c, array $a, Stub $stub, bool $isNested): array { $a[Caster::PREFIX_VIRTUAL.'count'] = $c->count(); $a[Caster::PREFIX_VIRTUAL.'capacity'] = $c->capacity(); if (!$c instanceof Map) { $a += $c->toArray(); } return $a; } public static function castMap(Map $c, array $a, Stub $stub, bool $isNested): array { foreach ($c as $k => $v) { $a[] = new DsPairStub($k, $v); } return $a; } public static function castPair(Pair $c, array $a, Stub $stub, bool $isNested): array { foreach ($c->toArray() as $k => $v) { $a[Caster::PREFIX_VIRTUAL.$k] = $v; } return $a; } public static function castPairStub(DsPairStub $c, array $a, Stub $stub, bool $isNested): array { if ($isNested) { $stub->class = Pair::class; $stub->value = null; $stub->handle = 0; $a = $c->value; } return $a; } } var-dumper/Caster/SymfonyCaster.php 0000644 00000005312 15025017654 0013374 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Uid\Ulid; use Symfony\Component\Uid\Uuid; use Symfony\Component\VarDumper\Cloner\Stub; /** * @final */ class SymfonyCaster { private const REQUEST_GETTERS = [ 'pathInfo' => 'getPathInfo', 'requestUri' => 'getRequestUri', 'baseUrl' => 'getBaseUrl', 'basePath' => 'getBasePath', 'method' => 'getMethod', 'format' => 'getRequestFormat', ]; public static function castRequest(Request $request, array $a, Stub $stub, bool $isNested) { $clone = null; foreach (self::REQUEST_GETTERS as $prop => $getter) { $key = Caster::PREFIX_PROTECTED.$prop; if (\array_key_exists($key, $a) && null === $a[$key]) { if (null === $clone) { $clone = clone $request; } $a[Caster::PREFIX_VIRTUAL.$prop] = $clone->{$getter}(); } } return $a; } public static function castHttpClient($client, array $a, Stub $stub, bool $isNested) { $multiKey = sprintf("\0%s\0multi", \get_class($client)); if (isset($a[$multiKey])) { $a[$multiKey] = new CutStub($a[$multiKey]); } return $a; } public static function castHttpClientResponse($response, array $a, Stub $stub, bool $isNested) { $stub->cut += \count($a); $a = []; foreach ($response->getInfo() as $k => $v) { $a[Caster::PREFIX_VIRTUAL.$k] = $v; } return $a; } public static function castUuid(Uuid $uuid, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'toBase58'] = $uuid->toBase58(); $a[Caster::PREFIX_VIRTUAL.'toBase32'] = $uuid->toBase32(); // symfony/uid >= 5.3 if (method_exists($uuid, 'getDateTime')) { $a[Caster::PREFIX_VIRTUAL.'time'] = $uuid->getDateTime()->format('Y-m-d H:i:s.u \U\T\C'); } return $a; } public static function castUlid(Ulid $ulid, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'toBase58'] = $ulid->toBase58(); $a[Caster::PREFIX_VIRTUAL.'toRfc4122'] = $ulid->toRfc4122(); // symfony/uid >= 5.3 if (method_exists($ulid, 'getDateTime')) { $a[Caster::PREFIX_VIRTUAL.'time'] = $ulid->getDateTime()->format('Y-m-d H:i:s.v \U\T\C'); } return $a; } } var-dumper/Caster/ImgStub.php 0000644 00000001175 15025017654 0012143 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; /** * @author Grégoire Pineau <lyrixx@lyrixx.info> */ class ImgStub extends ConstStub { public function __construct(string $data, string $contentType, string $size = '') { $this->value = ''; $this->attr['img-data'] = $data; $this->attr['img-size'] = $size; $this->attr['content-type'] = $contentType; } } var-dumper/Caster/DsPairStub.php 0000644 00000001166 15025017654 0012611 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * @author Nicolas Grekas <p@tchwork.com> */ class DsPairStub extends Stub { public function __construct(string|int $key, mixed $value) { $this->value = [ Caster::PREFIX_VIRTUAL.'key' => $key, Caster::PREFIX_VIRTUAL.'value' => $value, ]; } } var-dumper/Caster/MemcachedCaster.php 0000644 00000004465 15025017654 0013606 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * @author Jan Schädlich <jan.schaedlich@sensiolabs.de> * * @final */ class MemcachedCaster { private static array $optionConstants; private static array $defaultOptions; public static function castMemcached(\Memcached $c, array $a, Stub $stub, bool $isNested) { $a += [ Caster::PREFIX_VIRTUAL.'servers' => $c->getServerList(), Caster::PREFIX_VIRTUAL.'options' => new EnumStub( self::getNonDefaultOptions($c) ), ]; return $a; } private static function getNonDefaultOptions(\Memcached $c): array { self::$defaultOptions = self::$defaultOptions ?? self::discoverDefaultOptions(); self::$optionConstants = self::$optionConstants ?? self::getOptionConstants(); $nonDefaultOptions = []; foreach (self::$optionConstants as $constantKey => $value) { if (self::$defaultOptions[$constantKey] !== $option = $c->getOption($value)) { $nonDefaultOptions[$constantKey] = $option; } } return $nonDefaultOptions; } private static function discoverDefaultOptions(): array { $defaultMemcached = new \Memcached(); $defaultMemcached->addServer('127.0.0.1', 11211); $defaultOptions = []; self::$optionConstants = self::$optionConstants ?? self::getOptionConstants(); foreach (self::$optionConstants as $constantKey => $value) { $defaultOptions[$constantKey] = $defaultMemcached->getOption($value); } return $defaultOptions; } private static function getOptionConstants(): array { $reflectedMemcached = new \ReflectionClass(\Memcached::class); $optionConstants = []; foreach ($reflectedMemcached->getConstants() as $constantKey => $value) { if (str_starts_with($constantKey, 'OPT_')) { $optionConstants[$constantKey] = $value; } } return $optionConstants; } } var-dumper/Caster/DOMCaster.php 0000644 00000023606 15025017654 0012355 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts DOM related classes to array representation. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class DOMCaster { private const ERROR_CODES = [ \DOM_PHP_ERR => 'DOM_PHP_ERR', \DOM_INDEX_SIZE_ERR => 'DOM_INDEX_SIZE_ERR', \DOMSTRING_SIZE_ERR => 'DOMSTRING_SIZE_ERR', \DOM_HIERARCHY_REQUEST_ERR => 'DOM_HIERARCHY_REQUEST_ERR', \DOM_WRONG_DOCUMENT_ERR => 'DOM_WRONG_DOCUMENT_ERR', \DOM_INVALID_CHARACTER_ERR => 'DOM_INVALID_CHARACTER_ERR', \DOM_NO_DATA_ALLOWED_ERR => 'DOM_NO_DATA_ALLOWED_ERR', \DOM_NO_MODIFICATION_ALLOWED_ERR => 'DOM_NO_MODIFICATION_ALLOWED_ERR', \DOM_NOT_FOUND_ERR => 'DOM_NOT_FOUND_ERR', \DOM_NOT_SUPPORTED_ERR => 'DOM_NOT_SUPPORTED_ERR', \DOM_INUSE_ATTRIBUTE_ERR => 'DOM_INUSE_ATTRIBUTE_ERR', \DOM_INVALID_STATE_ERR => 'DOM_INVALID_STATE_ERR', \DOM_SYNTAX_ERR => 'DOM_SYNTAX_ERR', \DOM_INVALID_MODIFICATION_ERR => 'DOM_INVALID_MODIFICATION_ERR', \DOM_NAMESPACE_ERR => 'DOM_NAMESPACE_ERR', \DOM_INVALID_ACCESS_ERR => 'DOM_INVALID_ACCESS_ERR', \DOM_VALIDATION_ERR => 'DOM_VALIDATION_ERR', ]; private const NODE_TYPES = [ \XML_ELEMENT_NODE => 'XML_ELEMENT_NODE', \XML_ATTRIBUTE_NODE => 'XML_ATTRIBUTE_NODE', \XML_TEXT_NODE => 'XML_TEXT_NODE', \XML_CDATA_SECTION_NODE => 'XML_CDATA_SECTION_NODE', \XML_ENTITY_REF_NODE => 'XML_ENTITY_REF_NODE', \XML_ENTITY_NODE => 'XML_ENTITY_NODE', \XML_PI_NODE => 'XML_PI_NODE', \XML_COMMENT_NODE => 'XML_COMMENT_NODE', \XML_DOCUMENT_NODE => 'XML_DOCUMENT_NODE', \XML_DOCUMENT_TYPE_NODE => 'XML_DOCUMENT_TYPE_NODE', \XML_DOCUMENT_FRAG_NODE => 'XML_DOCUMENT_FRAG_NODE', \XML_NOTATION_NODE => 'XML_NOTATION_NODE', \XML_HTML_DOCUMENT_NODE => 'XML_HTML_DOCUMENT_NODE', \XML_DTD_NODE => 'XML_DTD_NODE', \XML_ELEMENT_DECL_NODE => 'XML_ELEMENT_DECL_NODE', \XML_ATTRIBUTE_DECL_NODE => 'XML_ATTRIBUTE_DECL_NODE', \XML_ENTITY_DECL_NODE => 'XML_ENTITY_DECL_NODE', \XML_NAMESPACE_DECL_NODE => 'XML_NAMESPACE_DECL_NODE', ]; public static function castException(\DOMException $e, array $a, Stub $stub, bool $isNested) { $k = Caster::PREFIX_PROTECTED.'code'; if (isset($a[$k], self::ERROR_CODES[$a[$k]])) { $a[$k] = new ConstStub(self::ERROR_CODES[$a[$k]], $a[$k]); } return $a; } public static function castLength($dom, array $a, Stub $stub, bool $isNested) { $a += [ 'length' => $dom->length, ]; return $a; } public static function castImplementation(\DOMImplementation $dom, array $a, Stub $stub, bool $isNested) { $a += [ Caster::PREFIX_VIRTUAL.'Core' => '1.0', Caster::PREFIX_VIRTUAL.'XML' => '2.0', ]; return $a; } public static function castNode(\DOMNode $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'nodeName' => $dom->nodeName, 'nodeValue' => new CutStub($dom->nodeValue), 'nodeType' => new ConstStub(self::NODE_TYPES[$dom->nodeType], $dom->nodeType), 'parentNode' => new CutStub($dom->parentNode), 'childNodes' => $dom->childNodes, 'firstChild' => new CutStub($dom->firstChild), 'lastChild' => new CutStub($dom->lastChild), 'previousSibling' => new CutStub($dom->previousSibling), 'nextSibling' => new CutStub($dom->nextSibling), 'attributes' => $dom->attributes, 'ownerDocument' => new CutStub($dom->ownerDocument), 'namespaceURI' => $dom->namespaceURI, 'prefix' => $dom->prefix, 'localName' => $dom->localName, 'baseURI' => $dom->baseURI ? new LinkStub($dom->baseURI) : $dom->baseURI, 'textContent' => new CutStub($dom->textContent), ]; return $a; } public static function castNameSpaceNode(\DOMNameSpaceNode $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'nodeName' => $dom->nodeName, 'nodeValue' => new CutStub($dom->nodeValue), 'nodeType' => new ConstStub(self::NODE_TYPES[$dom->nodeType], $dom->nodeType), 'prefix' => $dom->prefix, 'localName' => $dom->localName, 'namespaceURI' => $dom->namespaceURI, 'ownerDocument' => new CutStub($dom->ownerDocument), 'parentNode' => new CutStub($dom->parentNode), ]; return $a; } public static function castDocument(\DOMDocument $dom, array $a, Stub $stub, bool $isNested, int $filter = 0) { $a += [ 'doctype' => $dom->doctype, 'implementation' => $dom->implementation, 'documentElement' => new CutStub($dom->documentElement), 'actualEncoding' => $dom->actualEncoding, 'encoding' => $dom->encoding, 'xmlEncoding' => $dom->xmlEncoding, 'standalone' => $dom->standalone, 'xmlStandalone' => $dom->xmlStandalone, 'version' => $dom->version, 'xmlVersion' => $dom->xmlVersion, 'strictErrorChecking' => $dom->strictErrorChecking, 'documentURI' => $dom->documentURI ? new LinkStub($dom->documentURI) : $dom->documentURI, 'config' => $dom->config, 'formatOutput' => $dom->formatOutput, 'validateOnParse' => $dom->validateOnParse, 'resolveExternals' => $dom->resolveExternals, 'preserveWhiteSpace' => $dom->preserveWhiteSpace, 'recover' => $dom->recover, 'substituteEntities' => $dom->substituteEntities, ]; if (!($filter & Caster::EXCLUDE_VERBOSE)) { $formatOutput = $dom->formatOutput; $dom->formatOutput = true; $a += [Caster::PREFIX_VIRTUAL.'xml' => $dom->saveXML()]; $dom->formatOutput = $formatOutput; } return $a; } public static function castCharacterData(\DOMCharacterData $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'data' => $dom->data, 'length' => $dom->length, ]; return $a; } public static function castAttr(\DOMAttr $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'name' => $dom->name, 'specified' => $dom->specified, 'value' => $dom->value, 'ownerElement' => $dom->ownerElement, 'schemaTypeInfo' => $dom->schemaTypeInfo, ]; return $a; } public static function castElement(\DOMElement $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'tagName' => $dom->tagName, 'schemaTypeInfo' => $dom->schemaTypeInfo, ]; return $a; } public static function castText(\DOMText $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'wholeText' => $dom->wholeText, ]; return $a; } public static function castTypeinfo(\DOMTypeinfo $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'typeName' => $dom->typeName, 'typeNamespace' => $dom->typeNamespace, ]; return $a; } public static function castDomError(\DOMDomError $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'severity' => $dom->severity, 'message' => $dom->message, 'type' => $dom->type, 'relatedException' => $dom->relatedException, 'related_data' => $dom->related_data, 'location' => $dom->location, ]; return $a; } public static function castLocator(\DOMLocator $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'lineNumber' => $dom->lineNumber, 'columnNumber' => $dom->columnNumber, 'offset' => $dom->offset, 'relatedNode' => $dom->relatedNode, 'uri' => $dom->uri ? new LinkStub($dom->uri, $dom->lineNumber) : $dom->uri, ]; return $a; } public static function castDocumentType(\DOMDocumentType $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'name' => $dom->name, 'entities' => $dom->entities, 'notations' => $dom->notations, 'publicId' => $dom->publicId, 'systemId' => $dom->systemId, 'internalSubset' => $dom->internalSubset, ]; return $a; } public static function castNotation(\DOMNotation $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'publicId' => $dom->publicId, 'systemId' => $dom->systemId, ]; return $a; } public static function castEntity(\DOMEntity $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'publicId' => $dom->publicId, 'systemId' => $dom->systemId, 'notationName' => $dom->notationName, 'actualEncoding' => $dom->actualEncoding, 'encoding' => $dom->encoding, 'version' => $dom->version, ]; return $a; } public static function castProcessingInstruction(\DOMProcessingInstruction $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'target' => $dom->target, 'data' => $dom->data, ]; return $a; } public static function castXPath(\DOMXPath $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'document' => $dom->document, ]; return $a; } } var-dumper/Caster/PdoCaster.php 0000644 00000006746 15025017654 0012466 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts PDO related classes to array representation. * * @author Nicolas Grekas <p@tchwork.com> * * @final */ class PdoCaster { private const PDO_ATTRIBUTES = [ 'CASE' => [ \PDO::CASE_LOWER => 'LOWER', \PDO::CASE_NATURAL => 'NATURAL', \PDO::CASE_UPPER => 'UPPER', ], 'ERRMODE' => [ \PDO::ERRMODE_SILENT => 'SILENT', \PDO::ERRMODE_WARNING => 'WARNING', \PDO::ERRMODE_EXCEPTION => 'EXCEPTION', ], 'TIMEOUT', 'PREFETCH', 'AUTOCOMMIT', 'PERSISTENT', 'DRIVER_NAME', 'SERVER_INFO', 'ORACLE_NULLS' => [ \PDO::NULL_NATURAL => 'NATURAL', \PDO::NULL_EMPTY_STRING => 'EMPTY_STRING', \PDO::NULL_TO_STRING => 'TO_STRING', ], 'CLIENT_VERSION', 'SERVER_VERSION', 'STATEMENT_CLASS', 'EMULATE_PREPARES', 'CONNECTION_STATUS', 'STRINGIFY_FETCHES', 'DEFAULT_FETCH_MODE' => [ \PDO::FETCH_ASSOC => 'ASSOC', \PDO::FETCH_BOTH => 'BOTH', \PDO::FETCH_LAZY => 'LAZY', \PDO::FETCH_NUM => 'NUM', \PDO::FETCH_OBJ => 'OBJ', ], ]; public static function castPdo(\PDO $c, array $a, Stub $stub, bool $isNested) { $attr = []; $errmode = $c->getAttribute(\PDO::ATTR_ERRMODE); $c->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); foreach (self::PDO_ATTRIBUTES as $k => $v) { if (!isset($k[0])) { $k = $v; $v = []; } try { $attr[$k] = 'ERRMODE' === $k ? $errmode : $c->getAttribute(\constant('PDO::ATTR_'.$k)); if ($v && isset($v[$attr[$k]])) { $attr[$k] = new ConstStub($v[$attr[$k]], $attr[$k]); } } catch (\Exception $e) { } } if (isset($attr[$k = 'STATEMENT_CLASS'][1])) { if ($attr[$k][1]) { $attr[$k][1] = new ArgsStub($attr[$k][1], '__construct', $attr[$k][0]); } $attr[$k][0] = new ClassStub($attr[$k][0]); } $prefix = Caster::PREFIX_VIRTUAL; $a += [ $prefix.'inTransaction' => method_exists($c, 'inTransaction'), $prefix.'errorInfo' => $c->errorInfo(), $prefix.'attributes' => new EnumStub($attr), ]; if ($a[$prefix.'inTransaction']) { $a[$prefix.'inTransaction'] = $c->inTransaction(); } else { unset($a[$prefix.'inTransaction']); } if (!isset($a[$prefix.'errorInfo'][1], $a[$prefix.'errorInfo'][2])) { unset($a[$prefix.'errorInfo']); } $c->setAttribute(\PDO::ATTR_ERRMODE, $errmode); return $a; } public static function castPdoStatement(\PDOStatement $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a[$prefix.'errorInfo'] = $c->errorInfo(); if (!isset($a[$prefix.'errorInfo'][1], $a[$prefix.'errorInfo'][2])) { unset($a[$prefix.'errorInfo']); } return $a; } } var-dumper/VarDumper.php 0000644 00000007356 15025017654 0011264 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\VarDumper\Caster\ReflectionCaster; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Dumper\ContextProvider\CliContextProvider; use Symfony\Component\VarDumper\Dumper\ContextProvider\RequestContextProvider; use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider; use Symfony\Component\VarDumper\Dumper\ContextualizedDumper; use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Symfony\Component\VarDumper\Dumper\ServerDumper; // Load the global dump() function require_once __DIR__.'/Resources/functions/dump.php'; /** * @author Nicolas Grekas <p@tchwork.com> */ class VarDumper { /** * @var callable|null */ private static $handler; public static function dump(mixed $var) { if (null === self::$handler) { self::register(); } return (self::$handler)($var); } public static function setHandler(callable $callable = null): ?callable { $prevHandler = self::$handler; // Prevent replacing the handler with expected format as soon as the env var was set: if (isset($_SERVER['VAR_DUMPER_FORMAT'])) { return $prevHandler; } self::$handler = $callable; return $prevHandler; } private static function register(): void { $cloner = new VarCloner(); $cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); $format = $_SERVER['VAR_DUMPER_FORMAT'] ?? null; switch (true) { case 'html' === $format: $dumper = new HtmlDumper(); break; case 'cli' === $format: $dumper = new CliDumper(); break; case 'server' === $format: case $format && 'tcp' === parse_url($format, \PHP_URL_SCHEME): $host = 'server' === $format ? $_SERVER['VAR_DUMPER_SERVER'] ?? '127.0.0.1:9912' : $format; $dumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? new CliDumper() : new HtmlDumper(); $dumper = new ServerDumper($host, $dumper, self::getDefaultContextProviders()); break; default: $dumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? new CliDumper() : new HtmlDumper(); } if (!$dumper instanceof ServerDumper) { $dumper = new ContextualizedDumper($dumper, [new SourceContextProvider()]); } self::$handler = function ($var) use ($cloner, $dumper) { $dumper->dump($cloner->cloneVar($var)); }; } private static function getDefaultContextProviders(): array { $contextProviders = []; if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && class_exists(Request::class)) { $requestStack = new RequestStack(); $requestStack->push(Request::createFromGlobals()); $contextProviders['request'] = new RequestContextProvider($requestStack); } $fileLinkFormatter = class_exists(FileLinkFormatter::class) ? new FileLinkFormatter(null, $requestStack ?? null) : null; return $contextProviders + [ 'cli' => new CliContextProvider(), 'source' => new SourceContextProvider(null, null, $fileLinkFormatter), ]; } } var-dumper/composer.json 0000644 00000002713 15025017654 0011360 0 ustar 00 { "name": "symfony/var-dumper", "type": "library", "description": "Provides mechanisms for walking through any arbitrary PHP variable", "keywords": ["dump", "debug"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.0.2", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { "ext-iconv": "*", "symfony/console": "^5.4|^6.0", "symfony/process": "^5.4|^6.0", "symfony/uid": "^5.4|^6.0", "twig/twig": "^2.13|^3.0.4" }, "conflict": { "phpunit/phpunit": "<5.4.3", "symfony/console": "<5.4" }, "suggest": { "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", "ext-intl": "To show region name in time zone dump", "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" }, "autoload": { "files": [ "Resources/functions/dump.php" ], "psr-4": { "Symfony\\Component\\VarDumper\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "bin": [ "Resources/bin/var-dump-server" ], "minimum-stability": "dev" } var-dumper/CHANGELOG.md 0000644 00000004225 15025017654 0010447 0 ustar 00 CHANGELOG ========= 5.4 --- * Add ability to style integer and double values independently * Add casters for Symfony's UUIDs and ULIDs * Add support for `Fiber` 5.2.0 ----- * added support for PHPUnit `--colors` option * added `VAR_DUMPER_FORMAT=server` env var value support * prevent replacing the handler when the `VAR_DUMPER_FORMAT` env var is set 5.1.0 ----- * added `RdKafka` support 4.4.0 ----- * added `VarDumperTestTrait::setUpVarDumper()` and `VarDumperTestTrait::tearDownVarDumper()` to configure casters & flags to use in tests * added `ImagineCaster` and infrastructure to dump images * added the stamps of a message after it is dispatched in `TraceableMessageBus` and `MessengerDataCollector` collected data * added `UuidCaster` * made all casters final * added support for the `NO_COLOR` env var (https://no-color.org/) 4.3.0 ----- * added `DsCaster` to support dumping the contents of data structures from the Ds extension 4.2.0 ----- * support selecting the format to use by setting the environment variable `VAR_DUMPER_FORMAT` to `html` or `cli` 4.1.0 ----- * added a `ServerDumper` to send serialized Data clones to a server * added a `ServerDumpCommand` and `DumpServer` to run a server collecting and displaying dumps on a single place with multiple formats support * added `CliDescriptor` and `HtmlDescriptor` descriptors for `server:dump` CLI and HTML formats support 4.0.0 ----- * support for passing `\ReflectionClass` instances to the `Caster::castObject()` method has been dropped, pass class names as strings instead * the `Data::getRawData()` method has been removed * the `VarDumperTestTrait::assertDumpEquals()` method expects a 3rd `$filter = 0` argument and moves `$message = ''` argument at 4th position. * the `VarDumperTestTrait::assertDumpMatchesFormat()` method expects a 3rd `$filter = 0` argument and moves `$message = ''` argument at 4th position. 3.4.0 ----- * added `AbstractCloner::setMinDepth()` function to ensure minimum tree depth * deprecated `MongoCaster` 2.7.0 ----- * deprecated `Cloner\Data::getLimitedClone()`. Use `withMaxDepth`, `withMaxItemsPerDepth` or `withRefHandles` instead. var-dumper/Exception/ThrowingCasterException.php 0000644 00000001217 15025017654 0016125 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Exception; /** * @author Nicolas Grekas <p@tchwork.com> */ class ThrowingCasterException extends \Exception { /** * @param \Throwable $prev The exception thrown from the caster */ public function __construct(\Throwable $prev) { parent::__construct('Unexpected '.\get_class($prev).' thrown from a caster: '.$prev->getMessage(), 0, $prev); } } var-dumper/Command/Descriptor/HtmlDescriptor.php 0000644 00000007161 15025017654 0016010 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Command\Descriptor; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Dumper\HtmlDumper; /** * Describe collected data clones for html output. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> * * @final */ class HtmlDescriptor implements DumpDescriptorInterface { private $dumper; private bool $initialized = false; public function __construct(HtmlDumper $dumper) { $this->dumper = $dumper; } public function describe(OutputInterface $output, Data $data, array $context, int $clientId): void { if (!$this->initialized) { $styles = file_get_contents(__DIR__.'/../../Resources/css/htmlDescriptor.css'); $scripts = file_get_contents(__DIR__.'/../../Resources/js/htmlDescriptor.js'); $output->writeln("<style>$styles</style><script>$scripts</script>"); $this->initialized = true; } $title = '-'; if (isset($context['request'])) { $request = $context['request']; $controller = "<span class='dumped-tag'>{$this->dumper->dump($request['controller'], true, ['maxDepth' => 0])}</span>"; $title = sprintf('<code>%s</code> <a href="%s">%s</a>', $request['method'], $uri = $request['uri'], $uri); $dedupIdentifier = $request['identifier']; } elseif (isset($context['cli'])) { $title = '<code>$ </code>'.$context['cli']['command_line']; $dedupIdentifier = $context['cli']['identifier']; } else { $dedupIdentifier = uniqid('', true); } $sourceDescription = ''; if (isset($context['source'])) { $source = $context['source']; $projectDir = $source['project_dir'] ?? null; $sourceDescription = sprintf('%s on line %d', $source['name'], $source['line']); if (isset($source['file_link'])) { $sourceDescription = sprintf('<a href="%s">%s</a>', $source['file_link'], $sourceDescription); } } $isoDate = $this->extractDate($context, 'c'); $tags = array_filter([ 'controller' => $controller ?? null, 'project dir' => $projectDir ?? null, ]); $output->writeln(<<<HTML <article data-dedup-id="$dedupIdentifier"> <header> <div class="row"> <h2 class="col">$title</h2> <time class="col text-small" title="$isoDate" datetime="$isoDate"> {$this->extractDate($context)} </time> </div> {$this->renderTags($tags)} </header> <section class="body"> <p class="text-small"> $sourceDescription </p> {$this->dumper->dump($data, true)} </section> </article> HTML ); } private function extractDate(array $context, string $format = 'r'): string { return date($format, (int) $context['timestamp']); } private function renderTags(array $tags): string { if (!$tags) { return ''; } $renderedTags = ''; foreach ($tags as $key => $value) { $renderedTags .= sprintf('<li><span class="badge">%s</span>%s</li>', $key, $value); } return <<<HTML <div class="row"> <ul class="tags"> $renderedTags </ul> </div> HTML; } } var-dumper/Command/Descriptor/DumpDescriptorInterface.php 0000644 00000001142 15025017654 0017623 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Command\Descriptor; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\VarDumper\Cloner\Data; /** * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> */ interface DumpDescriptorInterface { public function describe(OutputInterface $output, Data $data, array $context, int $clientId): void; } var-dumper/Command/Descriptor/CliDescriptor.php 0000644 00000005132 15025017654 0015607 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Command\Descriptor; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Dumper\CliDumper; /** * Describe collected data clones for cli output. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> * * @final */ class CliDescriptor implements DumpDescriptorInterface { private $dumper; private mixed $lastIdentifier = null; public function __construct(CliDumper $dumper) { $this->dumper = $dumper; } public function describe(OutputInterface $output, Data $data, array $context, int $clientId): void { $io = $output instanceof SymfonyStyle ? $output : new SymfonyStyle(new ArrayInput([]), $output); $this->dumper->setColors($output->isDecorated()); $rows = [['date', date('r', (int) $context['timestamp'])]]; $lastIdentifier = $this->lastIdentifier; $this->lastIdentifier = $clientId; $section = "Received from client #$clientId"; if (isset($context['request'])) { $request = $context['request']; $this->lastIdentifier = $request['identifier']; $section = sprintf('%s %s', $request['method'], $request['uri']); if ($controller = $request['controller']) { $rows[] = ['controller', rtrim($this->dumper->dump($controller, true), "\n")]; } } elseif (isset($context['cli'])) { $this->lastIdentifier = $context['cli']['identifier']; $section = '$ '.$context['cli']['command_line']; } if ($this->lastIdentifier !== $lastIdentifier) { $io->section($section); } if (isset($context['source'])) { $source = $context['source']; $sourceInfo = sprintf('%s on line %d', $source['name'], $source['line']); if ($fileLink = $source['file_link'] ?? null) { $sourceInfo = sprintf('<href=%s>%s</>', $fileLink, $sourceInfo); } $rows[] = ['source', $sourceInfo]; $file = $source['file_relative'] ?? $source['file']; $rows[] = ['file', $file]; } $io->table([], $rows); $this->dumper->dump($data); $io->newLine(); } } var-dumper/Command/ServerDumpCommand.php 0000644 00000007405 15025017654 0014323 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Command\Descriptor\CliDescriptor; use Symfony\Component\VarDumper\Command\Descriptor\DumpDescriptorInterface; use Symfony\Component\VarDumper\Command\Descriptor\HtmlDescriptor; use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Symfony\Component\VarDumper\Server\DumpServer; /** * Starts a dump server to collect and output dumps on a single place with multiple formats support. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> * * @final */ #[AsCommand(name: 'server:dump', description: 'Start a dump server that collects and displays dumps in a single place')] class ServerDumpCommand extends Command { private $server; /** @var DumpDescriptorInterface[] */ private array $descriptors; public function __construct(DumpServer $server, array $descriptors = []) { $this->server = $server; $this->descriptors = $descriptors + [ 'cli' => new CliDescriptor(new CliDumper()), 'html' => new HtmlDescriptor(new HtmlDumper()), ]; parent::__construct(); } protected function configure() { $this ->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format (%s)', implode(', ', $this->getAvailableFormats())), 'cli') ->setHelp(<<<'EOF' <info>%command.name%</info> starts a dump server that collects and displays dumps in a single place for debugging you application: <info>php %command.full_name%</info> You can consult dumped data in HTML format in your browser by providing the <comment>--format=html</comment> option and redirecting the output to a file: <info>php %command.full_name% --format="html" > dump.html</info> EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $format = $input->getOption('format'); if (!$descriptor = $this->descriptors[$format] ?? null) { throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $format)); } $errorIo = $io->getErrorStyle(); $errorIo->title('Symfony Var Dumper Server'); $this->server->start(); $errorIo->success(sprintf('Server listening on %s', $this->server->getHost())); $errorIo->comment('Quit the server with CONTROL-C.'); $this->server->listen(function (Data $data, array $context, int $clientId) use ($descriptor, $io) { $descriptor->describe($io, $data, $context, $clientId); }); return 0; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues($this->getAvailableFormats()); } } private function getAvailableFormats(): array { return array_keys($this->descriptors); } } var-dumper/README.md 0000644 00000001137 15025017654 0010114 0 ustar 00 VarDumper Component =================== The VarDumper component provides mechanisms for walking through any arbitrary PHP variable. It provides a better `dump()` function that you can use instead of `var_dump()`. Resources --------- * [Documentation](https://symfony.com/doc/current/components/var_dumper/introduction.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) var-dumper/Test/VarDumperTestTrait.php 0000644 00000005065 15025017654 0014042 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Test; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\CliDumper; /** * @author Nicolas Grekas <p@tchwork.com> */ trait VarDumperTestTrait { /** * @internal */ private array $varDumperConfig = [ 'casters' => [], 'flags' => null, ]; protected function setUpVarDumper(array $casters, int $flags = null): void { $this->varDumperConfig['casters'] = $casters; $this->varDumperConfig['flags'] = $flags; } /** * @after */ protected function tearDownVarDumper(): void { $this->varDumperConfig['casters'] = []; $this->varDumperConfig['flags'] = null; } public function assertDumpEquals(mixed $expected, mixed $data, int $filter = 0, string $message = '') { $this->assertSame($this->prepareExpectation($expected, $filter), $this->getDump($data, null, $filter), $message); } public function assertDumpMatchesFormat(mixed $expected, mixed $data, int $filter = 0, string $message = '') { $this->assertStringMatchesFormat($this->prepareExpectation($expected, $filter), $this->getDump($data, null, $filter), $message); } protected function getDump(mixed $data, string|int $key = null, int $filter = 0): ?string { if (null === $flags = $this->varDumperConfig['flags']) { $flags = getenv('DUMP_LIGHT_ARRAY') ? CliDumper::DUMP_LIGHT_ARRAY : 0; $flags |= getenv('DUMP_STRING_LENGTH') ? CliDumper::DUMP_STRING_LENGTH : 0; $flags |= getenv('DUMP_COMMA_SEPARATOR') ? CliDumper::DUMP_COMMA_SEPARATOR : 0; } $cloner = new VarCloner(); $cloner->addCasters($this->varDumperConfig['casters']); $cloner->setMaxItems(-1); $dumper = new CliDumper(null, null, $flags); $dumper->setColors(false); $data = $cloner->cloneVar($data, $filter)->withRefHandles(false); if (null !== $key && null === $data = $data->seek($key)) { return null; } return rtrim($dumper->dump($data, true)); } private function prepareExpectation(mixed $expected, int $filter): string { if (!\is_string($expected)) { $expected = $this->getDump($expected, null, $filter); } return rtrim($expected); } } var-dumper/LICENSE 0000644 00000002051 15025017654 0007636 0 ustar 00 Copyright (c) 2014-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. var-dumper/Server/DumpServer.php 0000644 00000006316 15025017654 0012714 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Server; use Psr\Log\LoggerInterface; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\Stub; /** * A server collecting Data clones sent by a ServerDumper. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> * * @final */ class DumpServer { private string $host; private $logger; /** * @var resource|null */ private $socket; public function __construct(string $host, LoggerInterface $logger = null) { if (!str_contains($host, '://')) { $host = 'tcp://'.$host; } $this->host = $host; $this->logger = $logger; } public function start(): void { if (!$this->socket = stream_socket_server($this->host, $errno, $errstr)) { throw new \RuntimeException(sprintf('Server start failed on "%s": ', $this->host).$errstr.' '.$errno); } } public function listen(callable $callback): void { if (null === $this->socket) { $this->start(); } foreach ($this->getMessages() as $clientId => $message) { if ($this->logger) { $this->logger->info('Received a payload from client {clientId}', ['clientId' => $clientId]); } $payload = @unserialize(base64_decode($message), ['allowed_classes' => [Data::class, Stub::class]]); // Impossible to decode the message, give up. if (false === $payload) { if ($this->logger) { $this->logger->warning('Unable to decode a message from {clientId} client.', ['clientId' => $clientId]); } continue; } if (!\is_array($payload) || \count($payload) < 2 || !$payload[0] instanceof Data || !\is_array($payload[1])) { if ($this->logger) { $this->logger->warning('Invalid payload from {clientId} client. Expected an array of two elements (Data $data, array $context)', ['clientId' => $clientId]); } continue; } [$data, $context] = $payload; $callback($data, $context, $clientId); } } public function getHost(): string { return $this->host; } private function getMessages(): iterable { $sockets = [(int) $this->socket => $this->socket]; $write = []; while (true) { $read = $sockets; stream_select($read, $write, $write, null); foreach ($read as $stream) { if ($this->socket === $stream) { $stream = stream_socket_accept($this->socket); $sockets[(int) $stream] = $stream; } elseif (feof($stream)) { unset($sockets[(int) $stream]); fclose($stream); } else { yield (int) $stream => fgets($stream); } } } } } var-dumper/Server/Connection.php 0000644 00000005345 15025017654 0012720 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Server; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface; /** * Forwards serialized Data clones to a server. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> */ class Connection { private string $host; private array $contextProviders; /** * @var resource|null */ private $socket; /** * @param string $host The server host * @param ContextProviderInterface[] $contextProviders Context providers indexed by context name */ public function __construct(string $host, array $contextProviders = []) { if (!str_contains($host, '://')) { $host = 'tcp://'.$host; } $this->host = $host; $this->contextProviders = $contextProviders; } public function getContextProviders(): array { return $this->contextProviders; } public function write(Data $data): bool { $socketIsFresh = !$this->socket; if (!$this->socket = $this->socket ?: $this->createSocket()) { return false; } $context = ['timestamp' => microtime(true)]; foreach ($this->contextProviders as $name => $provider) { $context[$name] = $provider->getContext(); } $context = array_filter($context); $encodedPayload = base64_encode(serialize([$data, $context]))."\n"; set_error_handler([self::class, 'nullErrorHandler']); try { if (-1 !== stream_socket_sendto($this->socket, $encodedPayload)) { return true; } if (!$socketIsFresh) { stream_socket_shutdown($this->socket, \STREAM_SHUT_RDWR); fclose($this->socket); $this->socket = $this->createSocket(); } if (-1 !== stream_socket_sendto($this->socket, $encodedPayload)) { return true; } } finally { restore_error_handler(); } return false; } private static function nullErrorHandler(int $t, string $m) { // no-op } private function createSocket() { set_error_handler([self::class, 'nullErrorHandler']); try { return stream_socket_client($this->host, $errno, $errstr, 3, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT); } finally { restore_error_handler(); } } } var-dumper/Cloner/Data.php 0000644 00000032217 15025017654 0011444 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; use Symfony\Component\VarDumper\Caster\Caster; use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider; /** * @author Nicolas Grekas <p@tchwork.com> */ class Data implements \ArrayAccess, \Countable, \IteratorAggregate { private array $data; private int $position = 0; private int|string $key = 0; private int $maxDepth = 20; private int $maxItemsPerDepth = -1; private int $useRefHandles = -1; private array $context = []; /** * @param array $data An array as returned by ClonerInterface::cloneVar() */ public function __construct(array $data) { $this->data = $data; } public function getType(): ?string { $item = $this->data[$this->position][$this->key]; if ($item instanceof Stub && Stub::TYPE_REF === $item->type && !$item->position) { $item = $item->value; } if (!$item instanceof Stub) { return \gettype($item); } if (Stub::TYPE_STRING === $item->type) { return 'string'; } if (Stub::TYPE_ARRAY === $item->type) { return 'array'; } if (Stub::TYPE_OBJECT === $item->type) { return $item->class; } if (Stub::TYPE_RESOURCE === $item->type) { return $item->class.' resource'; } return null; } /** * Returns a native representation of the original value. * * @param array|bool $recursive Whether values should be resolved recursively or not * * @return string|int|float|bool|array|Data[]|null */ public function getValue(array|bool $recursive = false): string|int|float|bool|array|null { $item = $this->data[$this->position][$this->key]; if ($item instanceof Stub && Stub::TYPE_REF === $item->type && !$item->position) { $item = $item->value; } if (!($item = $this->getStub($item)) instanceof Stub) { return $item; } if (Stub::TYPE_STRING === $item->type) { return $item->value; } $children = $item->position ? $this->data[$item->position] : []; foreach ($children as $k => $v) { if ($recursive && !($v = $this->getStub($v)) instanceof Stub) { continue; } $children[$k] = clone $this; $children[$k]->key = $k; $children[$k]->position = $item->position; if ($recursive) { if (Stub::TYPE_REF === $v->type && ($v = $this->getStub($v->value)) instanceof Stub) { $recursive = (array) $recursive; if (isset($recursive[$v->position])) { continue; } $recursive[$v->position] = true; } $children[$k] = $children[$k]->getValue($recursive); } } return $children; } public function count(): int { return \count($this->getValue()); } public function getIterator(): \Traversable { if (!\is_array($value = $this->getValue())) { throw new \LogicException(sprintf('"%s" object holds non-iterable type "%s".', self::class, get_debug_type($value))); } yield from $value; } public function __get(string $key) { if (null !== $data = $this->seek($key)) { $item = $this->getStub($data->data[$data->position][$data->key]); return $item instanceof Stub || [] === $item ? $data : $item; } return null; } public function __isset(string $key): bool { return null !== $this->seek($key); } public function offsetExists(mixed $key): bool { return $this->__isset($key); } public function offsetGet(mixed $key): mixed { return $this->__get($key); } public function offsetSet(mixed $key, mixed $value): void { throw new \BadMethodCallException(self::class.' objects are immutable.'); } public function offsetUnset(mixed $key): void { throw new \BadMethodCallException(self::class.' objects are immutable.'); } public function __toString(): string { $value = $this->getValue(); if (!\is_array($value)) { return (string) $value; } return sprintf('%s (count=%d)', $this->getType(), \count($value)); } /** * Returns a depth limited clone of $this. */ public function withMaxDepth(int $maxDepth): static { $data = clone $this; $data->maxDepth = $maxDepth; return $data; } /** * Limits the number of elements per depth level. */ public function withMaxItemsPerDepth(int $maxItemsPerDepth): static { $data = clone $this; $data->maxItemsPerDepth = $maxItemsPerDepth; return $data; } /** * Enables/disables objects' identifiers tracking. * * @param bool $useRefHandles False to hide global ref. handles */ public function withRefHandles(bool $useRefHandles): static { $data = clone $this; $data->useRefHandles = $useRefHandles ? -1 : 0; return $data; } public function withContext(array $context): static { $data = clone $this; $data->context = $context; return $data; } /** * Seeks to a specific key in nested data structures. */ public function seek(string|int $key): ?static { $item = $this->data[$this->position][$this->key]; if ($item instanceof Stub && Stub::TYPE_REF === $item->type && !$item->position) { $item = $item->value; } if (!($item = $this->getStub($item)) instanceof Stub || !$item->position) { return null; } $keys = [$key]; switch ($item->type) { case Stub::TYPE_OBJECT: $keys[] = Caster::PREFIX_DYNAMIC.$key; $keys[] = Caster::PREFIX_PROTECTED.$key; $keys[] = Caster::PREFIX_VIRTUAL.$key; $keys[] = "\0$item->class\0$key"; // no break case Stub::TYPE_ARRAY: case Stub::TYPE_RESOURCE: break; default: return null; } $data = null; $children = $this->data[$item->position]; foreach ($keys as $key) { if (isset($children[$key]) || \array_key_exists($key, $children)) { $data = clone $this; $data->key = $key; $data->position = $item->position; break; } } return $data; } /** * Dumps data with a DumperInterface dumper. */ public function dump(DumperInterface $dumper) { $refs = [0]; $cursor = new Cursor(); if ($cursor->attr = $this->context[SourceContextProvider::class] ?? []) { $cursor->attr['if_links'] = true; $cursor->hashType = -1; $dumper->dumpScalar($cursor, 'default', '^'); $cursor->attr = ['if_links' => true]; $dumper->dumpScalar($cursor, 'default', ' '); $cursor->hashType = 0; } $this->dumpItem($dumper, $cursor, $refs, $this->data[$this->position][$this->key]); } /** * Depth-first dumping of items. * * @param mixed $item A Stub object or the original value being dumped */ private function dumpItem(DumperInterface $dumper, Cursor $cursor, array &$refs, mixed $item) { $cursor->refIndex = 0; $cursor->softRefTo = $cursor->softRefHandle = $cursor->softRefCount = 0; $cursor->hardRefTo = $cursor->hardRefHandle = $cursor->hardRefCount = 0; $firstSeen = true; if (!$item instanceof Stub) { $cursor->attr = []; $type = \gettype($item); if ($item && 'array' === $type) { $item = $this->getStub($item); } } elseif (Stub::TYPE_REF === $item->type) { if ($item->handle) { if (!isset($refs[$r = $item->handle - (\PHP_INT_MAX >> 1)])) { $cursor->refIndex = $refs[$r] = $cursor->refIndex ?: ++$refs[0]; } else { $firstSeen = false; } $cursor->hardRefTo = $refs[$r]; $cursor->hardRefHandle = $this->useRefHandles & $item->handle; $cursor->hardRefCount = 0 < $item->handle ? $item->refCount : 0; } $cursor->attr = $item->attr; $type = $item->class ?: \gettype($item->value); $item = $this->getStub($item->value); } if ($item instanceof Stub) { if ($item->refCount) { if (!isset($refs[$r = $item->handle])) { $cursor->refIndex = $refs[$r] = $cursor->refIndex ?: ++$refs[0]; } else { $firstSeen = false; } $cursor->softRefTo = $refs[$r]; } $cursor->softRefHandle = $this->useRefHandles & $item->handle; $cursor->softRefCount = $item->refCount; $cursor->attr = $item->attr; $cut = $item->cut; if ($item->position && $firstSeen) { $children = $this->data[$item->position]; if ($cursor->stop) { if ($cut >= 0) { $cut += \count($children); } $children = []; } } else { $children = []; } switch ($item->type) { case Stub::TYPE_STRING: $dumper->dumpString($cursor, $item->value, Stub::STRING_BINARY === $item->class, $cut); break; case Stub::TYPE_ARRAY: $item = clone $item; $item->type = $item->class; $item->class = $item->value; // no break case Stub::TYPE_OBJECT: case Stub::TYPE_RESOURCE: $withChildren = $children && $cursor->depth !== $this->maxDepth && $this->maxItemsPerDepth; $dumper->enterHash($cursor, $item->type, $item->class, $withChildren); if ($withChildren) { if ($cursor->skipChildren) { $withChildren = false; $cut = -1; } else { $cut = $this->dumpChildren($dumper, $cursor, $refs, $children, $cut, $item->type, null !== $item->class); } } elseif ($children && 0 <= $cut) { $cut += \count($children); } $cursor->skipChildren = false; $dumper->leaveHash($cursor, $item->type, $item->class, $withChildren, $cut); break; default: throw new \RuntimeException(sprintf('Unexpected Stub type: "%s".', $item->type)); } } elseif ('array' === $type) { $dumper->enterHash($cursor, Cursor::HASH_INDEXED, 0, false); $dumper->leaveHash($cursor, Cursor::HASH_INDEXED, 0, false, 0); } elseif ('string' === $type) { $dumper->dumpString($cursor, $item, false, 0); } else { $dumper->dumpScalar($cursor, $type, $item); } } /** * Dumps children of hash structures. * * @return int The final number of removed items */ private function dumpChildren(DumperInterface $dumper, Cursor $parentCursor, array &$refs, array $children, int $hashCut, int $hashType, bool $dumpKeys): int { $cursor = clone $parentCursor; ++$cursor->depth; $cursor->hashType = $hashType; $cursor->hashIndex = 0; $cursor->hashLength = \count($children); $cursor->hashCut = $hashCut; foreach ($children as $key => $child) { $cursor->hashKeyIsBinary = isset($key[0]) && !preg_match('//u', $key); $cursor->hashKey = $dumpKeys ? $key : null; $this->dumpItem($dumper, $cursor, $refs, $child); if (++$cursor->hashIndex === $this->maxItemsPerDepth || $cursor->stop) { $parentCursor->stop = true; return $hashCut >= 0 ? $hashCut + $cursor->hashLength - $cursor->hashIndex : $hashCut; } } return $hashCut; } private function getStub(mixed $item) { if (!$item || !\is_array($item)) { return $item; } $stub = new Stub(); $stub->type = Stub::TYPE_ARRAY; foreach ($item as $stub->class => $stub->position) { } if (isset($item[0])) { $stub->cut = $item[0]; } $stub->value = $stub->cut + ($stub->position ? \count($this->data[$stub->position]) : 0); return $stub; } } var-dumper/Cloner/AbstractCloner.php 0000644 00000050612 15025017654 0013500 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; use Symfony\Component\VarDumper\Caster\Caster; use Symfony\Component\VarDumper\Exception\ThrowingCasterException; /** * AbstractCloner implements a generic caster mechanism for objects and resources. * * @author Nicolas Grekas <p@tchwork.com> */ abstract class AbstractCloner implements ClonerInterface { public static $defaultCasters = [ '__PHP_Incomplete_Class' => ['Symfony\Component\VarDumper\Caster\Caster', 'castPhpIncompleteClass'], 'Symfony\Component\VarDumper\Caster\CutStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castStub'], 'Symfony\Component\VarDumper\Caster\CutArrayStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castCutArray'], 'Symfony\Component\VarDumper\Caster\ConstStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castStub'], 'Symfony\Component\VarDumper\Caster\EnumStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castEnum'], 'Fiber' => ['Symfony\Component\VarDumper\Caster\FiberCaster', 'castFiber'], 'Closure' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castClosure'], 'Generator' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castGenerator'], 'ReflectionType' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castType'], 'ReflectionAttribute' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castAttribute'], 'ReflectionGenerator' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castReflectionGenerator'], 'ReflectionClass' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castClass'], 'ReflectionClassConstant' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castClassConstant'], 'ReflectionFunctionAbstract' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castFunctionAbstract'], 'ReflectionMethod' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castMethod'], 'ReflectionParameter' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castParameter'], 'ReflectionProperty' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castProperty'], 'ReflectionReference' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castReference'], 'ReflectionExtension' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castExtension'], 'ReflectionZendExtension' => ['Symfony\Component\VarDumper\Caster\ReflectionCaster', 'castZendExtension'], 'Doctrine\Common\Persistence\ObjectManager' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], 'Doctrine\Common\Proxy\Proxy' => ['Symfony\Component\VarDumper\Caster\DoctrineCaster', 'castCommonProxy'], 'Doctrine\ORM\Proxy\Proxy' => ['Symfony\Component\VarDumper\Caster\DoctrineCaster', 'castOrmProxy'], 'Doctrine\ORM\PersistentCollection' => ['Symfony\Component\VarDumper\Caster\DoctrineCaster', 'castPersistentCollection'], 'Doctrine\Persistence\ObjectManager' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], 'DOMException' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castException'], 'DOMStringList' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLength'], 'DOMNameList' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLength'], 'DOMImplementation' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castImplementation'], 'DOMImplementationList' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLength'], 'DOMNode' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castNode'], 'DOMNameSpaceNode' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castNameSpaceNode'], 'DOMDocument' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castDocument'], 'DOMNodeList' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLength'], 'DOMNamedNodeMap' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLength'], 'DOMCharacterData' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castCharacterData'], 'DOMAttr' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castAttr'], 'DOMElement' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castElement'], 'DOMText' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castText'], 'DOMTypeinfo' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castTypeinfo'], 'DOMDomError' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castDomError'], 'DOMLocator' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castLocator'], 'DOMDocumentType' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castDocumentType'], 'DOMNotation' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castNotation'], 'DOMEntity' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castEntity'], 'DOMProcessingInstruction' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castProcessingInstruction'], 'DOMXPath' => ['Symfony\Component\VarDumper\Caster\DOMCaster', 'castXPath'], 'XMLReader' => ['Symfony\Component\VarDumper\Caster\XmlReaderCaster', 'castXmlReader'], 'ErrorException' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castErrorException'], 'Exception' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castException'], 'Error' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castError'], 'Symfony\Bridge\Monolog\Logger' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], 'Symfony\Component\DependencyInjection\ContainerInterface' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], 'Symfony\Component\EventDispatcher\EventDispatcherInterface' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], 'Symfony\Component\HttpClient\AmpHttpClient' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castHttpClient'], 'Symfony\Component\HttpClient\CurlHttpClient' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castHttpClient'], 'Symfony\Component\HttpClient\NativeHttpClient' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castHttpClient'], 'Symfony\Component\HttpClient\Response\AmpResponse' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castHttpClientResponse'], 'Symfony\Component\HttpClient\Response\CurlResponse' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castHttpClientResponse'], 'Symfony\Component\HttpClient\Response\NativeResponse' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castHttpClientResponse'], 'Symfony\Component\HttpFoundation\Request' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castRequest'], 'Symfony\Component\Uid\Ulid' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castUlid'], 'Symfony\Component\Uid\Uuid' => ['Symfony\Component\VarDumper\Caster\SymfonyCaster', 'castUuid'], 'Symfony\Component\VarDumper\Exception\ThrowingCasterException' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castThrowingCasterException'], 'Symfony\Component\VarDumper\Caster\TraceStub' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castTraceStub'], 'Symfony\Component\VarDumper\Caster\FrameStub' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castFrameStub'], 'Symfony\Component\VarDumper\Cloner\AbstractCloner' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], 'Symfony\Component\ErrorHandler\Exception\SilencedErrorContext' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castSilencedErrorContext'], 'Imagine\Image\ImageInterface' => ['Symfony\Component\VarDumper\Caster\ImagineCaster', 'castImage'], 'Ramsey\Uuid\UuidInterface' => ['Symfony\Component\VarDumper\Caster\UuidCaster', 'castRamseyUuid'], 'ProxyManager\Proxy\ProxyInterface' => ['Symfony\Component\VarDumper\Caster\ProxyManagerCaster', 'castProxy'], 'PHPUnit_Framework_MockObject_MockObject' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], 'PHPUnit\Framework\MockObject\MockObject' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], 'PHPUnit\Framework\MockObject\Stub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], 'Prophecy\Prophecy\ProphecySubjectInterface' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], 'Mockery\MockInterface' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], 'PDO' => ['Symfony\Component\VarDumper\Caster\PdoCaster', 'castPdo'], 'PDOStatement' => ['Symfony\Component\VarDumper\Caster\PdoCaster', 'castPdoStatement'], 'AMQPConnection' => ['Symfony\Component\VarDumper\Caster\AmqpCaster', 'castConnection'], 'AMQPChannel' => ['Symfony\Component\VarDumper\Caster\AmqpCaster', 'castChannel'], 'AMQPQueue' => ['Symfony\Component\VarDumper\Caster\AmqpCaster', 'castQueue'], 'AMQPExchange' => ['Symfony\Component\VarDumper\Caster\AmqpCaster', 'castExchange'], 'AMQPEnvelope' => ['Symfony\Component\VarDumper\Caster\AmqpCaster', 'castEnvelope'], 'ArrayObject' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castArrayObject'], 'ArrayIterator' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castArrayIterator'], 'SplDoublyLinkedList' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castDoublyLinkedList'], 'SplFileInfo' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castFileInfo'], 'SplFileObject' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castFileObject'], 'SplHeap' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castHeap'], 'SplObjectStorage' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castObjectStorage'], 'SplPriorityQueue' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castHeap'], 'OuterIterator' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castOuterIterator'], 'WeakReference' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castWeakReference'], 'Redis' => ['Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedis'], 'RedisArray' => ['Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedisArray'], 'RedisCluster' => ['Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedisCluster'], 'DateTimeInterface' => ['Symfony\Component\VarDumper\Caster\DateCaster', 'castDateTime'], 'DateInterval' => ['Symfony\Component\VarDumper\Caster\DateCaster', 'castInterval'], 'DateTimeZone' => ['Symfony\Component\VarDumper\Caster\DateCaster', 'castTimeZone'], 'DatePeriod' => ['Symfony\Component\VarDumper\Caster\DateCaster', 'castPeriod'], 'GMP' => ['Symfony\Component\VarDumper\Caster\GmpCaster', 'castGmp'], 'MessageFormatter' => ['Symfony\Component\VarDumper\Caster\IntlCaster', 'castMessageFormatter'], 'NumberFormatter' => ['Symfony\Component\VarDumper\Caster\IntlCaster', 'castNumberFormatter'], 'IntlTimeZone' => ['Symfony\Component\VarDumper\Caster\IntlCaster', 'castIntlTimeZone'], 'IntlCalendar' => ['Symfony\Component\VarDumper\Caster\IntlCaster', 'castIntlCalendar'], 'IntlDateFormatter' => ['Symfony\Component\VarDumper\Caster\IntlCaster', 'castIntlDateFormatter'], 'Memcached' => ['Symfony\Component\VarDumper\Caster\MemcachedCaster', 'castMemcached'], 'Ds\Collection' => ['Symfony\Component\VarDumper\Caster\DsCaster', 'castCollection'], 'Ds\Map' => ['Symfony\Component\VarDumper\Caster\DsCaster', 'castMap'], 'Ds\Pair' => ['Symfony\Component\VarDumper\Caster\DsCaster', 'castPair'], 'Symfony\Component\VarDumper\Caster\DsPairStub' => ['Symfony\Component\VarDumper\Caster\DsCaster', 'castPairStub'], 'mysqli_driver' => ['Symfony\Component\VarDumper\Caster\MysqliCaster', 'castMysqliDriver'], 'CurlHandle' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castCurl'], ':dba' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'], ':dba persistent' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'], 'GdImage' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castGd'], ':gd' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castGd'], ':mysql link' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castMysqlLink'], ':pgsql large object' => ['Symfony\Component\VarDumper\Caster\PgSqlCaster', 'castLargeObject'], ':pgsql link' => ['Symfony\Component\VarDumper\Caster\PgSqlCaster', 'castLink'], ':pgsql link persistent' => ['Symfony\Component\VarDumper\Caster\PgSqlCaster', 'castLink'], ':pgsql result' => ['Symfony\Component\VarDumper\Caster\PgSqlCaster', 'castResult'], ':process' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castProcess'], ':stream' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castStream'], 'OpenSSLCertificate' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castOpensslX509'], ':OpenSSL X.509' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castOpensslX509'], ':persistent stream' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castStream'], ':stream-context' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castStreamContext'], 'XmlParser' => ['Symfony\Component\VarDumper\Caster\XmlResourceCaster', 'castXml'], ':xml' => ['Symfony\Component\VarDumper\Caster\XmlResourceCaster', 'castXml'], 'RdKafka' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castRdKafka'], 'RdKafka\Conf' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castConf'], 'RdKafka\KafkaConsumer' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castKafkaConsumer'], 'RdKafka\Metadata\Broker' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castBrokerMetadata'], 'RdKafka\Metadata\Collection' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castCollectionMetadata'], 'RdKafka\Metadata\Partition' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castPartitionMetadata'], 'RdKafka\Metadata\Topic' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castTopicMetadata'], 'RdKafka\Message' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castMessage'], 'RdKafka\Topic' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castTopic'], 'RdKafka\TopicPartition' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castTopicPartition'], 'RdKafka\TopicConf' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castTopicConf'], ]; protected $maxItems = 2500; protected $maxString = -1; protected $minDepth = 1; /** * @var array<string, list<callable>> */ private array $casters = []; /** * @var callable|null */ private $prevErrorHandler; private array $classInfo = []; private int $filter = 0; /** * @param callable[]|null $casters A map of casters * * @see addCasters */ public function __construct(array $casters = null) { if (null === $casters) { $casters = static::$defaultCasters; } $this->addCasters($casters); } /** * Adds casters for resources and objects. * * Maps resources or objects types to a callback. * Types are in the key, with a callable caster for value. * Resource types are to be prefixed with a `:`, * see e.g. static::$defaultCasters. * * @param callable[] $casters A map of casters */ public function addCasters(array $casters) { foreach ($casters as $type => $callback) { $this->casters[$type][] = $callback; } } /** * Sets the maximum number of items to clone past the minimum depth in nested structures. */ public function setMaxItems(int $maxItems) { $this->maxItems = $maxItems; } /** * Sets the maximum cloned length for strings. */ public function setMaxString(int $maxString) { $this->maxString = $maxString; } /** * Sets the minimum tree depth where we are guaranteed to clone all the items. After this * depth is reached, only setMaxItems items will be cloned. */ public function setMinDepth(int $minDepth) { $this->minDepth = $minDepth; } /** * Clones a PHP variable. * * @param int $filter A bit field of Caster::EXCLUDE_* constants */ public function cloneVar(mixed $var, int $filter = 0): Data { $this->prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) { if (\E_RECOVERABLE_ERROR === $type || \E_USER_ERROR === $type) { // Cloner never dies throw new \ErrorException($msg, 0, $type, $file, $line); } if ($this->prevErrorHandler) { return ($this->prevErrorHandler)($type, $msg, $file, $line, $context); } return false; }); $this->filter = $filter; if ($gc = gc_enabled()) { gc_disable(); } try { return new Data($this->doClone($var)); } finally { if ($gc) { gc_enable(); } restore_error_handler(); $this->prevErrorHandler = null; } } /** * Effectively clones the PHP variable. */ abstract protected function doClone(mixed $var): array; /** * Casts an object to an array representation. * * @param bool $isNested True if the object is nested in the dumped structure */ protected function castObject(Stub $stub, bool $isNested): array { $obj = $stub->value; $class = $stub->class; if (str_contains($class, "@anonymous\0")) { $stub->class = get_debug_type($obj); } if (isset($this->classInfo[$class])) { [$i, $parents, $hasDebugInfo, $fileInfo] = $this->classInfo[$class]; } else { $i = 2; $parents = [$class]; $hasDebugInfo = method_exists($class, '__debugInfo'); foreach (class_parents($class) as $p) { $parents[] = $p; ++$i; } foreach (class_implements($class) as $p) { $parents[] = $p; ++$i; } $parents[] = '*'; $r = new \ReflectionClass($class); $fileInfo = $r->isInternal() || $r->isSubclassOf(Stub::class) ? [] : [ 'file' => $r->getFileName(), 'line' => $r->getStartLine(), ]; $this->classInfo[$class] = [$i, $parents, $hasDebugInfo, $fileInfo]; } $stub->attr += $fileInfo; $a = Caster::castObject($obj, $class, $hasDebugInfo, $stub->class); try { while ($i--) { if (!empty($this->casters[$p = $parents[$i]])) { foreach ($this->casters[$p] as $callback) { $a = $callback($obj, $a, $stub, $isNested, $this->filter); } } } } catch (\Exception $e) { $a = [(Stub::TYPE_OBJECT === $stub->type ? Caster::PREFIX_VIRTUAL : '').'⚠' => new ThrowingCasterException($e)] + $a; } return $a; } /** * Casts a resource to an array representation. * * @param bool $isNested True if the object is nested in the dumped structure */ protected function castResource(Stub $stub, bool $isNested): array { $a = []; $res = $stub->value; $type = $stub->class; try { if (!empty($this->casters[':'.$type])) { foreach ($this->casters[':'.$type] as $callback) { $a = $callback($res, $a, $stub, $isNested, $this->filter); } } } catch (\Exception $e) { $a = [(Stub::TYPE_OBJECT === $stub->type ? Caster::PREFIX_VIRTUAL : '').'⚠' => new ThrowingCasterException($e)] + $a; } return $a; } } var-dumper/Cloner/ClonerInterface.php 0000644 00000000713 15025017654 0013632 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; /** * @author Nicolas Grekas <p@tchwork.com> */ interface ClonerInterface { /** * Clones a PHP variable. */ public function cloneVar(mixed $var): Data; } var-dumper/Cloner/Cursor.php 0000644 00000002071 15025017654 0012043 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; /** * Represents the current state of a dumper while dumping. * * @author Nicolas Grekas <p@tchwork.com> */ class Cursor { public const HASH_INDEXED = Stub::ARRAY_INDEXED; public const HASH_ASSOC = Stub::ARRAY_ASSOC; public const HASH_OBJECT = Stub::TYPE_OBJECT; public const HASH_RESOURCE = Stub::TYPE_RESOURCE; public $depth = 0; public $refIndex = 0; public $softRefTo = 0; public $softRefCount = 0; public $softRefHandle = 0; public $hardRefTo = 0; public $hardRefCount = 0; public $hardRefHandle = 0; public $hashType; public $hashKey; public $hashKeyIsBinary; public $hashIndex = 0; public $hashLength = 0; public $hashCut = 0; public $stop = false; public $attr = []; public $skipChildren = false; } var-dumper/Cloner/Stub.php 0000644 00000003032 15025017654 0011501 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; /** * Represents the main properties of a PHP variable. * * @author Nicolas Grekas <p@tchwork.com> */ class Stub { public const TYPE_REF = 1; public const TYPE_STRING = 2; public const TYPE_ARRAY = 3; public const TYPE_OBJECT = 4; public const TYPE_RESOURCE = 5; public const STRING_BINARY = 1; public const STRING_UTF8 = 2; public const ARRAY_ASSOC = 1; public const ARRAY_INDEXED = 2; public $type = self::TYPE_REF; public $class = ''; public $value; public $cut = 0; public $handle = 0; public $refCount = 0; public $position = 0; public $attr = []; private static array $defaultProperties = []; /** * @internal */ public function __sleep(): array { $properties = []; if (!isset(self::$defaultProperties[$c = static::class])) { self::$defaultProperties[$c] = get_class_vars($c); foreach ((new \ReflectionClass($c))->getStaticProperties() as $k => $v) { unset(self::$defaultProperties[$c][$k]); } } foreach (self::$defaultProperties[$c] as $k => $v) { if ($this->$k !== $v) { $properties[] = $k; } } return $properties; } } var-dumper/Cloner/DumperInterface.php 0000644 00000003370 15025017654 0013646 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; /** * DumperInterface used by Data objects. * * @author Nicolas Grekas <p@tchwork.com> */ interface DumperInterface { /** * Dumps a scalar value. */ public function dumpScalar(Cursor $cursor, string $type, string|int|float|bool|null $value); /** * Dumps a string. * * @param string $str The string being dumped * @param bool $bin Whether $str is UTF-8 or binary encoded * @param int $cut The number of characters $str has been cut by */ public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut); /** * Dumps while entering an hash. * * @param int $type A Cursor::HASH_* const for the type of hash * @param string|int|null $class The object class, resource type or array count * @param bool $hasChild When the dump of the hash has child item */ public function enterHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild); /** * Dumps while leaving an hash. * * @param int $type A Cursor::HASH_* const for the type of hash * @param string|int|null $class The object class, resource type or array count * @param bool $hasChild When the dump of the hash has child item * @param int $cut The number of items the hash has been cut by */ public function leaveHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild, int $cut); } var-dumper/Cloner/VarCloner.php 0000644 00000030546 15025017654 0012471 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; /** * @author Nicolas Grekas <p@tchwork.com> */ class VarCloner extends AbstractCloner { private static string $gid; private static array $arrayCache = []; /** * {@inheritdoc} */ protected function doClone(mixed $var): array { $len = 1; // Length of $queue $pos = 0; // Number of cloned items past the minimum depth $refsCounter = 0; // Hard references counter $queue = [[$var]]; // This breadth-first queue is the return value $hardRefs = []; // Map of original zval ids to stub objects $objRefs = []; // Map of original object handles to their stub object counterpart $objects = []; // Keep a ref to objects to ensure their handle cannot be reused while cloning $resRefs = []; // Map of original resource handles to their stub object counterpart $values = []; // Map of stub objects' ids to original values $maxItems = $this->maxItems; $maxString = $this->maxString; $minDepth = $this->minDepth; $currentDepth = 0; // Current tree depth $currentDepthFinalIndex = 0; // Final $queue index for current tree depth $minimumDepthReached = 0 === $minDepth; // Becomes true when minimum tree depth has been reached $cookie = (object) []; // Unique object used to detect hard references $a = null; // Array cast for nested structures $stub = null; // Stub capturing the main properties of an original item value // or null if the original value is used directly $gid = self::$gid ??= md5(random_bytes(6)); // Unique string used to detect the special $GLOBALS variable $arrayStub = new Stub(); $arrayStub->type = Stub::TYPE_ARRAY; $fromObjCast = false; for ($i = 0; $i < $len; ++$i) { // Detect when we move on to the next tree depth if ($i > $currentDepthFinalIndex) { ++$currentDepth; $currentDepthFinalIndex = $len - 1; if ($currentDepth >= $minDepth) { $minimumDepthReached = true; } } $refs = $vals = $queue[$i]; foreach ($vals as $k => $v) { // $v is the original value or a stub object in case of hard references $zvalRef = ($r = \ReflectionReference::fromArrayElement($vals, $k)) ? $r->getId() : null; if ($zvalRef) { $vals[$k] = &$stub; // Break hard references to make $queue completely unset($stub); // independent from the original structure if (null !== $vals[$k] = $hardRefs[$zvalRef] ?? null) { $v = $vals[$k]; if ($v->value instanceof Stub && (Stub::TYPE_OBJECT === $v->value->type || Stub::TYPE_RESOURCE === $v->value->type)) { ++$v->value->refCount; } ++$v->refCount; continue; } $vals[$k] = new Stub(); $vals[$k]->value = $v; $vals[$k]->handle = ++$refsCounter; $hardRefs[$zvalRef] = $vals[$k]; } // Create $stub when the original value $v cannot be used directly // If $v is a nested structure, put that structure in array $a switch (true) { case null === $v: case \is_bool($v): case \is_int($v): case \is_float($v): continue 2; case \is_string($v): if ('' === $v) { continue 2; } if (!preg_match('//u', $v)) { $stub = new Stub(); $stub->type = Stub::TYPE_STRING; $stub->class = Stub::STRING_BINARY; if (0 <= $maxString && 0 < $cut = \strlen($v) - $maxString) { $stub->cut = $cut; $stub->value = substr($v, 0, -$cut); } else { $stub->value = $v; } } elseif (0 <= $maxString && isset($v[1 + ($maxString >> 2)]) && 0 < $cut = mb_strlen($v, 'UTF-8') - $maxString) { $stub = new Stub(); $stub->type = Stub::TYPE_STRING; $stub->class = Stub::STRING_UTF8; $stub->cut = $cut; $stub->value = mb_substr($v, 0, $maxString, 'UTF-8'); } else { continue 2; } $a = null; break; case \is_array($v): if (!$v) { continue 2; } $stub = $arrayStub; if (\PHP_VERSION_ID >= 80100) { $stub->class = array_is_list($v) ? Stub::ARRAY_INDEXED : Stub::ARRAY_ASSOC; $a = $v; break; } $stub->class = Stub::ARRAY_INDEXED; $j = -1; foreach ($v as $gk => $gv) { if ($gk !== ++$j) { $stub->class = Stub::ARRAY_ASSOC; $a = $v; $a[$gid] = true; break; } } // Copies of $GLOBALS have very strange behavior, // let's detect them with some black magic if (isset($v[$gid])) { unset($v[$gid]); $a = []; foreach ($v as $gk => &$gv) { if ($v === $gv && !isset($hardRefs[\ReflectionReference::fromArrayElement($v, $gk)->getId()])) { unset($v); $v = new Stub(); $v->value = [$v->cut = \count($gv), Stub::TYPE_ARRAY => 0]; $v->handle = -1; $gv = &$a[$gk]; $hardRefs[\ReflectionReference::fromArrayElement($a, $gk)->getId()] = &$gv; $gv = $v; } $a[$gk] = &$gv; } unset($gv); } else { $a = $v; } break; case \is_object($v): if (empty($objRefs[$h = spl_object_id($v)])) { $stub = new Stub(); $stub->type = Stub::TYPE_OBJECT; $stub->class = \get_class($v); $stub->value = $v; $stub->handle = $h; $a = $this->castObject($stub, 0 < $i); if ($v !== $stub->value) { if (Stub::TYPE_OBJECT !== $stub->type || null === $stub->value) { break; } $stub->handle = $h = spl_object_id($stub->value); } $stub->value = null; if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) { $stub->cut = \count($a); $a = null; } } if (empty($objRefs[$h])) { $objRefs[$h] = $stub; $objects[] = $v; } else { $stub = $objRefs[$h]; ++$stub->refCount; $a = null; } break; default: // resource if (empty($resRefs[$h = (int) $v])) { $stub = new Stub(); $stub->type = Stub::TYPE_RESOURCE; if ('Unknown' === $stub->class = @get_resource_type($v)) { $stub->class = 'Closed'; } $stub->value = $v; $stub->handle = $h; $a = $this->castResource($stub, 0 < $i); $stub->value = null; if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) { $stub->cut = \count($a); $a = null; } } if (empty($resRefs[$h])) { $resRefs[$h] = $stub; } else { $stub = $resRefs[$h]; ++$stub->refCount; $a = null; } break; } if ($a) { if (!$minimumDepthReached || 0 > $maxItems) { $queue[$len] = $a; $stub->position = $len++; } elseif ($pos < $maxItems) { if ($maxItems < $pos += \count($a)) { $a = \array_slice($a, 0, $maxItems - $pos, true); if ($stub->cut >= 0) { $stub->cut += $pos - $maxItems; } } $queue[$len] = $a; $stub->position = $len++; } elseif ($stub->cut >= 0) { $stub->cut += \count($a); $stub->position = 0; } } if ($arrayStub === $stub) { if ($arrayStub->cut) { $stub = [$arrayStub->cut, $arrayStub->class => $arrayStub->position]; $arrayStub->cut = 0; } elseif (isset(self::$arrayCache[$arrayStub->class][$arrayStub->position])) { $stub = self::$arrayCache[$arrayStub->class][$arrayStub->position]; } else { self::$arrayCache[$arrayStub->class][$arrayStub->position] = $stub = [$arrayStub->class => $arrayStub->position]; } } if (!$zvalRef) { $vals[$k] = $stub; } else { $hardRefs[$zvalRef]->value = $stub; } } if ($fromObjCast) { $fromObjCast = false; $refs = $vals; $vals = []; $j = -1; foreach ($queue[$i] as $k => $v) { foreach ([$k => true] as $gk => $gv) { } if ($gk !== $k) { $vals = (object) $vals; $vals->{$k} = $refs[++$j]; $vals = (array) $vals; } else { $vals[$k] = $refs[++$j]; } } } $queue[$i] = $vals; } foreach ($values as $h => $v) { $hardRefs[$h] = $v; } return $queue; } } var-dumper/Dumper/CliDumper.php 0000644 00000052374 15025017654 0012477 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper; use Symfony\Component\VarDumper\Cloner\Cursor; use Symfony\Component\VarDumper\Cloner\Stub; /** * CliDumper dumps variables for command line output. * * @author Nicolas Grekas <p@tchwork.com> */ class CliDumper extends AbstractDumper { public static $defaultColors; public static $defaultOutput = 'php://stdout'; protected $colors; protected $maxStringWidth = 0; protected $styles = [ // See http://en.wikipedia.org/wiki/ANSI_escape_code#graphics 'default' => '0;38;5;208', 'num' => '1;38;5;38', 'const' => '1;38;5;208', 'str' => '1;38;5;113', 'note' => '38;5;38', 'ref' => '38;5;247', 'public' => '', 'protected' => '', 'private' => '', 'meta' => '38;5;170', 'key' => '38;5;113', 'index' => '38;5;38', ]; protected static $controlCharsRx = '/[\x00-\x1F\x7F]+/'; protected static $controlCharsMap = [ "\t" => '\t', "\n" => '\n', "\v" => '\v', "\f" => '\f', "\r" => '\r', "\033" => '\e', ]; protected $collapseNextHash = false; protected $expandNextHash = false; private array $displayOptions = [ 'fileLinkFormat' => null, ]; private bool $handlesHrefGracefully; /** * {@inheritdoc} */ public function __construct($output = null, string $charset = null, int $flags = 0) { parent::__construct($output, $charset, $flags); if ('\\' === \DIRECTORY_SEPARATOR && !$this->isWindowsTrueColor()) { // Use only the base 16 xterm colors when using ANSICON or standard Windows 10 CLI $this->setStyles([ 'default' => '31', 'num' => '1;34', 'const' => '1;31', 'str' => '1;32', 'note' => '34', 'ref' => '1;30', 'meta' => '35', 'key' => '32', 'index' => '34', ]); } $this->displayOptions['fileLinkFormat'] = \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: 'file://%f#L%l'; } /** * Enables/disables colored output. */ public function setColors(bool $colors) { $this->colors = $colors; } /** * Sets the maximum number of characters per line for dumped strings. */ public function setMaxStringWidth(int $maxStringWidth) { $this->maxStringWidth = $maxStringWidth; } /** * Configures styles. * * @param array $styles A map of style names to style definitions */ public function setStyles(array $styles) { $this->styles = $styles + $this->styles; } /** * Configures display options. * * @param array $displayOptions A map of display options to customize the behavior */ public function setDisplayOptions(array $displayOptions) { $this->displayOptions = $displayOptions + $this->displayOptions; } /** * {@inheritdoc} */ public function dumpScalar(Cursor $cursor, string $type, string|int|float|bool|null $value) { $this->dumpKey($cursor); $style = 'const'; $attr = $cursor->attr; switch ($type) { case 'default': $style = 'default'; break; case 'integer': $style = 'num'; if (isset($this->styles['integer'])) { $style = 'integer'; } break; case 'double': $style = 'num'; if (isset($this->styles['float'])) { $style = 'float'; } switch (true) { case \INF === $value: $value = 'INF'; break; case -\INF === $value: $value = '-INF'; break; case is_nan($value): $value = 'NAN'; break; default: $value = (string) $value; if (!str_contains($value, $this->decimalPoint)) { $value .= $this->decimalPoint.'0'; } break; } break; case 'NULL': $value = 'null'; break; case 'boolean': $value = $value ? 'true' : 'false'; break; default: $attr += ['value' => $this->utf8Encode($value)]; $value = $this->utf8Encode($type); break; } $this->line .= $this->style($style, $value, $attr); $this->endValue($cursor); } /** * {@inheritdoc} */ public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut) { $this->dumpKey($cursor); $attr = $cursor->attr; if ($bin) { $str = $this->utf8Encode($str); } if ('' === $str) { $this->line .= '""'; $this->endValue($cursor); } else { $attr += [ 'length' => 0 <= $cut ? mb_strlen($str, 'UTF-8') + $cut : 0, 'binary' => $bin, ]; $str = $bin && false !== strpos($str, "\0") ? [$str] : explode("\n", $str); if (isset($str[1]) && !isset($str[2]) && !isset($str[1][0])) { unset($str[1]); $str[0] .= "\n"; } $m = \count($str) - 1; $i = $lineCut = 0; if (self::DUMP_STRING_LENGTH & $this->flags) { $this->line .= '('.$attr['length'].') '; } if ($bin) { $this->line .= 'b'; } if ($m) { $this->line .= '"""'; $this->dumpLine($cursor->depth); } else { $this->line .= '"'; } foreach ($str as $str) { if ($i < $m) { $str .= "\n"; } if (0 < $this->maxStringWidth && $this->maxStringWidth < $len = mb_strlen($str, 'UTF-8')) { $str = mb_substr($str, 0, $this->maxStringWidth, 'UTF-8'); $lineCut = $len - $this->maxStringWidth; } if ($m && 0 < $cursor->depth) { $this->line .= $this->indentPad; } if ('' !== $str) { $this->line .= $this->style('str', $str, $attr); } if ($i++ == $m) { if ($m) { if ('' !== $str) { $this->dumpLine($cursor->depth); if (0 < $cursor->depth) { $this->line .= $this->indentPad; } } $this->line .= '"""'; } else { $this->line .= '"'; } if ($cut < 0) { $this->line .= '…'; $lineCut = 0; } elseif ($cut) { $lineCut += $cut; } } if ($lineCut) { $this->line .= '…'.$lineCut; $lineCut = 0; } if ($i > $m) { $this->endValue($cursor); } else { $this->dumpLine($cursor->depth); } } } } /** * {@inheritdoc} */ public function enterHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild) { if (null === $this->colors) { $this->colors = $this->supportsColors(); } $this->dumpKey($cursor); $attr = $cursor->attr; if ($this->collapseNextHash) { $cursor->skipChildren = true; $this->collapseNextHash = $hasChild = false; } $class = $this->utf8Encode($class); if (Cursor::HASH_OBJECT === $type) { $prefix = $class && 'stdClass' !== $class ? $this->style('note', $class, $attr).(empty($attr['cut_hash']) ? ' {' : '') : '{'; } elseif (Cursor::HASH_RESOURCE === $type) { $prefix = $this->style('note', $class.' resource', $attr).($hasChild ? ' {' : ' '); } else { $prefix = $class && !(self::DUMP_LIGHT_ARRAY & $this->flags) ? $this->style('note', 'array:'.$class).' [' : '['; } if (($cursor->softRefCount || 0 < $cursor->softRefHandle) && empty($attr['cut_hash'])) { $prefix .= $this->style('ref', (Cursor::HASH_RESOURCE === $type ? '@' : '#').(0 < $cursor->softRefHandle ? $cursor->softRefHandle : $cursor->softRefTo), ['count' => $cursor->softRefCount]); } elseif ($cursor->hardRefTo && !$cursor->refIndex && $class) { $prefix .= $this->style('ref', '&'.$cursor->hardRefTo, ['count' => $cursor->hardRefCount]); } elseif (!$hasChild && Cursor::HASH_RESOURCE === $type) { $prefix = substr($prefix, 0, -1); } $this->line .= $prefix; if ($hasChild) { $this->dumpLine($cursor->depth); } } /** * {@inheritdoc} */ public function leaveHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild, int $cut) { if (empty($cursor->attr['cut_hash'])) { $this->dumpEllipsis($cursor, $hasChild, $cut); $this->line .= Cursor::HASH_OBJECT === $type ? '}' : (Cursor::HASH_RESOURCE !== $type ? ']' : ($hasChild ? '}' : '')); } $this->endValue($cursor); } /** * Dumps an ellipsis for cut children. * * @param bool $hasChild When the dump of the hash has child item * @param int $cut The number of items the hash has been cut by */ protected function dumpEllipsis(Cursor $cursor, bool $hasChild, int $cut) { if ($cut) { $this->line .= ' …'; if (0 < $cut) { $this->line .= $cut; } if ($hasChild) { $this->dumpLine($cursor->depth + 1); } } } /** * Dumps a key in a hash structure. */ protected function dumpKey(Cursor $cursor) { if (null !== $key = $cursor->hashKey) { if ($cursor->hashKeyIsBinary) { $key = $this->utf8Encode($key); } $attr = ['binary' => $cursor->hashKeyIsBinary]; $bin = $cursor->hashKeyIsBinary ? 'b' : ''; $style = 'key'; switch ($cursor->hashType) { default: case Cursor::HASH_INDEXED: if (self::DUMP_LIGHT_ARRAY & $this->flags) { break; } $style = 'index'; // no break case Cursor::HASH_ASSOC: if (\is_int($key)) { $this->line .= $this->style($style, $key).' => '; } else { $this->line .= $bin.'"'.$this->style($style, $key).'" => '; } break; case Cursor::HASH_RESOURCE: $key = "\0~\0".$key; // no break case Cursor::HASH_OBJECT: if (!isset($key[0]) || "\0" !== $key[0]) { $this->line .= '+'.$bin.$this->style('public', $key).': '; } elseif (0 < strpos($key, "\0", 1)) { $key = explode("\0", substr($key, 1), 2); switch ($key[0][0]) { case '+': // User inserted keys $attr['dynamic'] = true; $this->line .= '+'.$bin.'"'.$this->style('public', $key[1], $attr).'": '; break 2; case '~': $style = 'meta'; if (isset($key[0][1])) { parse_str(substr($key[0], 1), $attr); $attr += ['binary' => $cursor->hashKeyIsBinary]; } break; case '*': $style = 'protected'; $bin = '#'.$bin; break; default: $attr['class'] = $key[0]; $style = 'private'; $bin = '-'.$bin; break; } if (isset($attr['collapse'])) { if ($attr['collapse']) { $this->collapseNextHash = true; } else { $this->expandNextHash = true; } } $this->line .= $bin.$this->style($style, $key[1], $attr).($attr['separator'] ?? ': '); } else { // This case should not happen $this->line .= '-'.$bin.'"'.$this->style('private', $key, ['class' => '']).'": '; } break; } if ($cursor->hardRefTo) { $this->line .= $this->style('ref', '&'.($cursor->hardRefCount ? $cursor->hardRefTo : ''), ['count' => $cursor->hardRefCount]).' '; } } } /** * Decorates a value with some style. * * @param string $style The type of style being applied * @param string $value The value being styled * @param array $attr Optional context information */ protected function style(string $style, string $value, array $attr = []): string { if (null === $this->colors) { $this->colors = $this->supportsColors(); } $this->handlesHrefGracefully ??= 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') && (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100); if (isset($attr['ellipsis'], $attr['ellipsis-type'])) { $prefix = substr($value, 0, -$attr['ellipsis']); if ('cli' === \PHP_SAPI && 'path' === $attr['ellipsis-type'] && isset($_SERVER[$pwd = '\\' === \DIRECTORY_SEPARATOR ? 'CD' : 'PWD']) && str_starts_with($prefix, $_SERVER[$pwd])) { $prefix = '.'.substr($prefix, \strlen($_SERVER[$pwd])); } if (!empty($attr['ellipsis-tail'])) { $prefix .= substr($value, -$attr['ellipsis'], $attr['ellipsis-tail']); $value = substr($value, -$attr['ellipsis'] + $attr['ellipsis-tail']); } else { $value = substr($value, -$attr['ellipsis']); } $value = $this->style('default', $prefix).$this->style($style, $value); goto href; } $map = static::$controlCharsMap; $startCchr = $this->colors ? "\033[m\033[{$this->styles['default']}m" : ''; $endCchr = $this->colors ? "\033[m\033[{$this->styles[$style]}m" : ''; $value = preg_replace_callback(static::$controlCharsRx, function ($c) use ($map, $startCchr, $endCchr) { $s = $startCchr; $c = $c[$i = 0]; do { $s .= $map[$c[$i]] ?? sprintf('\x%02X', \ord($c[$i])); } while (isset($c[++$i])); return $s.$endCchr; }, $value, -1, $cchrCount); if ($this->colors) { if ($cchrCount && "\033" === $value[0]) { $value = substr($value, \strlen($startCchr)); } else { $value = "\033[{$this->styles[$style]}m".$value; } if ($cchrCount && str_ends_with($value, $endCchr)) { $value = substr($value, 0, -\strlen($endCchr)); } else { $value .= "\033[{$this->styles['default']}m"; } } href: if ($this->colors && $this->handlesHrefGracefully) { if (isset($attr['file']) && $href = $this->getSourceLink($attr['file'], $attr['line'] ?? 0)) { if ('note' === $style) { $value .= "\033]8;;{$href}\033\\^\033]8;;\033\\"; } else { $attr['href'] = $href; } } if (isset($attr['href'])) { $value = "\033]8;;{$attr['href']}\033\\{$value}\033]8;;\033\\"; } } elseif ($attr['if_links'] ?? false) { return ''; } return $value; } protected function supportsColors(): bool { if ($this->outputStream !== static::$defaultOutput) { return $this->hasColorSupport($this->outputStream); } if (null !== static::$defaultColors) { return static::$defaultColors; } if (isset($_SERVER['argv'][1])) { $colors = $_SERVER['argv']; $i = \count($colors); while (--$i > 0) { if (isset($colors[$i][5])) { switch ($colors[$i]) { case '--ansi': case '--color': case '--color=yes': case '--color=force': case '--color=always': case '--colors=always': return static::$defaultColors = true; case '--no-ansi': case '--color=no': case '--color=none': case '--color=never': case '--colors=never': return static::$defaultColors = false; } } } } $h = stream_get_meta_data($this->outputStream) + ['wrapper_type' => null]; $h = 'Output' === $h['stream_type'] && 'PHP' === $h['wrapper_type'] ? fopen('php://stdout', 'w') : $this->outputStream; return static::$defaultColors = $this->hasColorSupport($h); } /** * {@inheritdoc} */ protected function dumpLine(int $depth, bool $endOfValue = false) { if ($this->colors) { $this->line = sprintf("\033[%sm%s\033[m", $this->styles['default'], $this->line); } parent::dumpLine($depth); } protected function endValue(Cursor $cursor) { if (-1 === $cursor->hashType) { return; } if (Stub::ARRAY_INDEXED === $cursor->hashType || Stub::ARRAY_ASSOC === $cursor->hashType) { if (self::DUMP_TRAILING_COMMA & $this->flags && 0 < $cursor->depth) { $this->line .= ','; } elseif (self::DUMP_COMMA_SEPARATOR & $this->flags && 1 < $cursor->hashLength - $cursor->hashIndex) { $this->line .= ','; } } $this->dumpLine($cursor->depth, true); } /** * Returns true if the stream supports colorization. * * Reference: Composer\XdebugHandler\Process::supportsColor * https://github.com/composer/xdebug-handler */ private function hasColorSupport(mixed $stream): bool { if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) { return false; } // Follow https://no-color.org/ if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) { return false; } if ('Hyper' === getenv('TERM_PROGRAM')) { return true; } if (\DIRECTORY_SEPARATOR === '\\') { return (\function_exists('sapi_windows_vt100_support') && @sapi_windows_vt100_support($stream)) || false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM'); } return stream_isatty($stream); } /** * Returns true if the Windows terminal supports true color. * * Note that this does not check an output stream, but relies on environment * variables from known implementations, or a PHP and Windows version that * supports true color. */ private function isWindowsTrueColor(): bool { $result = 183 <= getenv('ANSICON_VER') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM') || 'Hyper' === getenv('TERM_PROGRAM'); if (!$result) { $version = sprintf( '%s.%s.%s', PHP_WINDOWS_VERSION_MAJOR, PHP_WINDOWS_VERSION_MINOR, PHP_WINDOWS_VERSION_BUILD ); $result = $version >= '10.0.15063'; } return $result; } private function getSourceLink(string $file, int $line) { if ($fmt = $this->displayOptions['fileLinkFormat']) { return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : ($fmt->format($file, $line) ?: 'file://'.$file.'#L'.$line); } return false; } } var-dumper/Dumper/ContextualizedDumper.php 0000644 00000002234 15025017654 0014760 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface; /** * @author Kévin Thérage <therage.kevin@gmail.com> */ class ContextualizedDumper implements DataDumperInterface { private $wrappedDumper; private array $contextProviders; /** * @param ContextProviderInterface[] $contextProviders */ public function __construct(DataDumperInterface $wrappedDumper, array $contextProviders) { $this->wrappedDumper = $wrappedDumper; $this->contextProviders = $contextProviders; } public function dump(Data $data) { $context = []; foreach ($this->contextProviders as $contextProvider) { $context[\get_class($contextProvider)] = $contextProvider->getContext(); } $this->wrappedDumper->dump($data->withContext($context)); } } var-dumper/Dumper/AbstractDumper.php 0000644 00000013520 15025017654 0013521 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\DumperInterface; /** * Abstract mechanism for dumping a Data object. * * @author Nicolas Grekas <p@tchwork.com> */ abstract class AbstractDumper implements DataDumperInterface, DumperInterface { public const DUMP_LIGHT_ARRAY = 1; public const DUMP_STRING_LENGTH = 2; public const DUMP_COMMA_SEPARATOR = 4; public const DUMP_TRAILING_COMMA = 8; public static $defaultOutput = 'php://output'; protected $line = ''; protected $lineDumper; protected $outputStream; protected $decimalPoint = '.'; protected $indentPad = ' '; protected $flags; private string $charset = ''; /** * @param callable|resource|string|null $output A line dumper callable, an opened stream or an output path, defaults to static::$defaultOutput * @param string|null $charset The default character encoding to use for non-UTF8 strings * @param int $flags A bit field of static::DUMP_* constants to fine tune dumps representation */ public function __construct($output = null, string $charset = null, int $flags = 0) { $this->flags = $flags; $this->setCharset($charset ?: \ini_get('php.output_encoding') ?: \ini_get('default_charset') ?: 'UTF-8'); $this->setOutput($output ?: static::$defaultOutput); if (!$output && \is_string(static::$defaultOutput)) { static::$defaultOutput = $this->outputStream; } } /** * Sets the output destination of the dumps. * * @param callable|resource|string $output A line dumper callable, an opened stream or an output path * * @return callable|resource|string The previous output destination */ public function setOutput($output) { $prev = $this->outputStream ?? $this->lineDumper; if (\is_callable($output)) { $this->outputStream = null; $this->lineDumper = $output; } else { if (\is_string($output)) { $output = fopen($output, 'w'); } $this->outputStream = $output; $this->lineDumper = [$this, 'echoLine']; } return $prev; } /** * Sets the default character encoding to use for non-UTF8 strings. * * @return string The previous charset */ public function setCharset(string $charset): string { $prev = $this->charset; $charset = strtoupper($charset); $charset = null === $charset || 'UTF-8' === $charset || 'UTF8' === $charset ? 'CP1252' : $charset; $this->charset = $charset; return $prev; } /** * Sets the indentation pad string. * * @param string $pad A string that will be prepended to dumped lines, repeated by nesting level * * @return string The previous indent pad */ public function setIndentPad(string $pad): string { $prev = $this->indentPad; $this->indentPad = $pad; return $prev; } /** * Dumps a Data object. * * @param callable|resource|string|true|null $output A line dumper callable, an opened stream, an output path or true to return the dump * * @return string|null The dump as string when $output is true */ public function dump(Data $data, $output = null): ?string { if ($locale = $this->flags & (self::DUMP_COMMA_SEPARATOR | self::DUMP_TRAILING_COMMA) ? setlocale(\LC_NUMERIC, 0) : null) { setlocale(\LC_NUMERIC, 'C'); } if ($returnDump = true === $output) { $output = fopen('php://memory', 'r+'); } if ($output) { $prevOutput = $this->setOutput($output); } try { $data->dump($this); $this->dumpLine(-1); if ($returnDump) { $result = stream_get_contents($output, -1, 0); fclose($output); return $result; } } finally { if ($output) { $this->setOutput($prevOutput); } if ($locale) { setlocale(\LC_NUMERIC, $locale); } } return null; } /** * Dumps the current line. * * @param int $depth The recursive depth in the dumped structure for the line being dumped, * or -1 to signal the end-of-dump to the line dumper callable */ protected function dumpLine(int $depth) { ($this->lineDumper)($this->line, $depth, $this->indentPad); $this->line = ''; } /** * Generic line dumper callback. */ protected function echoLine(string $line, int $depth, string $indentPad) { if (-1 !== $depth) { fwrite($this->outputStream, str_repeat($indentPad, $depth).$line."\n"); } } /** * Converts a non-UTF-8 string to UTF-8. */ protected function utf8Encode(?string $s): ?string { if (null === $s || preg_match('//u', $s)) { return $s; } if (!\function_exists('iconv')) { throw new \RuntimeException('Unable to convert a non-UTF-8 string to UTF-8: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); } if (false !== $c = @iconv($this->charset, 'UTF-8', $s)) { return $c; } if ('CP1252' !== $this->charset && false !== $c = @iconv('CP1252', 'UTF-8', $s)) { return $c; } return iconv('CP850', 'UTF-8', $s); } } var-dumper/Dumper/ServerDumper.php 0000644 00000003105 15025017654 0013222 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface; use Symfony\Component\VarDumper\Server\Connection; /** * ServerDumper forwards serialized Data clones to a server. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> */ class ServerDumper implements DataDumperInterface { private $connection; private $wrappedDumper; /** * @param string $host The server host * @param DataDumperInterface|null $wrappedDumper A wrapped instance used whenever we failed contacting the server * @param ContextProviderInterface[] $contextProviders Context providers indexed by context name */ public function __construct(string $host, DataDumperInterface $wrappedDumper = null, array $contextProviders = []) { $this->connection = new Connection($host, $contextProviders); $this->wrappedDumper = $wrappedDumper; } public function getContextProviders(): array { return $this->connection->getContextProviders(); } /** * {@inheritdoc} */ public function dump(Data $data) { if (!$this->connection->write($data) && $this->wrappedDumper) { $this->wrappedDumper->dump($data); } } } var-dumper/Dumper/ContextProvider/RequestContextProvider.php 0000644 00000002701 15025017654 0020447 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper\ContextProvider; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\VarDumper\Caster\ReflectionCaster; use Symfony\Component\VarDumper\Cloner\VarCloner; /** * Tries to provide context from a request. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> */ final class RequestContextProvider implements ContextProviderInterface { private $requestStack; private $cloner; public function __construct(RequestStack $requestStack) { $this->requestStack = $requestStack; $this->cloner = new VarCloner(); $this->cloner->setMaxItems(0); $this->cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); } public function getContext(): ?array { if (null === $request = $this->requestStack->getCurrentRequest()) { return null; } $controller = $request->attributes->get('_controller'); return [ 'uri' => $request->getUri(), 'method' => $request->getMethod(), 'controller' => $controller ? $this->cloner->cloneVar($controller) : $controller, 'identifier' => spl_object_hash($request), ]; } } var-dumper/Dumper/ContextProvider/CliContextProvider.php 0000644 00000001446 15025017654 0017533 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper\ContextProvider; /** * Tries to provide context on CLI. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> */ final class CliContextProvider implements ContextProviderInterface { public function getContext(): ?array { if ('cli' !== \PHP_SAPI) { return null; } return [ 'command_line' => $commandLine = implode(' ', $_SERVER['argv'] ?? []), 'identifier' => hash('crc32b', $commandLine.$_SERVER['REQUEST_TIME_FLOAT']), ]; } } var-dumper/Dumper/ContextProvider/SourceContextProvider.php 0000644 00000011466 15025017654 0020267 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper\ContextProvider; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Symfony\Component\VarDumper\VarDumper; use Twig\Template; /** * Tries to provide context from sources (class name, file, line, code excerpt, ...). * * @author Nicolas Grekas <p@tchwork.com> * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> */ final class SourceContextProvider implements ContextProviderInterface { private int $limit; private ?string $charset; private ?string $projectDir; private $fileLinkFormatter; public function __construct(string $charset = null, string $projectDir = null, FileLinkFormatter $fileLinkFormatter = null, int $limit = 9) { $this->charset = $charset; $this->projectDir = $projectDir; $this->fileLinkFormatter = $fileLinkFormatter; $this->limit = $limit; } public function getContext(): ?array { $trace = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, $this->limit); $file = $trace[1]['file']; $line = $trace[1]['line']; $name = false; $fileExcerpt = false; for ($i = 2; $i < $this->limit; ++$i) { if (isset($trace[$i]['class'], $trace[$i]['function']) && 'dump' === $trace[$i]['function'] && VarDumper::class === $trace[$i]['class'] ) { $file = $trace[$i]['file'] ?? $file; $line = $trace[$i]['line'] ?? $line; while (++$i < $this->limit) { if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && !str_starts_with($trace[$i]['function'], 'call_user_func')) { $file = $trace[$i]['file']; $line = $trace[$i]['line']; break; } elseif (isset($trace[$i]['object']) && $trace[$i]['object'] instanceof Template) { $template = $trace[$i]['object']; $name = $template->getTemplateName(); $src = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getCode() : (method_exists($template, 'getSource') ? $template->getSource() : false); $info = $template->getDebugInfo(); if (isset($info[$trace[$i - 1]['line']])) { $line = $info[$trace[$i - 1]['line']]; $file = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getPath() : null; if ($src) { $src = explode("\n", $src); $fileExcerpt = []; for ($i = max($line - 3, 1), $max = min($line + 3, \count($src)); $i <= $max; ++$i) { $fileExcerpt[] = '<li'.($i === $line ? ' class="selected"' : '').'><code>'.$this->htmlEncode($src[$i - 1]).'</code></li>'; } $fileExcerpt = '<ol start="'.max($line - 3, 1).'">'.implode("\n", $fileExcerpt).'</ol>'; } } break; } } break; } } if (false === $name) { $name = str_replace('\\', '/', $file); $name = substr($name, strrpos($name, '/') + 1); } $context = ['name' => $name, 'file' => $file, 'line' => $line]; $context['file_excerpt'] = $fileExcerpt; if (null !== $this->projectDir) { $context['project_dir'] = $this->projectDir; if (str_starts_with($file, $this->projectDir)) { $context['file_relative'] = ltrim(substr($file, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR); } } if ($this->fileLinkFormatter && $fileLink = $this->fileLinkFormatter->format($context['file'], $context['line'])) { $context['file_link'] = $fileLink; } return $context; } private function htmlEncode(string $s): string { $html = ''; $dumper = new HtmlDumper(function ($line) use (&$html) { $html .= $line; }, $this->charset); $dumper->setDumpHeader(''); $dumper->setDumpBoundaries('', ''); $cloner = new VarCloner(); $dumper->dump($cloner->cloneVar($s)); return substr(strip_tags($html), 1, -1); } } var-dumper/Dumper/ContextProvider/ContextProviderInterface.php 0000644 00000001031 15025017654 0020712 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper\ContextProvider; /** * Interface to provide contextual data about dump data clones sent to a server. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> */ interface ContextProviderInterface { public function getContext(): ?array; } var-dumper/Dumper/DataDumperInterface.php 0000644 00000000771 15025017654 0014454 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper; use Symfony\Component\VarDumper\Cloner\Data; /** * DataDumperInterface for dumping Data objects. * * @author Nicolas Grekas <p@tchwork.com> */ interface DataDumperInterface { public function dump(Data $data); } var-dumper/Dumper/HtmlDumper.php 0000644 00000102434 15025017654 0012665 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper; use Symfony\Component\VarDumper\Cloner\Cursor; use Symfony\Component\VarDumper\Cloner\Data; /** * HtmlDumper dumps variables as HTML. * * @author Nicolas Grekas <p@tchwork.com> */ class HtmlDumper extends CliDumper { public static $defaultOutput = 'php://output'; protected static $themes = [ 'dark' => [ 'default' => 'background-color:#18171B; color:#FF8400; line-height:1.2em; font:12px Menlo, Monaco, Consolas, monospace; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: break-all', 'num' => 'font-weight:bold; color:#1299DA', 'const' => 'font-weight:bold', 'str' => 'font-weight:bold; color:#56DB3A', 'note' => 'color:#1299DA', 'ref' => 'color:#A0A0A0', 'public' => 'color:#FFFFFF', 'protected' => 'color:#FFFFFF', 'private' => 'color:#FFFFFF', 'meta' => 'color:#B729D9', 'key' => 'color:#56DB3A', 'index' => 'color:#1299DA', 'ellipsis' => 'color:#FF8400', 'ns' => 'user-select:none;', ], 'light' => [ 'default' => 'background:none; color:#CC7832; line-height:1.2em; font:12px Menlo, Monaco, Consolas, monospace; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: break-all', 'num' => 'font-weight:bold; color:#1299DA', 'const' => 'font-weight:bold', 'str' => 'font-weight:bold; color:#629755;', 'note' => 'color:#6897BB', 'ref' => 'color:#6E6E6E', 'public' => 'color:#262626', 'protected' => 'color:#262626', 'private' => 'color:#262626', 'meta' => 'color:#B729D9', 'key' => 'color:#789339', 'index' => 'color:#1299DA', 'ellipsis' => 'color:#CC7832', 'ns' => 'user-select:none;', ], ]; protected $dumpHeader; protected $dumpPrefix = '<pre class=sf-dump id=%s data-indent-pad="%s">'; protected $dumpSuffix = '</pre><script>Sfdump(%s)</script>'; protected $dumpId = 'sf-dump'; protected $colors = true; protected $headerIsDumped = false; protected $lastDepth = -1; protected $styles; private array $displayOptions = [ 'maxDepth' => 1, 'maxStringLength' => 160, 'fileLinkFormat' => null, ]; private array $extraDisplayOptions = []; /** * {@inheritdoc} */ public function __construct($output = null, string $charset = null, int $flags = 0) { AbstractDumper::__construct($output, $charset, $flags); $this->dumpId = 'sf-dump-'.mt_rand(); $this->displayOptions['fileLinkFormat'] = \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); $this->styles = static::$themes['dark'] ?? self::$themes['dark']; } /** * {@inheritdoc} */ public function setStyles(array $styles) { $this->headerIsDumped = false; $this->styles = $styles + $this->styles; } public function setTheme(string $themeName) { if (!isset(static::$themes[$themeName])) { throw new \InvalidArgumentException(sprintf('Theme "%s" does not exist in class "%s".', $themeName, static::class)); } $this->setStyles(static::$themes[$themeName]); } /** * Configures display options. * * @param array $displayOptions A map of display options to customize the behavior */ public function setDisplayOptions(array $displayOptions) { $this->headerIsDumped = false; $this->displayOptions = $displayOptions + $this->displayOptions; } /** * Sets an HTML header that will be dumped once in the output stream. */ public function setDumpHeader(?string $header) { $this->dumpHeader = $header; } /** * Sets an HTML prefix and suffix that will encapse every single dump. */ public function setDumpBoundaries(string $prefix, string $suffix) { $this->dumpPrefix = $prefix; $this->dumpSuffix = $suffix; } /** * {@inheritdoc} */ public function dump(Data $data, $output = null, array $extraDisplayOptions = []): ?string { $this->extraDisplayOptions = $extraDisplayOptions; $result = parent::dump($data, $output); $this->dumpId = 'sf-dump-'.mt_rand(); return $result; } /** * Dumps the HTML header. */ protected function getDumpHeader() { $this->headerIsDumped = $this->outputStream ?? $this->lineDumper; if (null !== $this->dumpHeader) { return $this->dumpHeader; } $line = str_replace('{$options}', json_encode($this->displayOptions, \JSON_FORCE_OBJECT), <<<'EOHTML' <script> Sfdump = window.Sfdump || (function (doc) { var refStyle = doc.createElement('style'), rxEsc = /([.*+?^${}()|\[\]\/\\])/g, idRx = /\bsf-dump-\d+-ref[012]\w+\b/, keyHint = 0 <= navigator.platform.toUpperCase().indexOf('MAC') ? 'Cmd' : 'Ctrl', addEventListener = function (e, n, cb) { e.addEventListener(n, cb, false); }; refStyle.innerHTML = 'pre.sf-dump .sf-dump-compact, .sf-dump-str-collapse .sf-dump-str-collapse, .sf-dump-str-expand .sf-dump-str-expand { display: none; }'; doc.head.appendChild(refStyle); refStyle = doc.createElement('style'); doc.head.appendChild(refStyle); if (!doc.addEventListener) { addEventListener = function (element, eventName, callback) { element.attachEvent('on' + eventName, function (e) { e.preventDefault = function () {e.returnValue = false;}; e.target = e.srcElement; callback(e); }); }; } function toggle(a, recursive) { var s = a.nextSibling || {}, oldClass = s.className, arrow, newClass; if (/\bsf-dump-compact\b/.test(oldClass)) { arrow = '▼'; newClass = 'sf-dump-expanded'; } else if (/\bsf-dump-expanded\b/.test(oldClass)) { arrow = '▶'; newClass = 'sf-dump-compact'; } else { return false; } if (doc.createEvent && s.dispatchEvent) { var event = doc.createEvent('Event'); event.initEvent('sf-dump-expanded' === newClass ? 'sfbeforedumpexpand' : 'sfbeforedumpcollapse', true, false); s.dispatchEvent(event); } a.lastChild.innerHTML = arrow; s.className = s.className.replace(/\bsf-dump-(compact|expanded)\b/, newClass); if (recursive) { try { a = s.querySelectorAll('.'+oldClass); for (s = 0; s < a.length; ++s) { if (-1 == a[s].className.indexOf(newClass)) { a[s].className = newClass; a[s].previousSibling.lastChild.innerHTML = arrow; } } } catch (e) { } } return true; }; function collapse(a, recursive) { var s = a.nextSibling || {}, oldClass = s.className; if (/\bsf-dump-expanded\b/.test(oldClass)) { toggle(a, recursive); return true; } return false; }; function expand(a, recursive) { var s = a.nextSibling || {}, oldClass = s.className; if (/\bsf-dump-compact\b/.test(oldClass)) { toggle(a, recursive); return true; } return false; }; function collapseAll(root) { var a = root.querySelector('a.sf-dump-toggle'); if (a) { collapse(a, true); expand(a); return true; } return false; } function reveal(node) { var previous, parents = []; while ((node = node.parentNode || {}) && (previous = node.previousSibling) && 'A' === previous.tagName) { parents.push(previous); } if (0 !== parents.length) { parents.forEach(function (parent) { expand(parent); }); return true; } return false; } function highlight(root, activeNode, nodes) { resetHighlightedNodes(root); Array.from(nodes||[]).forEach(function (node) { if (!/\bsf-dump-highlight\b/.test(node.className)) { node.className = node.className + ' sf-dump-highlight'; } }); if (!/\bsf-dump-highlight-active\b/.test(activeNode.className)) { activeNode.className = activeNode.className + ' sf-dump-highlight-active'; } } function resetHighlightedNodes(root) { Array.from(root.querySelectorAll('.sf-dump-str, .sf-dump-key, .sf-dump-public, .sf-dump-protected, .sf-dump-private')).forEach(function (strNode) { strNode.className = strNode.className.replace(/\bsf-dump-highlight\b/, ''); strNode.className = strNode.className.replace(/\bsf-dump-highlight-active\b/, ''); }); } return function (root, x) { root = doc.getElementById(root); var indentRx = new RegExp('^('+(root.getAttribute('data-indent-pad') || ' ').replace(rxEsc, '\\$1')+')+', 'm'), options = {$options}, elt = root.getElementsByTagName('A'), len = elt.length, i = 0, s, h, t = []; while (i < len) t.push(elt[i++]); for (i in x) { options[i] = x[i]; } function a(e, f) { addEventListener(root, e, function (e, n) { if ('A' == e.target.tagName) { f(e.target, e); } else if ('A' == e.target.parentNode.tagName) { f(e.target.parentNode, e); } else { n = /\bsf-dump-ellipsis\b/.test(e.target.className) ? e.target.parentNode : e.target; if ((n = n.nextElementSibling) && 'A' == n.tagName) { if (!/\bsf-dump-toggle\b/.test(n.className)) { n = n.nextElementSibling || n; } f(n, e, true); } } }); }; function isCtrlKey(e) { return e.ctrlKey || e.metaKey; } function xpathString(str) { var parts = str.match(/[^'"]+|['"]/g).map(function (part) { if ("'" == part) { return '"\'"'; } if ('"' == part) { return "'\"'"; } return "'" + part + "'"; }); return "concat(" + parts.join(",") + ", '')"; } function xpathHasClass(className) { return "contains(concat(' ', normalize-space(@class), ' '), ' " + className +" ')"; } addEventListener(root, 'mouseover', function (e) { if ('' != refStyle.innerHTML) { refStyle.innerHTML = ''; } }); a('mouseover', function (a, e, c) { if (c) { e.target.style.cursor = "pointer"; } else if (a = idRx.exec(a.className)) { try { refStyle.innerHTML = 'pre.sf-dump .'+a[0]+'{background-color: #B729D9; color: #FFF !important; border-radius: 2px}'; } catch (e) { } } }); a('click', function (a, e, c) { if (/\bsf-dump-toggle\b/.test(a.className)) { e.preventDefault(); if (!toggle(a, isCtrlKey(e))) { var r = doc.getElementById(a.getAttribute('href').slice(1)), s = r.previousSibling, f = r.parentNode, t = a.parentNode; t.replaceChild(r, a); f.replaceChild(a, s); t.insertBefore(s, r); f = f.firstChild.nodeValue.match(indentRx); t = t.firstChild.nodeValue.match(indentRx); if (f && t && f[0] !== t[0]) { r.innerHTML = r.innerHTML.replace(new RegExp('^'+f[0].replace(rxEsc, '\\$1'), 'mg'), t[0]); } if (/\bsf-dump-compact\b/.test(r.className)) { toggle(s, isCtrlKey(e)); } } if (c) { } else if (doc.getSelection) { try { doc.getSelection().removeAllRanges(); } catch (e) { doc.getSelection().empty(); } } else { doc.selection.empty(); } } else if (/\bsf-dump-str-toggle\b/.test(a.className)) { e.preventDefault(); e = a.parentNode.parentNode; e.className = e.className.replace(/\bsf-dump-str-(expand|collapse)\b/, a.parentNode.className); } }); elt = root.getElementsByTagName('SAMP'); len = elt.length; i = 0; while (i < len) t.push(elt[i++]); len = t.length; for (i = 0; i < len; ++i) { elt = t[i]; if ('SAMP' == elt.tagName) { a = elt.previousSibling || {}; if ('A' != a.tagName) { a = doc.createElement('A'); a.className = 'sf-dump-ref'; elt.parentNode.insertBefore(a, elt); } else { a.innerHTML += ' '; } a.title = (a.title ? a.title+'\n[' : '[')+keyHint+'+click] Expand all children'; a.innerHTML += elt.className == 'sf-dump-compact' ? '<span>▶</span>' : '<span>▼</span>'; a.className += ' sf-dump-toggle'; x = 1; if ('sf-dump' != elt.parentNode.className) { x += elt.parentNode.getAttribute('data-depth')/1; } } else if (/\bsf-dump-ref\b/.test(elt.className) && (a = elt.getAttribute('href'))) { a = a.slice(1); elt.className += ' '+a; if (/[\[{]$/.test(elt.previousSibling.nodeValue)) { a = a != elt.nextSibling.id && doc.getElementById(a); try { s = a.nextSibling; elt.appendChild(a); s.parentNode.insertBefore(a, s); if (/^[@#]/.test(elt.innerHTML)) { elt.innerHTML += ' <span>▶</span>'; } else { elt.innerHTML = '<span>▶</span>'; elt.className = 'sf-dump-ref'; } elt.className += ' sf-dump-toggle'; } catch (e) { if ('&' == elt.innerHTML.charAt(0)) { elt.innerHTML = '…'; elt.className = 'sf-dump-ref'; } } } } } if (doc.evaluate && Array.from && root.children.length > 1) { root.setAttribute('tabindex', 0); SearchState = function () { this.nodes = []; this.idx = 0; }; SearchState.prototype = { next: function () { if (this.isEmpty()) { return this.current(); } this.idx = this.idx < (this.nodes.length - 1) ? this.idx + 1 : 0; return this.current(); }, previous: function () { if (this.isEmpty()) { return this.current(); } this.idx = this.idx > 0 ? this.idx - 1 : (this.nodes.length - 1); return this.current(); }, isEmpty: function () { return 0 === this.count(); }, current: function () { if (this.isEmpty()) { return null; } return this.nodes[this.idx]; }, reset: function () { this.nodes = []; this.idx = 0; }, count: function () { return this.nodes.length; }, }; function showCurrent(state) { var currentNode = state.current(), currentRect, searchRect; if (currentNode) { reveal(currentNode); highlight(root, currentNode, state.nodes); if ('scrollIntoView' in currentNode) { currentNode.scrollIntoView(true); currentRect = currentNode.getBoundingClientRect(); searchRect = search.getBoundingClientRect(); if (currentRect.top < (searchRect.top + searchRect.height)) { window.scrollBy(0, -(searchRect.top + searchRect.height + 5)); } } } counter.textContent = (state.isEmpty() ? 0 : state.idx + 1) + ' of ' + state.count(); } var search = doc.createElement('div'); search.className = 'sf-dump-search-wrapper sf-dump-search-hidden'; search.innerHTML = ' <input type="text" class="sf-dump-search-input"> <span class="sf-dump-search-count">0 of 0<\/span> <button type="button" class="sf-dump-search-input-previous" tabindex="-1"> <svg viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1683 1331l-166 165q-19 19-45 19t-45-19L896 965l-531 531q-19 19-45 19t-45-19l-166-165q-19-19-19-45.5t19-45.5l742-741q19-19 45-19t45 19l742 741q19 19 19 45.5t-19 45.5z"\/><\/svg> <\/button> <button type="button" class="sf-dump-search-input-next" tabindex="-1"> <svg viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1683 808l-742 741q-19 19-45 19t-45-19L109 808q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"\/><\/svg> <\/button> '; root.insertBefore(search, root.firstChild); var state = new SearchState(); var searchInput = search.querySelector('.sf-dump-search-input'); var counter = search.querySelector('.sf-dump-search-count'); var searchInputTimer = 0; var previousSearchQuery = ''; addEventListener(searchInput, 'keyup', function (e) { var searchQuery = e.target.value; /* Don't perform anything if the pressed key didn't change the query */ if (searchQuery === previousSearchQuery) { return; } previousSearchQuery = searchQuery; clearTimeout(searchInputTimer); searchInputTimer = setTimeout(function () { state.reset(); collapseAll(root); resetHighlightedNodes(root); if ('' === searchQuery) { counter.textContent = '0 of 0'; return; } var classMatches = [ "sf-dump-str", "sf-dump-key", "sf-dump-public", "sf-dump-protected", "sf-dump-private", ].map(xpathHasClass).join(' or '); var xpathResult = doc.evaluate('.//span[' + classMatches + '][contains(translate(child::text(), ' + xpathString(searchQuery.toUpperCase()) + ', ' + xpathString(searchQuery.toLowerCase()) + '), ' + xpathString(searchQuery.toLowerCase()) + ')]', root, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); while (node = xpathResult.iterateNext()) state.nodes.push(node); showCurrent(state); }, 400); }); Array.from(search.querySelectorAll('.sf-dump-search-input-next, .sf-dump-search-input-previous')).forEach(function (btn) { addEventListener(btn, 'click', function (e) { e.preventDefault(); -1 !== e.target.className.indexOf('next') ? state.next() : state.previous(); searchInput.focus(); collapseAll(root); showCurrent(state); }) }); addEventListener(root, 'keydown', function (e) { var isSearchActive = !/\bsf-dump-search-hidden\b/.test(search.className); if ((114 === e.keyCode && !isSearchActive) || (isCtrlKey(e) && 70 === e.keyCode)) { /* F3 or CMD/CTRL + F */ if (70 === e.keyCode && document.activeElement === searchInput) { /* * If CMD/CTRL + F is hit while having focus on search input, * the user probably meant to trigger browser search instead. * Let the browser execute its behavior: */ return; } e.preventDefault(); search.className = search.className.replace(/\bsf-dump-search-hidden\b/, ''); searchInput.focus(); } else if (isSearchActive) { if (27 === e.keyCode) { /* ESC key */ search.className += ' sf-dump-search-hidden'; e.preventDefault(); resetHighlightedNodes(root); searchInput.value = ''; } else if ( (isCtrlKey(e) && 71 === e.keyCode) /* CMD/CTRL + G */ || 13 === e.keyCode /* Enter */ || 114 === e.keyCode /* F3 */ ) { e.preventDefault(); e.shiftKey ? state.previous() : state.next(); collapseAll(root); showCurrent(state); } } }); } if (0 >= options.maxStringLength) { return; } try { elt = root.querySelectorAll('.sf-dump-str'); len = elt.length; i = 0; t = []; while (i < len) t.push(elt[i++]); len = t.length; for (i = 0; i < len; ++i) { elt = t[i]; s = elt.innerText || elt.textContent; x = s.length - options.maxStringLength; if (0 < x) { h = elt.innerHTML; elt[elt.innerText ? 'innerText' : 'textContent'] = s.substring(0, options.maxStringLength); elt.className += ' sf-dump-str-collapse'; elt.innerHTML = '<span class=sf-dump-str-collapse>'+h+'<a class="sf-dump-ref sf-dump-str-toggle" title="Collapse"> ◀</a></span>'+ '<span class=sf-dump-str-expand>'+elt.innerHTML+'<a class="sf-dump-ref sf-dump-str-toggle" title="'+x+' remaining characters"> ▶</a></span>'; } } } catch (e) { } }; })(document); </script><style> pre.sf-dump { display: block; white-space: pre; padding: 5px; overflow: initial !important; } pre.sf-dump:after { content: ""; visibility: hidden; display: block; height: 0; clear: both; } pre.sf-dump span { display: inline; } pre.sf-dump a { text-decoration: none; cursor: pointer; border: 0; outline: none; color: inherit; } pre.sf-dump img { max-width: 50em; max-height: 50em; margin: .5em 0 0 0; padding: 0; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAHUlEQVQY02O8zAABilCaiQEN0EeA8QuUcX9g3QEAAjcC5piyhyEAAAAASUVORK5CYII=) #D3D3D3; } pre.sf-dump .sf-dump-ellipsis { display: inline-block; overflow: visible; text-overflow: ellipsis; max-width: 5em; white-space: nowrap; overflow: hidden; vertical-align: top; } pre.sf-dump .sf-dump-ellipsis+.sf-dump-ellipsis { max-width: none; } pre.sf-dump code { display:inline; padding:0; background:none; } .sf-dump-public.sf-dump-highlight, .sf-dump-protected.sf-dump-highlight, .sf-dump-private.sf-dump-highlight, .sf-dump-str.sf-dump-highlight, .sf-dump-key.sf-dump-highlight { background: rgba(111, 172, 204, 0.3); border: 1px solid #7DA0B1; border-radius: 3px; } .sf-dump-public.sf-dump-highlight-active, .sf-dump-protected.sf-dump-highlight-active, .sf-dump-private.sf-dump-highlight-active, .sf-dump-str.sf-dump-highlight-active, .sf-dump-key.sf-dump-highlight-active { background: rgba(253, 175, 0, 0.4); border: 1px solid #ffa500; border-radius: 3px; } pre.sf-dump .sf-dump-search-hidden { display: none !important; } pre.sf-dump .sf-dump-search-wrapper { font-size: 0; white-space: nowrap; margin-bottom: 5px; display: flex; position: -webkit-sticky; position: sticky; top: 5px; } pre.sf-dump .sf-dump-search-wrapper > * { vertical-align: top; box-sizing: border-box; height: 21px; font-weight: normal; border-radius: 0; background: #FFF; color: #757575; border: 1px solid #BBB; } pre.sf-dump .sf-dump-search-wrapper > input.sf-dump-search-input { padding: 3px; height: 21px; font-size: 12px; border-right: none; border-top-left-radius: 3px; border-bottom-left-radius: 3px; color: #000; min-width: 15px; width: 100%; } pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next, pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-previous { background: #F2F2F2; outline: none; border-left: none; font-size: 0; line-height: 0; } pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next { border-top-right-radius: 3px; border-bottom-right-radius: 3px; } pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next > svg, pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-previous > svg { pointer-events: none; width: 12px; height: 12px; } pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-count { display: inline-block; padding: 0 5px; margin: 0; border-left: none; line-height: 21px; font-size: 12px; } EOHTML ); foreach ($this->styles as $class => $style) { $line .= 'pre.sf-dump'.('default' === $class ? ', pre.sf-dump' : '').' .sf-dump-'.$class.'{'.$style.'}'; } $line .= 'pre.sf-dump .sf-dump-ellipsis-note{'.$this->styles['note'].'}'; return $this->dumpHeader = preg_replace('/\s+/', ' ', $line).'</style>'.$this->dumpHeader; } /** * {@inheritdoc} */ public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut) { if ('' === $str && isset($cursor->attr['img-data'], $cursor->attr['content-type'])) { $this->dumpKey($cursor); $this->line .= $this->style('default', $cursor->attr['img-size'] ?? '', []); $this->line .= $cursor->depth >= $this->displayOptions['maxDepth'] ? ' <samp class=sf-dump-compact>' : ' <samp class=sf-dump-expanded>'; $this->endValue($cursor); $this->line .= $this->indentPad; $this->line .= sprintf('<img src="data:%s;base64,%s" /></samp>', $cursor->attr['content-type'], base64_encode($cursor->attr['img-data'])); $this->endValue($cursor); } else { parent::dumpString($cursor, $str, $bin, $cut); } } /** * {@inheritdoc} */ public function enterHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild) { if (Cursor::HASH_OBJECT === $type) { $cursor->attr['depth'] = $cursor->depth; } parent::enterHash($cursor, $type, $class, false); if ($cursor->skipChildren || $cursor->depth >= $this->displayOptions['maxDepth']) { $cursor->skipChildren = false; $eol = ' class=sf-dump-compact>'; } else { $this->expandNextHash = false; $eol = ' class=sf-dump-expanded>'; } if ($hasChild) { $this->line .= '<samp data-depth='.($cursor->depth + 1); if ($cursor->refIndex) { $r = Cursor::HASH_OBJECT !== $type ? 1 - (Cursor::HASH_RESOURCE !== $type) : 2; $r .= $r && 0 < $cursor->softRefHandle ? $cursor->softRefHandle : $cursor->refIndex; $this->line .= sprintf(' id=%s-ref%s', $this->dumpId, $r); } $this->line .= $eol; $this->dumpLine($cursor->depth); } } /** * {@inheritdoc} */ public function leaveHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild, int $cut) { $this->dumpEllipsis($cursor, $hasChild, $cut); if ($hasChild) { $this->line .= '</samp>'; } parent::leaveHash($cursor, $type, $class, $hasChild, 0); } /** * {@inheritdoc} */ protected function style(string $style, string $value, array $attr = []): string { if ('' === $value) { return ''; } $v = esc($value); if ('ref' === $style) { if (empty($attr['count'])) { return sprintf('<a class=sf-dump-ref>%s</a>', $v); } $r = ('#' !== $v[0] ? 1 - ('@' !== $v[0]) : 2).substr($value, 1); return sprintf('<a class=sf-dump-ref href=#%s-ref%s title="%d occurrences">%s</a>', $this->dumpId, $r, 1 + $attr['count'], $v); } if ('const' === $style && isset($attr['value'])) { $style .= sprintf(' title="%s"', esc(\is_scalar($attr['value']) ? $attr['value'] : json_encode($attr['value']))); } elseif ('public' === $style) { $style .= sprintf(' title="%s"', empty($attr['dynamic']) ? 'Public property' : 'Runtime added dynamic property'); } elseif ('str' === $style && 1 < $attr['length']) { $style .= sprintf(' title="%d%s characters"', $attr['length'], $attr['binary'] ? ' binary or non-UTF-8' : ''); } elseif ('note' === $style && 0 < ($attr['depth'] ?? 0) && false !== $c = strrpos($value, '\\')) { $style .= ' title=""'; $attr += [ 'ellipsis' => \strlen($value) - $c, 'ellipsis-type' => 'note', 'ellipsis-tail' => 1, ]; } elseif ('protected' === $style) { $style .= ' title="Protected property"'; } elseif ('meta' === $style && isset($attr['title'])) { $style .= sprintf(' title="%s"', esc($this->utf8Encode($attr['title']))); } elseif ('private' === $style) { $style .= sprintf(' title="Private property defined in class: `%s`"', esc($this->utf8Encode($attr['class']))); } $map = static::$controlCharsMap; if (isset($attr['ellipsis'])) { $class = 'sf-dump-ellipsis'; if (isset($attr['ellipsis-type'])) { $class = sprintf('"%s sf-dump-ellipsis-%s"', $class, $attr['ellipsis-type']); } $label = esc(substr($value, -$attr['ellipsis'])); $style = str_replace(' title="', " title=\"$v\n", $style); $v = sprintf('<span class=%s>%s</span>', $class, substr($v, 0, -\strlen($label))); if (!empty($attr['ellipsis-tail'])) { $tail = \strlen(esc(substr($value, -$attr['ellipsis'], $attr['ellipsis-tail']))); $v .= sprintf('<span class=%s>%s</span>%s', $class, substr($label, 0, $tail), substr($label, $tail)); } else { $v .= $label; } } $v = "<span class=sf-dump-{$style}>".preg_replace_callback(static::$controlCharsRx, function ($c) use ($map) { $s = $b = '<span class="sf-dump-default'; $c = $c[$i = 0]; if ($ns = "\r" === $c[$i] || "\n" === $c[$i]) { $s .= ' sf-dump-ns'; } $s .= '">'; do { if (("\r" === $c[$i] || "\n" === $c[$i]) !== $ns) { $s .= '</span>'.$b; if ($ns = !$ns) { $s .= ' sf-dump-ns'; } $s .= '">'; } $s .= $map[$c[$i]] ?? sprintf('\x%02X', \ord($c[$i])); } while (isset($c[++$i])); return $s.'</span>'; }, $v).'</span>'; if (isset($attr['file']) && $href = $this->getSourceLink($attr['file'], $attr['line'] ?? 0)) { $attr['href'] = $href; } if (isset($attr['href'])) { $target = isset($attr['file']) ? '' : ' target="_blank"'; $v = sprintf('<a href="%s"%s rel="noopener noreferrer">%s</a>', esc($this->utf8Encode($attr['href'])), $target, $v); } if (isset($attr['lang'])) { $v = sprintf('<code class="%s">%s</code>', esc($attr['lang']), $v); } return $v; } /** * {@inheritdoc} */ protected function dumpLine(int $depth, bool $endOfValue = false) { if (-1 === $this->lastDepth) { $this->line = sprintf($this->dumpPrefix, $this->dumpId, $this->indentPad).$this->line; } if ($this->headerIsDumped !== ($this->outputStream ?? $this->lineDumper)) { $this->line = $this->getDumpHeader().$this->line; } if (-1 === $depth) { $args = ['"'.$this->dumpId.'"']; if ($this->extraDisplayOptions) { $args[] = json_encode($this->extraDisplayOptions, \JSON_FORCE_OBJECT); } // Replace is for BC $this->line .= sprintf(str_replace('"%s"', '%s', $this->dumpSuffix), implode(', ', $args)); } $this->lastDepth = $depth; $this->line = mb_encode_numericentity($this->line, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8'); if (-1 === $depth) { AbstractDumper::dumpLine(0); } AbstractDumper::dumpLine($depth); } private function getSourceLink(string $file, int $line) { $options = $this->extraDisplayOptions + $this->displayOptions; if ($fmt = $options['fileLinkFormat']) { return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line); } return false; } } function esc(string $str) { return htmlspecialchars($str, \ENT_QUOTES, 'UTF-8'); } var-dumper/Resources/css/htmlDescriptor.css 0000644 00000005702 15025017654 0015116 0 ustar 00 body { display: flex; flex-direction: column-reverse; justify-content: flex-end; max-width: 1140px; margin: auto; padding: 15px; word-wrap: break-word; background-color: #F9F9F9; color: #222; font-family: Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.4; } p { margin: 0; } a { color: #218BC3; text-decoration: none; } a:hover { text-decoration: underline; } .text-small { font-size: 12px !important; } article { margin: 5px; margin-bottom: 10px; } article > header > .row { display: flex; flex-direction: row; align-items: baseline; margin-bottom: 10px; } article > header > .row > .col { flex: 1; display: flex; align-items: baseline; } article > header > .row > h2 { font-size: 14px; color: #222; font-weight: normal; font-family: "Lucida Console", monospace, sans-serif; word-break: break-all; margin: 20px 5px 0 0; user-select: all; } article > header > .row > h2 > code { white-space: nowrap; user-select: none; color: #cc2255; background-color: #f7f7f9; border: 1px solid #e1e1e8; border-radius: 3px; margin-right: 5px; padding: 0 3px; } article > header > .row > time.col { flex: 0; text-align: right; white-space: nowrap; color: #999; font-style: italic; } article > header ul.tags { list-style: none; padding: 0; margin: 0; font-size: 12px; } article > header ul.tags > li { user-select: all; margin-bottom: 2px; } article > header ul.tags > li > span.badge { display: inline-block; padding: .25em .4em; margin-right: 5px; border-radius: 4px; background-color: #6c757d3b; color: #524d4d; font-size: 12px; text-align: center; font-weight: 700; line-height: 1; white-space: nowrap; vertical-align: baseline; user-select: none; } article > section.body { border: 1px solid #d8d8d8; background: #FFF; padding: 10px; border-radius: 3px; } pre.sf-dump { border-radius: 3px; margin-bottom: 0; } .hidden { display: none !important; } .dumped-tag > .sf-dump { display: inline-block; margin: 0; padding: 1px 5px; line-height: 1.4; vertical-align: top; background-color: transparent; user-select: auto; } .dumped-tag > pre.sf-dump, .dumped-tag > .sf-dump-default { color: #CC7832; background: none; } .dumped-tag > .sf-dump .sf-dump-str { color: #629755; } .dumped-tag > .sf-dump .sf-dump-private, .dumped-tag > .sf-dump .sf-dump-protected, .dumped-tag > .sf-dump .sf-dump-public { color: #262626; } .dumped-tag > .sf-dump .sf-dump-note { color: #6897BB; } .dumped-tag > .sf-dump .sf-dump-key { color: #789339; } .dumped-tag > .sf-dump .sf-dump-ref { color: #6E6E6E; } .dumped-tag > .sf-dump .sf-dump-ellipsis { color: #CC7832; max-width: 100em; } .dumped-tag > .sf-dump .sf-dump-ellipsis-path { max-width: 5em; } .dumped-tag > .sf-dump .sf-dump-ns { user-select: none; } var-dumper/Resources/bin/var-dump-server 0000644 00000004136 15025017654 0014343 0 ustar 00 #!/usr/bin/env php <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if ('cli' !== PHP_SAPI) { throw new Exception('This script must be run from the command line.'); } /** * Starts a dump server to collect and output dumps on a single place with multiple formats support. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> */ use Psr\Log\LoggerInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\VarDumper\Command\ServerDumpCommand; use Symfony\Component\VarDumper\Server\DumpServer; function includeIfExists(string $file): bool { return file_exists($file) && include $file; } if ( !includeIfExists(__DIR__ . '/../../../../autoload.php') && !includeIfExists(__DIR__ . '/../../vendor/autoload.php') && !includeIfExists(__DIR__ . '/../../../../../../vendor/autoload.php') ) { fwrite(STDERR, 'Install dependencies using Composer.'.PHP_EOL); exit(1); } if (!class_exists(Application::class)) { fwrite(STDERR, 'You need the "symfony/console" component in order to run the VarDumper server.'.PHP_EOL); exit(1); } $input = new ArgvInput(); $output = new ConsoleOutput(); $defaultHost = '127.0.0.1:9912'; $host = $input->getParameterOption(['--host'], $_SERVER['VAR_DUMPER_SERVER'] ?? $defaultHost, true); $logger = interface_exists(LoggerInterface::class) ? new ConsoleLogger($output->getErrorOutput()) : null; $app = new Application(); $app->getDefinition()->addOption( new InputOption('--host', null, InputOption::VALUE_REQUIRED, 'The address the server should listen to', $defaultHost) ); $app->add($command = new ServerDumpCommand(new DumpServer($host, $logger))) ->getApplication() ->setDefaultCommand($command->getName(), true) ->run($input, $output) ; var-dumper/Resources/js/htmlDescriptor.js 0000644 00000000542 15025017654 0014563 0 ustar 00 document.addEventListener('DOMContentLoaded', function() { let prev = null; Array.from(document.getElementsByTagName('article')).reverse().forEach(function (article) { const dedupId = article.dataset.dedupId; if (dedupId === prev) { article.getElementsByTagName('header')[0].classList.add('hidden'); } prev = dedupId; }); }); var-dumper/Resources/functions/dump.php 0000644 00000001772 15025017654 0014302 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Component\VarDumper\VarDumper; if (!function_exists('dump')) { /** * @author Nicolas Grekas <p@tchwork.com> */ function dump(mixed $var, mixed ...$moreVars): mixed { VarDumper::dump($var); foreach ($moreVars as $v) { VarDumper::dump($v); } if (1 < func_num_args()) { return func_get_args(); } return $var; } } if (!function_exists('dd')) { /** * @return never */ function dd(...$vars): void { if (!in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && !headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } foreach ($vars as $v) { VarDumper::dump($v); } exit(1); } } polyfill-intl-normalizer/bootstrap.php 0000644 00000001325 15025017654 0014256 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Intl\Normalizer as p; if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; } if (!function_exists('normalizer_is_normalized')) { function normalizer_is_normalized($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::isNormalized($string, $form); } } if (!function_exists('normalizer_normalize')) { function normalizer_normalize($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::normalize($string, $form); } } polyfill-intl-normalizer/composer.json 0000644 00000002103 15025017654 0014245 0 ustar 00 { "name": "symfony/polyfill-intl-normalizer", "type": "library", "description": "Symfony polyfill for intl's Normalizer class and related functions", "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "normalizer"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.1" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Intl\\Normalizer\\": "" }, "files": [ "bootstrap.php" ], "classmap": [ "Resources/stubs" ] }, "suggest": { "ext-intl": "For best performance" }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } polyfill-intl-normalizer/README.md 0000644 00000000660 15025017654 0013010 0 ustar 00 Symfony Polyfill / Intl: Normalizer =================================== This component provides a fallback implementation for the [`Normalizer`](https://php.net/Normalizer) class provided by the [Intl](https://php.net/intl) extension. More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). polyfill-intl-normalizer/LICENSE 0000644 00000002051 15025017654 0012532 0 ustar 00 Copyright (c) 2015-2019 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. polyfill-intl-normalizer/Normalizer.php 0000644 00000022544 15025017654 0014371 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Intl\Normalizer; /** * Normalizer is a PHP fallback implementation of the Normalizer class provided by the intl extension. * * It has been validated with Unicode 6.3 Normalization Conformance Test. * See http://www.unicode.org/reports/tr15/ for detailed info about Unicode normalizations. * * @author Nicolas Grekas <p@tchwork.com> * * @internal */ class Normalizer { public const FORM_D = \Normalizer::FORM_D; public const FORM_KD = \Normalizer::FORM_KD; public const FORM_C = \Normalizer::FORM_C; public const FORM_KC = \Normalizer::FORM_KC; public const NFD = \Normalizer::NFD; public const NFKD = \Normalizer::NFKD; public const NFC = \Normalizer::NFC; public const NFKC = \Normalizer::NFKC; private static $C; private static $D; private static $KD; private static $cC; private static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; private static $ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; public static function isNormalized(string $s, int $form = self::FORM_C) { if (!\in_array($form, [self::NFD, self::NFKD, self::NFC, self::NFKC])) { return false; } if (!isset($s[strspn($s, self::$ASCII)])) { return true; } if (self::NFC == $form && preg_match('//u', $s) && !preg_match('/[^\x00-\x{2FF}]/u', $s)) { return true; } return self::normalize($s, $form) === $s; } public static function normalize(string $s, int $form = self::FORM_C) { if (!preg_match('//u', $s)) { return false; } switch ($form) { case self::NFC: $C = true; $K = false; break; case self::NFD: $C = false; $K = false; break; case self::NFKC: $C = true; $K = true; break; case self::NFKD: $C = false; $K = true; break; default: if (\defined('Normalizer::NONE') && \Normalizer::NONE == $form) { return $s; } if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('normalizer_normalize(): Argument #2 ($form) must be a a valid normalization form'); } if ('' === $s) { return ''; } if ($K && null === self::$KD) { self::$KD = self::getData('compatibilityDecomposition'); } if (null === self::$D) { self::$D = self::getData('canonicalDecomposition'); self::$cC = self::getData('combiningClass'); } if (null !== $mbEncoding = (2 /* MB_OVERLOAD_STRING */ & (int) \ini_get('mbstring.func_overload')) ? mb_internal_encoding() : null) { mb_internal_encoding('8bit'); } $r = self::decompose($s, $K); if ($C) { if (null === self::$C) { self::$C = self::getData('canonicalComposition'); } $r = self::recompose($r); } if (null !== $mbEncoding) { mb_internal_encoding($mbEncoding); } return $r; } private static function recompose($s) { $ASCII = self::$ASCII; $compMap = self::$C; $combClass = self::$cC; $ulenMask = self::$ulenMask; $result = $tail = ''; $i = $s[0] < "\x80" ? 1 : $ulenMask[$s[0] & "\xF0"]; $len = \strlen($s); $lastUchr = substr($s, 0, $i); $lastUcls = isset($combClass[$lastUchr]) ? 256 : 0; while ($i < $len) { if ($s[$i] < "\x80") { // ASCII chars if ($tail) { $lastUchr .= $tail; $tail = ''; } if ($j = strspn($s, $ASCII, $i + 1)) { $lastUchr .= substr($s, $i, $j); $i += $j; } $result .= $lastUchr; $lastUchr = $s[$i]; $lastUcls = 0; ++$i; continue; } $ulen = $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); if ($lastUchr < "\xE1\x84\x80" || "\xE1\x84\x92" < $lastUchr || $uchr < "\xE1\x85\xA1" || "\xE1\x85\xB5" < $uchr || $lastUcls) { // Table lookup and combining chars composition $ucls = $combClass[$uchr] ?? 0; if (isset($compMap[$lastUchr.$uchr]) && (!$lastUcls || $lastUcls < $ucls)) { $lastUchr = $compMap[$lastUchr.$uchr]; } elseif ($lastUcls = $ucls) { $tail .= $uchr; } else { if ($tail) { $lastUchr .= $tail; $tail = ''; } $result .= $lastUchr; $lastUchr = $uchr; } } else { // Hangul chars $L = \ord($lastUchr[2]) - 0x80; $V = \ord($uchr[2]) - 0xA1; $T = 0; $uchr = substr($s, $i + $ulen, 3); if ("\xE1\x86\xA7" <= $uchr && $uchr <= "\xE1\x87\x82") { $T = \ord($uchr[2]) - 0xA7; 0 > $T && $T += 0x40; $ulen += 3; } $L = 0xAC00 + ($L * 21 + $V) * 28 + $T; $lastUchr = \chr(0xE0 | $L >> 12).\chr(0x80 | $L >> 6 & 0x3F).\chr(0x80 | $L & 0x3F); } $i += $ulen; } return $result.$lastUchr.$tail; } private static function decompose($s, $c) { $result = ''; $ASCII = self::$ASCII; $decompMap = self::$D; $combClass = self::$cC; $ulenMask = self::$ulenMask; if ($c) { $compatMap = self::$KD; } $c = []; $i = 0; $len = \strlen($s); while ($i < $len) { if ($s[$i] < "\x80") { // ASCII chars if ($c) { ksort($c); $result .= implode('', $c); $c = []; } $j = 1 + strspn($s, $ASCII, $i + 1); $result .= substr($s, $i, $j); $i += $j; continue; } $ulen = $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; if ($uchr < "\xEA\xB0\x80" || "\xED\x9E\xA3" < $uchr) { // Table lookup if ($uchr !== $j = $compatMap[$uchr] ?? ($decompMap[$uchr] ?? $uchr)) { $uchr = $j; $j = \strlen($uchr); $ulen = $uchr[0] < "\x80" ? 1 : $ulenMask[$uchr[0] & "\xF0"]; if ($ulen != $j) { // Put trailing chars in $s $j -= $ulen; $i -= $j; if (0 > $i) { $s = str_repeat(' ', -$i).$s; $len -= $i; $i = 0; } while ($j--) { $s[$i + $j] = $uchr[$ulen + $j]; } $uchr = substr($uchr, 0, $ulen); } } if (isset($combClass[$uchr])) { // Combining chars, for sorting if (!isset($c[$combClass[$uchr]])) { $c[$combClass[$uchr]] = ''; } $c[$combClass[$uchr]] .= $uchr; continue; } } else { // Hangul chars $uchr = unpack('C*', $uchr); $j = (($uchr[1] - 224) << 12) + (($uchr[2] - 128) << 6) + $uchr[3] - 0xAC80; $uchr = "\xE1\x84".\chr(0x80 + (int) ($j / 588)) ."\xE1\x85".\chr(0xA1 + (int) (($j % 588) / 28)); if ($j %= 28) { $uchr .= $j < 25 ? ("\xE1\x86".\chr(0xA7 + $j)) : ("\xE1\x87".\chr(0x67 + $j)); } } if ($c) { ksort($c); $result .= implode('', $c); $c = []; } $result .= $uchr; } if ($c) { ksort($c); $result .= implode('', $c); } return $result; } private static function getData($file) { if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { return require $file; } return false; } } polyfill-intl-normalizer/bootstrap80.php 0000644 00000001317 15025017654 0014427 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Intl\Normalizer as p; if (!function_exists('normalizer_is_normalized')) { function normalizer_is_normalized(?string $string, ?int $form = p\Normalizer::FORM_C): bool { return p\Normalizer::isNormalized((string) $string, (int) $form); } } if (!function_exists('normalizer_normalize')) { function normalizer_normalize(?string $string, ?int $form = p\Normalizer::FORM_C): string|false { return p\Normalizer::normalize((string) $string, (int) $form); } } polyfill-intl-normalizer/Resources/unidata/canonicalComposition.php 0000644 00000042343 15025017654 0022020 0 ustar 00 <?php return array ( 'À' => 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', 'Ç' => 'Ç', 'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'ç' => 'ç', 'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'ÿ' => 'ÿ', 'Ā' => 'Ā', 'ā' => 'ā', 'Ă' => 'Ă', 'ă' => 'ă', 'Ą' => 'Ą', 'ą' => 'ą', 'Ć' => 'Ć', 'ć' => 'ć', 'Ĉ' => 'Ĉ', 'ĉ' => 'ĉ', 'Ċ' => 'Ċ', 'ċ' => 'ċ', 'Č' => 'Č', 'č' => 'č', 'Ď' => 'Ď', 'ď' => 'ď', 'Ē' => 'Ē', 'ē' => 'ē', 'Ĕ' => 'Ĕ', 'ĕ' => 'ĕ', 'Ė' => 'Ė', 'ė' => 'ė', 'Ę' => 'Ę', 'ę' => 'ę', 'Ě' => 'Ě', 'ě' => 'ě', 'Ĝ' => 'Ĝ', 'ĝ' => 'ĝ', 'Ğ' => 'Ğ', 'ğ' => 'ğ', 'Ġ' => 'Ġ', 'ġ' => 'ġ', 'Ģ' => 'Ģ', 'ģ' => 'ģ', 'Ĥ' => 'Ĥ', 'ĥ' => 'ĥ', 'Ĩ' => 'Ĩ', 'ĩ' => 'ĩ', 'Ī' => 'Ī', 'ī' => 'ī', 'Ĭ' => 'Ĭ', 'ĭ' => 'ĭ', 'Į' => 'Į', 'į' => 'į', 'İ' => 'İ', 'Ĵ' => 'Ĵ', 'ĵ' => 'ĵ', 'Ķ' => 'Ķ', 'ķ' => 'ķ', 'Ĺ' => 'Ĺ', 'ĺ' => 'ĺ', 'Ļ' => 'Ļ', 'ļ' => 'ļ', 'Ľ' => 'Ľ', 'ľ' => 'ľ', 'Ń' => 'Ń', 'ń' => 'ń', 'Ņ' => 'Ņ', 'ņ' => 'ņ', 'Ň' => 'Ň', 'ň' => 'ň', 'Ō' => 'Ō', 'ō' => 'ō', 'Ŏ' => 'Ŏ', 'ŏ' => 'ŏ', 'Ő' => 'Ő', 'ő' => 'ő', 'Ŕ' => 'Ŕ', 'ŕ' => 'ŕ', 'Ŗ' => 'Ŗ', 'ŗ' => 'ŗ', 'Ř' => 'Ř', 'ř' => 'ř', 'Ś' => 'Ś', 'ś' => 'ś', 'Ŝ' => 'Ŝ', 'ŝ' => 'ŝ', 'Ş' => 'Ş', 'ş' => 'ş', 'Š' => 'Š', 'š' => 'š', 'Ţ' => 'Ţ', 'ţ' => 'ţ', 'Ť' => 'Ť', 'ť' => 'ť', 'Ũ' => 'Ũ', 'ũ' => 'ũ', 'Ū' => 'Ū', 'ū' => 'ū', 'Ŭ' => 'Ŭ', 'ŭ' => 'ŭ', 'Ů' => 'Ů', 'ů' => 'ů', 'Ű' => 'Ű', 'ű' => 'ű', 'Ų' => 'Ų', 'ų' => 'ų', 'Ŵ' => 'Ŵ', 'ŵ' => 'ŵ', 'Ŷ' => 'Ŷ', 'ŷ' => 'ŷ', 'Ÿ' => 'Ÿ', 'Ź' => 'Ź', 'ź' => 'ź', 'Ż' => 'Ż', 'ż' => 'ż', 'Ž' => 'Ž', 'ž' => 'ž', 'Ơ' => 'Ơ', 'ơ' => 'ơ', 'Ư' => 'Ư', 'ư' => 'ư', 'Ǎ' => 'Ǎ', 'ǎ' => 'ǎ', 'Ǐ' => 'Ǐ', 'ǐ' => 'ǐ', 'Ǒ' => 'Ǒ', 'ǒ' => 'ǒ', 'Ǔ' => 'Ǔ', 'ǔ' => 'ǔ', 'Ǖ' => 'Ǖ', 'ǖ' => 'ǖ', 'Ǘ' => 'Ǘ', 'ǘ' => 'ǘ', 'Ǚ' => 'Ǚ', 'ǚ' => 'ǚ', 'Ǜ' => 'Ǜ', 'ǜ' => 'ǜ', 'Ǟ' => 'Ǟ', 'ǟ' => 'ǟ', 'Ǡ' => 'Ǡ', 'ǡ' => 'ǡ', 'Ǣ' => 'Ǣ', 'ǣ' => 'ǣ', 'Ǧ' => 'Ǧ', 'ǧ' => 'ǧ', 'Ǩ' => 'Ǩ', 'ǩ' => 'ǩ', 'Ǫ' => 'Ǫ', 'ǫ' => 'ǫ', 'Ǭ' => 'Ǭ', 'ǭ' => 'ǭ', 'Ǯ' => 'Ǯ', 'ǯ' => 'ǯ', 'ǰ' => 'ǰ', 'Ǵ' => 'Ǵ', 'ǵ' => 'ǵ', 'Ǹ' => 'Ǹ', 'ǹ' => 'ǹ', 'Ǻ' => 'Ǻ', 'ǻ' => 'ǻ', 'Ǽ' => 'Ǽ', 'ǽ' => 'ǽ', 'Ǿ' => 'Ǿ', 'ǿ' => 'ǿ', 'Ȁ' => 'Ȁ', 'ȁ' => 'ȁ', 'Ȃ' => 'Ȃ', 'ȃ' => 'ȃ', 'Ȅ' => 'Ȅ', 'ȅ' => 'ȅ', 'Ȇ' => 'Ȇ', 'ȇ' => 'ȇ', 'Ȉ' => 'Ȉ', 'ȉ' => 'ȉ', 'Ȋ' => 'Ȋ', 'ȋ' => 'ȋ', 'Ȍ' => 'Ȍ', 'ȍ' => 'ȍ', 'Ȏ' => 'Ȏ', 'ȏ' => 'ȏ', 'Ȑ' => 'Ȑ', 'ȑ' => 'ȑ', 'Ȓ' => 'Ȓ', 'ȓ' => 'ȓ', 'Ȕ' => 'Ȕ', 'ȕ' => 'ȕ', 'Ȗ' => 'Ȗ', 'ȗ' => 'ȗ', 'Ș' => 'Ș', 'ș' => 'ș', 'Ț' => 'Ț', 'ț' => 'ț', 'Ȟ' => 'Ȟ', 'ȟ' => 'ȟ', 'Ȧ' => 'Ȧ', 'ȧ' => 'ȧ', 'Ȩ' => 'Ȩ', 'ȩ' => 'ȩ', 'Ȫ' => 'Ȫ', 'ȫ' => 'ȫ', 'Ȭ' => 'Ȭ', 'ȭ' => 'ȭ', 'Ȯ' => 'Ȯ', 'ȯ' => 'ȯ', 'Ȱ' => 'Ȱ', 'ȱ' => 'ȱ', 'Ȳ' => 'Ȳ', 'ȳ' => 'ȳ', '΅' => '΅', 'Ά' => 'Ά', 'Έ' => 'Έ', 'Ή' => 'Ή', 'Ί' => 'Ί', 'Ό' => 'Ό', 'Ύ' => 'Ύ', 'Ώ' => 'Ώ', 'ΐ' => 'ΐ', 'Ϊ' => 'Ϊ', 'Ϋ' => 'Ϋ', 'ά' => 'ά', 'έ' => 'έ', 'ή' => 'ή', 'ί' => 'ί', 'ΰ' => 'ΰ', 'ϊ' => 'ϊ', 'ϋ' => 'ϋ', 'ό' => 'ό', 'ύ' => 'ύ', 'ώ' => 'ώ', 'ϓ' => 'ϓ', 'ϔ' => 'ϔ', 'Ѐ' => 'Ѐ', 'Ё' => 'Ё', 'Ѓ' => 'Ѓ', 'Ї' => 'Ї', 'Ќ' => 'Ќ', 'Ѝ' => 'Ѝ', 'Ў' => 'Ў', 'Й' => 'Й', 'й' => 'й', 'ѐ' => 'ѐ', 'ё' => 'ё', 'ѓ' => 'ѓ', 'ї' => 'ї', 'ќ' => 'ќ', 'ѝ' => 'ѝ', 'ў' => 'ў', 'Ѷ' => 'Ѷ', 'ѷ' => 'ѷ', 'Ӂ' => 'Ӂ', 'ӂ' => 'ӂ', 'Ӑ' => 'Ӑ', 'ӑ' => 'ӑ', 'Ӓ' => 'Ӓ', 'ӓ' => 'ӓ', 'Ӗ' => 'Ӗ', 'ӗ' => 'ӗ', 'Ӛ' => 'Ӛ', 'ӛ' => 'ӛ', 'Ӝ' => 'Ӝ', 'ӝ' => 'ӝ', 'Ӟ' => 'Ӟ', 'ӟ' => 'ӟ', 'Ӣ' => 'Ӣ', 'ӣ' => 'ӣ', 'Ӥ' => 'Ӥ', 'ӥ' => 'ӥ', 'Ӧ' => 'Ӧ', 'ӧ' => 'ӧ', 'Ӫ' => 'Ӫ', 'ӫ' => 'ӫ', 'Ӭ' => 'Ӭ', 'ӭ' => 'ӭ', 'Ӯ' => 'Ӯ', 'ӯ' => 'ӯ', 'Ӱ' => 'Ӱ', 'ӱ' => 'ӱ', 'Ӳ' => 'Ӳ', 'ӳ' => 'ӳ', 'Ӵ' => 'Ӵ', 'ӵ' => 'ӵ', 'Ӹ' => 'Ӹ', 'ӹ' => 'ӹ', 'آ' => 'آ', 'أ' => 'أ', 'ؤ' => 'ؤ', 'إ' => 'إ', 'ئ' => 'ئ', 'ۀ' => 'ۀ', 'ۂ' => 'ۂ', 'ۓ' => 'ۓ', 'ऩ' => 'ऩ', 'ऱ' => 'ऱ', 'ऴ' => 'ऴ', 'ো' => 'ো', 'ৌ' => 'ৌ', 'ୈ' => 'ୈ', 'ୋ' => 'ୋ', 'ୌ' => 'ୌ', 'ஔ' => 'ஔ', 'ொ' => 'ொ', 'ோ' => 'ோ', 'ௌ' => 'ௌ', 'ై' => 'ై', 'ೀ' => 'ೀ', 'ೇ' => 'ೇ', 'ೈ' => 'ೈ', 'ೊ' => 'ೊ', 'ೋ' => 'ೋ', 'ൊ' => 'ൊ', 'ോ' => 'ോ', 'ൌ' => 'ൌ', 'ේ' => 'ේ', 'ො' => 'ො', 'ෝ' => 'ෝ', 'ෞ' => 'ෞ', 'ဦ' => 'ဦ', 'ᬆ' => 'ᬆ', 'ᬈ' => 'ᬈ', 'ᬊ' => 'ᬊ', 'ᬌ' => 'ᬌ', 'ᬎ' => 'ᬎ', 'ᬒ' => 'ᬒ', 'ᬻ' => 'ᬻ', 'ᬽ' => 'ᬽ', 'ᭀ' => 'ᭀ', 'ᭁ' => 'ᭁ', 'ᭃ' => 'ᭃ', 'Ḁ' => 'Ḁ', 'ḁ' => 'ḁ', 'Ḃ' => 'Ḃ', 'ḃ' => 'ḃ', 'Ḅ' => 'Ḅ', 'ḅ' => 'ḅ', 'Ḇ' => 'Ḇ', 'ḇ' => 'ḇ', 'Ḉ' => 'Ḉ', 'ḉ' => 'ḉ', 'Ḋ' => 'Ḋ', 'ḋ' => 'ḋ', 'Ḍ' => 'Ḍ', 'ḍ' => 'ḍ', 'Ḏ' => 'Ḏ', 'ḏ' => 'ḏ', 'Ḑ' => 'Ḑ', 'ḑ' => 'ḑ', 'Ḓ' => 'Ḓ', 'ḓ' => 'ḓ', 'Ḕ' => 'Ḕ', 'ḕ' => 'ḕ', 'Ḗ' => 'Ḗ', 'ḗ' => 'ḗ', 'Ḙ' => 'Ḙ', 'ḙ' => 'ḙ', 'Ḛ' => 'Ḛ', 'ḛ' => 'ḛ', 'Ḝ' => 'Ḝ', 'ḝ' => 'ḝ', 'Ḟ' => 'Ḟ', 'ḟ' => 'ḟ', 'Ḡ' => 'Ḡ', 'ḡ' => 'ḡ', 'Ḣ' => 'Ḣ', 'ḣ' => 'ḣ', 'Ḥ' => 'Ḥ', 'ḥ' => 'ḥ', 'Ḧ' => 'Ḧ', 'ḧ' => 'ḧ', 'Ḩ' => 'Ḩ', 'ḩ' => 'ḩ', 'Ḫ' => 'Ḫ', 'ḫ' => 'ḫ', 'Ḭ' => 'Ḭ', 'ḭ' => 'ḭ', 'Ḯ' => 'Ḯ', 'ḯ' => 'ḯ', 'Ḱ' => 'Ḱ', 'ḱ' => 'ḱ', 'Ḳ' => 'Ḳ', 'ḳ' => 'ḳ', 'Ḵ' => 'Ḵ', 'ḵ' => 'ḵ', 'Ḷ' => 'Ḷ', 'ḷ' => 'ḷ', 'Ḹ' => 'Ḹ', 'ḹ' => 'ḹ', 'Ḻ' => 'Ḻ', 'ḻ' => 'ḻ', 'Ḽ' => 'Ḽ', 'ḽ' => 'ḽ', 'Ḿ' => 'Ḿ', 'ḿ' => 'ḿ', 'Ṁ' => 'Ṁ', 'ṁ' => 'ṁ', 'Ṃ' => 'Ṃ', 'ṃ' => 'ṃ', 'Ṅ' => 'Ṅ', 'ṅ' => 'ṅ', 'Ṇ' => 'Ṇ', 'ṇ' => 'ṇ', 'Ṉ' => 'Ṉ', 'ṉ' => 'ṉ', 'Ṋ' => 'Ṋ', 'ṋ' => 'ṋ', 'Ṍ' => 'Ṍ', 'ṍ' => 'ṍ', 'Ṏ' => 'Ṏ', 'ṏ' => 'ṏ', 'Ṑ' => 'Ṑ', 'ṑ' => 'ṑ', 'Ṓ' => 'Ṓ', 'ṓ' => 'ṓ', 'Ṕ' => 'Ṕ', 'ṕ' => 'ṕ', 'Ṗ' => 'Ṗ', 'ṗ' => 'ṗ', 'Ṙ' => 'Ṙ', 'ṙ' => 'ṙ', 'Ṛ' => 'Ṛ', 'ṛ' => 'ṛ', 'Ṝ' => 'Ṝ', 'ṝ' => 'ṝ', 'Ṟ' => 'Ṟ', 'ṟ' => 'ṟ', 'Ṡ' => 'Ṡ', 'ṡ' => 'ṡ', 'Ṣ' => 'Ṣ', 'ṣ' => 'ṣ', 'Ṥ' => 'Ṥ', 'ṥ' => 'ṥ', 'Ṧ' => 'Ṧ', 'ṧ' => 'ṧ', 'Ṩ' => 'Ṩ', 'ṩ' => 'ṩ', 'Ṫ' => 'Ṫ', 'ṫ' => 'ṫ', 'Ṭ' => 'Ṭ', 'ṭ' => 'ṭ', 'Ṯ' => 'Ṯ', 'ṯ' => 'ṯ', 'Ṱ' => 'Ṱ', 'ṱ' => 'ṱ', 'Ṳ' => 'Ṳ', 'ṳ' => 'ṳ', 'Ṵ' => 'Ṵ', 'ṵ' => 'ṵ', 'Ṷ' => 'Ṷ', 'ṷ' => 'ṷ', 'Ṹ' => 'Ṹ', 'ṹ' => 'ṹ', 'Ṻ' => 'Ṻ', 'ṻ' => 'ṻ', 'Ṽ' => 'Ṽ', 'ṽ' => 'ṽ', 'Ṿ' => 'Ṿ', 'ṿ' => 'ṿ', 'Ẁ' => 'Ẁ', 'ẁ' => 'ẁ', 'Ẃ' => 'Ẃ', 'ẃ' => 'ẃ', 'Ẅ' => 'Ẅ', 'ẅ' => 'ẅ', 'Ẇ' => 'Ẇ', 'ẇ' => 'ẇ', 'Ẉ' => 'Ẉ', 'ẉ' => 'ẉ', 'Ẋ' => 'Ẋ', 'ẋ' => 'ẋ', 'Ẍ' => 'Ẍ', 'ẍ' => 'ẍ', 'Ẏ' => 'Ẏ', 'ẏ' => 'ẏ', 'Ẑ' => 'Ẑ', 'ẑ' => 'ẑ', 'Ẓ' => 'Ẓ', 'ẓ' => 'ẓ', 'Ẕ' => 'Ẕ', 'ẕ' => 'ẕ', 'ẖ' => 'ẖ', 'ẗ' => 'ẗ', 'ẘ' => 'ẘ', 'ẙ' => 'ẙ', 'ẛ' => 'ẛ', 'Ạ' => 'Ạ', 'ạ' => 'ạ', 'Ả' => 'Ả', 'ả' => 'ả', 'Ấ' => 'Ấ', 'ấ' => 'ấ', 'Ầ' => 'Ầ', 'ầ' => 'ầ', 'Ẩ' => 'Ẩ', 'ẩ' => 'ẩ', 'Ẫ' => 'Ẫ', 'ẫ' => 'ẫ', 'Ậ' => 'Ậ', 'ậ' => 'ậ', 'Ắ' => 'Ắ', 'ắ' => 'ắ', 'Ằ' => 'Ằ', 'ằ' => 'ằ', 'Ẳ' => 'Ẳ', 'ẳ' => 'ẳ', 'Ẵ' => 'Ẵ', 'ẵ' => 'ẵ', 'Ặ' => 'Ặ', 'ặ' => 'ặ', 'Ẹ' => 'Ẹ', 'ẹ' => 'ẹ', 'Ẻ' => 'Ẻ', 'ẻ' => 'ẻ', 'Ẽ' => 'Ẽ', 'ẽ' => 'ẽ', 'Ế' => 'Ế', 'ế' => 'ế', 'Ề' => 'Ề', 'ề' => 'ề', 'Ể' => 'Ể', 'ể' => 'ể', 'Ễ' => 'Ễ', 'ễ' => 'ễ', 'Ệ' => 'Ệ', 'ệ' => 'ệ', 'Ỉ' => 'Ỉ', 'ỉ' => 'ỉ', 'Ị' => 'Ị', 'ị' => 'ị', 'Ọ' => 'Ọ', 'ọ' => 'ọ', 'Ỏ' => 'Ỏ', 'ỏ' => 'ỏ', 'Ố' => 'Ố', 'ố' => 'ố', 'Ồ' => 'Ồ', 'ồ' => 'ồ', 'Ổ' => 'Ổ', 'ổ' => 'ổ', 'Ỗ' => 'Ỗ', 'ỗ' => 'ỗ', 'Ộ' => 'Ộ', 'ộ' => 'ộ', 'Ớ' => 'Ớ', 'ớ' => 'ớ', 'Ờ' => 'Ờ', 'ờ' => 'ờ', 'Ở' => 'Ở', 'ở' => 'ở', 'Ỡ' => 'Ỡ', 'ỡ' => 'ỡ', 'Ợ' => 'Ợ', 'ợ' => 'ợ', 'Ụ' => 'Ụ', 'ụ' => 'ụ', 'Ủ' => 'Ủ', 'ủ' => 'ủ', 'Ứ' => 'Ứ', 'ứ' => 'ứ', 'Ừ' => 'Ừ', 'ừ' => 'ừ', 'Ử' => 'Ử', 'ử' => 'ử', 'Ữ' => 'Ữ', 'ữ' => 'ữ', 'Ự' => 'Ự', 'ự' => 'ự', 'Ỳ' => 'Ỳ', 'ỳ' => 'ỳ', 'Ỵ' => 'Ỵ', 'ỵ' => 'ỵ', 'Ỷ' => 'Ỷ', 'ỷ' => 'ỷ', 'Ỹ' => 'Ỹ', 'ỹ' => 'ỹ', 'ἀ' => 'ἀ', 'ἁ' => 'ἁ', 'ἂ' => 'ἂ', 'ἃ' => 'ἃ', 'ἄ' => 'ἄ', 'ἅ' => 'ἅ', 'ἆ' => 'ἆ', 'ἇ' => 'ἇ', 'Ἀ' => 'Ἀ', 'Ἁ' => 'Ἁ', 'Ἂ' => 'Ἂ', 'Ἃ' => 'Ἃ', 'Ἄ' => 'Ἄ', 'Ἅ' => 'Ἅ', 'Ἆ' => 'Ἆ', 'Ἇ' => 'Ἇ', 'ἐ' => 'ἐ', 'ἑ' => 'ἑ', 'ἒ' => 'ἒ', 'ἓ' => 'ἓ', 'ἔ' => 'ἔ', 'ἕ' => 'ἕ', 'Ἐ' => 'Ἐ', 'Ἑ' => 'Ἑ', 'Ἒ' => 'Ἒ', 'Ἓ' => 'Ἓ', 'Ἔ' => 'Ἔ', 'Ἕ' => 'Ἕ', 'ἠ' => 'ἠ', 'ἡ' => 'ἡ', 'ἢ' => 'ἢ', 'ἣ' => 'ἣ', 'ἤ' => 'ἤ', 'ἥ' => 'ἥ', 'ἦ' => 'ἦ', 'ἧ' => 'ἧ', 'Ἠ' => 'Ἠ', 'Ἡ' => 'Ἡ', 'Ἢ' => 'Ἢ', 'Ἣ' => 'Ἣ', 'Ἤ' => 'Ἤ', 'Ἥ' => 'Ἥ', 'Ἦ' => 'Ἦ', 'Ἧ' => 'Ἧ', 'ἰ' => 'ἰ', 'ἱ' => 'ἱ', 'ἲ' => 'ἲ', 'ἳ' => 'ἳ', 'ἴ' => 'ἴ', 'ἵ' => 'ἵ', 'ἶ' => 'ἶ', 'ἷ' => 'ἷ', 'Ἰ' => 'Ἰ', 'Ἱ' => 'Ἱ', 'Ἲ' => 'Ἲ', 'Ἳ' => 'Ἳ', 'Ἴ' => 'Ἴ', 'Ἵ' => 'Ἵ', 'Ἶ' => 'Ἶ', 'Ἷ' => 'Ἷ', 'ὀ' => 'ὀ', 'ὁ' => 'ὁ', 'ὂ' => 'ὂ', 'ὃ' => 'ὃ', 'ὄ' => 'ὄ', 'ὅ' => 'ὅ', 'Ὀ' => 'Ὀ', 'Ὁ' => 'Ὁ', 'Ὂ' => 'Ὂ', 'Ὃ' => 'Ὃ', 'Ὄ' => 'Ὄ', 'Ὅ' => 'Ὅ', 'ὐ' => 'ὐ', 'ὑ' => 'ὑ', 'ὒ' => 'ὒ', 'ὓ' => 'ὓ', 'ὔ' => 'ὔ', 'ὕ' => 'ὕ', 'ὖ' => 'ὖ', 'ὗ' => 'ὗ', 'Ὑ' => 'Ὑ', 'Ὓ' => 'Ὓ', 'Ὕ' => 'Ὕ', 'Ὗ' => 'Ὗ', 'ὠ' => 'ὠ', 'ὡ' => 'ὡ', 'ὢ' => 'ὢ', 'ὣ' => 'ὣ', 'ὤ' => 'ὤ', 'ὥ' => 'ὥ', 'ὦ' => 'ὦ', 'ὧ' => 'ὧ', 'Ὠ' => 'Ὠ', 'Ὡ' => 'Ὡ', 'Ὢ' => 'Ὢ', 'Ὣ' => 'Ὣ', 'Ὤ' => 'Ὤ', 'Ὥ' => 'Ὥ', 'Ὦ' => 'Ὦ', 'Ὧ' => 'Ὧ', 'ὰ' => 'ὰ', 'ὲ' => 'ὲ', 'ὴ' => 'ὴ', 'ὶ' => 'ὶ', 'ὸ' => 'ὸ', 'ὺ' => 'ὺ', 'ὼ' => 'ὼ', 'ᾀ' => 'ᾀ', 'ᾁ' => 'ᾁ', 'ᾂ' => 'ᾂ', 'ᾃ' => 'ᾃ', 'ᾄ' => 'ᾄ', 'ᾅ' => 'ᾅ', 'ᾆ' => 'ᾆ', 'ᾇ' => 'ᾇ', 'ᾈ' => 'ᾈ', 'ᾉ' => 'ᾉ', 'ᾊ' => 'ᾊ', 'ᾋ' => 'ᾋ', 'ᾌ' => 'ᾌ', 'ᾍ' => 'ᾍ', 'ᾎ' => 'ᾎ', 'ᾏ' => 'ᾏ', 'ᾐ' => 'ᾐ', 'ᾑ' => 'ᾑ', 'ᾒ' => 'ᾒ', 'ᾓ' => 'ᾓ', 'ᾔ' => 'ᾔ', 'ᾕ' => 'ᾕ', 'ᾖ' => 'ᾖ', 'ᾗ' => 'ᾗ', 'ᾘ' => 'ᾘ', 'ᾙ' => 'ᾙ', 'ᾚ' => 'ᾚ', 'ᾛ' => 'ᾛ', 'ᾜ' => 'ᾜ', 'ᾝ' => 'ᾝ', 'ᾞ' => 'ᾞ', 'ᾟ' => 'ᾟ', 'ᾠ' => 'ᾠ', 'ᾡ' => 'ᾡ', 'ᾢ' => 'ᾢ', 'ᾣ' => 'ᾣ', 'ᾤ' => 'ᾤ', 'ᾥ' => 'ᾥ', 'ᾦ' => 'ᾦ', 'ᾧ' => 'ᾧ', 'ᾨ' => 'ᾨ', 'ᾩ' => 'ᾩ', 'ᾪ' => 'ᾪ', 'ᾫ' => 'ᾫ', 'ᾬ' => 'ᾬ', 'ᾭ' => 'ᾭ', 'ᾮ' => 'ᾮ', 'ᾯ' => 'ᾯ', 'ᾰ' => 'ᾰ', 'ᾱ' => 'ᾱ', 'ᾲ' => 'ᾲ', 'ᾳ' => 'ᾳ', 'ᾴ' => 'ᾴ', 'ᾶ' => 'ᾶ', 'ᾷ' => 'ᾷ', 'Ᾰ' => 'Ᾰ', 'Ᾱ' => 'Ᾱ', 'Ὰ' => 'Ὰ', 'ᾼ' => 'ᾼ', '῁' => '῁', 'ῂ' => 'ῂ', 'ῃ' => 'ῃ', 'ῄ' => 'ῄ', 'ῆ' => 'ῆ', 'ῇ' => 'ῇ', 'Ὲ' => 'Ὲ', 'Ὴ' => 'Ὴ', 'ῌ' => 'ῌ', '῍' => '῍', '῎' => '῎', '῏' => '῏', 'ῐ' => 'ῐ', 'ῑ' => 'ῑ', 'ῒ' => 'ῒ', 'ῖ' => 'ῖ', 'ῗ' => 'ῗ', 'Ῐ' => 'Ῐ', 'Ῑ' => 'Ῑ', 'Ὶ' => 'Ὶ', '῝' => '῝', '῞' => '῞', '῟' => '῟', 'ῠ' => 'ῠ', 'ῡ' => 'ῡ', 'ῢ' => 'ῢ', 'ῤ' => 'ῤ', 'ῥ' => 'ῥ', 'ῦ' => 'ῦ', 'ῧ' => 'ῧ', 'Ῠ' => 'Ῠ', 'Ῡ' => 'Ῡ', 'Ὺ' => 'Ὺ', 'Ῥ' => 'Ῥ', '῭' => '῭', 'ῲ' => 'ῲ', 'ῳ' => 'ῳ', 'ῴ' => 'ῴ', 'ῶ' => 'ῶ', 'ῷ' => 'ῷ', 'Ὸ' => 'Ὸ', 'Ὼ' => 'Ὼ', 'ῼ' => 'ῼ', '↚' => '↚', '↛' => '↛', '↮' => '↮', '⇍' => '⇍', '⇎' => '⇎', '⇏' => '⇏', '∄' => '∄', '∉' => '∉', '∌' => '∌', '∤' => '∤', '∦' => '∦', '≁' => '≁', '≄' => '≄', '≇' => '≇', '≉' => '≉', '≠' => '≠', '≢' => '≢', '≭' => '≭', '≮' => '≮', '≯' => '≯', '≰' => '≰', '≱' => '≱', '≴' => '≴', '≵' => '≵', '≸' => '≸', '≹' => '≹', '⊀' => '⊀', '⊁' => '⊁', '⊄' => '⊄', '⊅' => '⊅', '⊈' => '⊈', '⊉' => '⊉', '⊬' => '⊬', '⊭' => '⊭', '⊮' => '⊮', '⊯' => '⊯', '⋠' => '⋠', '⋡' => '⋡', '⋢' => '⋢', '⋣' => '⋣', '⋪' => '⋪', '⋫' => '⋫', '⋬' => '⋬', '⋭' => '⋭', 'が' => 'が', 'ぎ' => 'ぎ', 'ぐ' => 'ぐ', 'げ' => 'げ', 'ご' => 'ご', 'ざ' => 'ざ', 'じ' => 'じ', 'ず' => 'ず', 'ぜ' => 'ぜ', 'ぞ' => 'ぞ', 'だ' => 'だ', 'ぢ' => 'ぢ', 'づ' => 'づ', 'で' => 'で', 'ど' => 'ど', 'ば' => 'ば', 'ぱ' => 'ぱ', 'び' => 'び', 'ぴ' => 'ぴ', 'ぶ' => 'ぶ', 'ぷ' => 'ぷ', 'べ' => 'べ', 'ぺ' => 'ぺ', 'ぼ' => 'ぼ', 'ぽ' => 'ぽ', 'ゔ' => 'ゔ', 'ゞ' => 'ゞ', 'ガ' => 'ガ', 'ギ' => 'ギ', 'グ' => 'グ', 'ゲ' => 'ゲ', 'ゴ' => 'ゴ', 'ザ' => 'ザ', 'ジ' => 'ジ', 'ズ' => 'ズ', 'ゼ' => 'ゼ', 'ゾ' => 'ゾ', 'ダ' => 'ダ', 'ヂ' => 'ヂ', 'ヅ' => 'ヅ', 'デ' => 'デ', 'ド' => 'ド', 'バ' => 'バ', 'パ' => 'パ', 'ビ' => 'ビ', 'ピ' => 'ピ', 'ブ' => 'ブ', 'プ' => 'プ', 'ベ' => 'ベ', 'ペ' => 'ペ', 'ボ' => 'ボ', 'ポ' => 'ポ', 'ヴ' => 'ヴ', 'ヷ' => 'ヷ', 'ヸ' => 'ヸ', 'ヹ' => 'ヹ', 'ヺ' => 'ヺ', 'ヾ' => 'ヾ', '𑂚' => '𑂚', '𑂜' => '𑂜', '𑂫' => '𑂫', '𑄮' => '𑄮', '𑄯' => '𑄯', '𑍋' => '𑍋', '𑍌' => '𑍌', '𑒻' => '𑒻', '𑒼' => '𑒼', '𑒾' => '𑒾', '𑖺' => '𑖺', '𑖻' => '𑖻', '𑤸' => '𑤸', ); polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php 0000644 00000114173 15025017654 0022332 0 ustar 00 <?php return array ( 'À' => 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', 'Ç' => 'Ç', 'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'ç' => 'ç', 'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'ÿ' => 'ÿ', 'Ā' => 'Ā', 'ā' => 'ā', 'Ă' => 'Ă', 'ă' => 'ă', 'Ą' => 'Ą', 'ą' => 'ą', 'Ć' => 'Ć', 'ć' => 'ć', 'Ĉ' => 'Ĉ', 'ĉ' => 'ĉ', 'Ċ' => 'Ċ', 'ċ' => 'ċ', 'Č' => 'Č', 'č' => 'č', 'Ď' => 'Ď', 'ď' => 'ď', 'Ē' => 'Ē', 'ē' => 'ē', 'Ĕ' => 'Ĕ', 'ĕ' => 'ĕ', 'Ė' => 'Ė', 'ė' => 'ė', 'Ę' => 'Ę', 'ę' => 'ę', 'Ě' => 'Ě', 'ě' => 'ě', 'Ĝ' => 'Ĝ', 'ĝ' => 'ĝ', 'Ğ' => 'Ğ', 'ğ' => 'ğ', 'Ġ' => 'Ġ', 'ġ' => 'ġ', 'Ģ' => 'Ģ', 'ģ' => 'ģ', 'Ĥ' => 'Ĥ', 'ĥ' => 'ĥ', 'Ĩ' => 'Ĩ', 'ĩ' => 'ĩ', 'Ī' => 'Ī', 'ī' => 'ī', 'Ĭ' => 'Ĭ', 'ĭ' => 'ĭ', 'Į' => 'Į', 'į' => 'į', 'İ' => 'İ', 'Ĵ' => 'Ĵ', 'ĵ' => 'ĵ', 'Ķ' => 'Ķ', 'ķ' => 'ķ', 'Ĺ' => 'Ĺ', 'ĺ' => 'ĺ', 'Ļ' => 'Ļ', 'ļ' => 'ļ', 'Ľ' => 'Ľ', 'ľ' => 'ľ', 'Ń' => 'Ń', 'ń' => 'ń', 'Ņ' => 'Ņ', 'ņ' => 'ņ', 'Ň' => 'Ň', 'ň' => 'ň', 'Ō' => 'Ō', 'ō' => 'ō', 'Ŏ' => 'Ŏ', 'ŏ' => 'ŏ', 'Ő' => 'Ő', 'ő' => 'ő', 'Ŕ' => 'Ŕ', 'ŕ' => 'ŕ', 'Ŗ' => 'Ŗ', 'ŗ' => 'ŗ', 'Ř' => 'Ř', 'ř' => 'ř', 'Ś' => 'Ś', 'ś' => 'ś', 'Ŝ' => 'Ŝ', 'ŝ' => 'ŝ', 'Ş' => 'Ş', 'ş' => 'ş', 'Š' => 'Š', 'š' => 'š', 'Ţ' => 'Ţ', 'ţ' => 'ţ', 'Ť' => 'Ť', 'ť' => 'ť', 'Ũ' => 'Ũ', 'ũ' => 'ũ', 'Ū' => 'Ū', 'ū' => 'ū', 'Ŭ' => 'Ŭ', 'ŭ' => 'ŭ', 'Ů' => 'Ů', 'ů' => 'ů', 'Ű' => 'Ű', 'ű' => 'ű', 'Ų' => 'Ų', 'ų' => 'ų', 'Ŵ' => 'Ŵ', 'ŵ' => 'ŵ', 'Ŷ' => 'Ŷ', 'ŷ' => 'ŷ', 'Ÿ' => 'Ÿ', 'Ź' => 'Ź', 'ź' => 'ź', 'Ż' => 'Ż', 'ż' => 'ż', 'Ž' => 'Ž', 'ž' => 'ž', 'Ơ' => 'Ơ', 'ơ' => 'ơ', 'Ư' => 'Ư', 'ư' => 'ư', 'Ǎ' => 'Ǎ', 'ǎ' => 'ǎ', 'Ǐ' => 'Ǐ', 'ǐ' => 'ǐ', 'Ǒ' => 'Ǒ', 'ǒ' => 'ǒ', 'Ǔ' => 'Ǔ', 'ǔ' => 'ǔ', 'Ǖ' => 'Ǖ', 'ǖ' => 'ǖ', 'Ǘ' => 'Ǘ', 'ǘ' => 'ǘ', 'Ǚ' => 'Ǚ', 'ǚ' => 'ǚ', 'Ǜ' => 'Ǜ', 'ǜ' => 'ǜ', 'Ǟ' => 'Ǟ', 'ǟ' => 'ǟ', 'Ǡ' => 'Ǡ', 'ǡ' => 'ǡ', 'Ǣ' => 'Ǣ', 'ǣ' => 'ǣ', 'Ǧ' => 'Ǧ', 'ǧ' => 'ǧ', 'Ǩ' => 'Ǩ', 'ǩ' => 'ǩ', 'Ǫ' => 'Ǫ', 'ǫ' => 'ǫ', 'Ǭ' => 'Ǭ', 'ǭ' => 'ǭ', 'Ǯ' => 'Ǯ', 'ǯ' => 'ǯ', 'ǰ' => 'ǰ', 'Ǵ' => 'Ǵ', 'ǵ' => 'ǵ', 'Ǹ' => 'Ǹ', 'ǹ' => 'ǹ', 'Ǻ' => 'Ǻ', 'ǻ' => 'ǻ', 'Ǽ' => 'Ǽ', 'ǽ' => 'ǽ', 'Ǿ' => 'Ǿ', 'ǿ' => 'ǿ', 'Ȁ' => 'Ȁ', 'ȁ' => 'ȁ', 'Ȃ' => 'Ȃ', 'ȃ' => 'ȃ', 'Ȅ' => 'Ȅ', 'ȅ' => 'ȅ', 'Ȇ' => 'Ȇ', 'ȇ' => 'ȇ', 'Ȉ' => 'Ȉ', 'ȉ' => 'ȉ', 'Ȋ' => 'Ȋ', 'ȋ' => 'ȋ', 'Ȍ' => 'Ȍ', 'ȍ' => 'ȍ', 'Ȏ' => 'Ȏ', 'ȏ' => 'ȏ', 'Ȑ' => 'Ȑ', 'ȑ' => 'ȑ', 'Ȓ' => 'Ȓ', 'ȓ' => 'ȓ', 'Ȕ' => 'Ȕ', 'ȕ' => 'ȕ', 'Ȗ' => 'Ȗ', 'ȗ' => 'ȗ', 'Ș' => 'Ș', 'ș' => 'ș', 'Ț' => 'Ț', 'ț' => 'ț', 'Ȟ' => 'Ȟ', 'ȟ' => 'ȟ', 'Ȧ' => 'Ȧ', 'ȧ' => 'ȧ', 'Ȩ' => 'Ȩ', 'ȩ' => 'ȩ', 'Ȫ' => 'Ȫ', 'ȫ' => 'ȫ', 'Ȭ' => 'Ȭ', 'ȭ' => 'ȭ', 'Ȯ' => 'Ȯ', 'ȯ' => 'ȯ', 'Ȱ' => 'Ȱ', 'ȱ' => 'ȱ', 'Ȳ' => 'Ȳ', 'ȳ' => 'ȳ', '̀' => '̀', '́' => '́', '̓' => '̓', '̈́' => '̈́', 'ʹ' => 'ʹ', ';' => ';', '΅' => '΅', 'Ά' => 'Ά', '·' => '·', 'Έ' => 'Έ', 'Ή' => 'Ή', 'Ί' => 'Ί', 'Ό' => 'Ό', 'Ύ' => 'Ύ', 'Ώ' => 'Ώ', 'ΐ' => 'ΐ', 'Ϊ' => 'Ϊ', 'Ϋ' => 'Ϋ', 'ά' => 'ά', 'έ' => 'έ', 'ή' => 'ή', 'ί' => 'ί', 'ΰ' => 'ΰ', 'ϊ' => 'ϊ', 'ϋ' => 'ϋ', 'ό' => 'ό', 'ύ' => 'ύ', 'ώ' => 'ώ', 'ϓ' => 'ϓ', 'ϔ' => 'ϔ', 'Ѐ' => 'Ѐ', 'Ё' => 'Ё', 'Ѓ' => 'Ѓ', 'Ї' => 'Ї', 'Ќ' => 'Ќ', 'Ѝ' => 'Ѝ', 'Ў' => 'Ў', 'Й' => 'Й', 'й' => 'й', 'ѐ' => 'ѐ', 'ё' => 'ё', 'ѓ' => 'ѓ', 'ї' => 'ї', 'ќ' => 'ќ', 'ѝ' => 'ѝ', 'ў' => 'ў', 'Ѷ' => 'Ѷ', 'ѷ' => 'ѷ', 'Ӂ' => 'Ӂ', 'ӂ' => 'ӂ', 'Ӑ' => 'Ӑ', 'ӑ' => 'ӑ', 'Ӓ' => 'Ӓ', 'ӓ' => 'ӓ', 'Ӗ' => 'Ӗ', 'ӗ' => 'ӗ', 'Ӛ' => 'Ӛ', 'ӛ' => 'ӛ', 'Ӝ' => 'Ӝ', 'ӝ' => 'ӝ', 'Ӟ' => 'Ӟ', 'ӟ' => 'ӟ', 'Ӣ' => 'Ӣ', 'ӣ' => 'ӣ', 'Ӥ' => 'Ӥ', 'ӥ' => 'ӥ', 'Ӧ' => 'Ӧ', 'ӧ' => 'ӧ', 'Ӫ' => 'Ӫ', 'ӫ' => 'ӫ', 'Ӭ' => 'Ӭ', 'ӭ' => 'ӭ', 'Ӯ' => 'Ӯ', 'ӯ' => 'ӯ', 'Ӱ' => 'Ӱ', 'ӱ' => 'ӱ', 'Ӳ' => 'Ӳ', 'ӳ' => 'ӳ', 'Ӵ' => 'Ӵ', 'ӵ' => 'ӵ', 'Ӹ' => 'Ӹ', 'ӹ' => 'ӹ', 'آ' => 'آ', 'أ' => 'أ', 'ؤ' => 'ؤ', 'إ' => 'إ', 'ئ' => 'ئ', 'ۀ' => 'ۀ', 'ۂ' => 'ۂ', 'ۓ' => 'ۓ', 'ऩ' => 'ऩ', 'ऱ' => 'ऱ', 'ऴ' => 'ऴ', 'क़' => 'क़', 'ख़' => 'ख़', 'ग़' => 'ग़', 'ज़' => 'ज़', 'ड़' => 'ड़', 'ढ़' => 'ढ़', 'फ़' => 'फ़', 'य़' => 'य़', 'ো' => 'ো', 'ৌ' => 'ৌ', 'ড়' => 'ড়', 'ঢ়' => 'ঢ়', 'য়' => 'য়', 'ਲ਼' => 'ਲ਼', 'ਸ਼' => 'ਸ਼', 'ਖ਼' => 'ਖ਼', 'ਗ਼' => 'ਗ਼', 'ਜ਼' => 'ਜ਼', 'ਫ਼' => 'ਫ਼', 'ୈ' => 'ୈ', 'ୋ' => 'ୋ', 'ୌ' => 'ୌ', 'ଡ଼' => 'ଡ଼', 'ଢ଼' => 'ଢ଼', 'ஔ' => 'ஔ', 'ொ' => 'ொ', 'ோ' => 'ோ', 'ௌ' => 'ௌ', 'ై' => 'ై', 'ೀ' => 'ೀ', 'ೇ' => 'ೇ', 'ೈ' => 'ೈ', 'ೊ' => 'ೊ', 'ೋ' => 'ೋ', 'ൊ' => 'ൊ', 'ോ' => 'ോ', 'ൌ' => 'ൌ', 'ේ' => 'ේ', 'ො' => 'ො', 'ෝ' => 'ෝ', 'ෞ' => 'ෞ', 'གྷ' => 'གྷ', 'ཌྷ' => 'ཌྷ', 'དྷ' => 'དྷ', 'བྷ' => 'བྷ', 'ཛྷ' => 'ཛྷ', 'ཀྵ' => 'ཀྵ', 'ཱི' => 'ཱི', 'ཱུ' => 'ཱུ', 'ྲྀ' => 'ྲྀ', 'ླྀ' => 'ླྀ', 'ཱྀ' => 'ཱྀ', 'ྒྷ' => 'ྒྷ', 'ྜྷ' => 'ྜྷ', 'ྡྷ' => 'ྡྷ', 'ྦྷ' => 'ྦྷ', 'ྫྷ' => 'ྫྷ', 'ྐྵ' => 'ྐྵ', 'ဦ' => 'ဦ', 'ᬆ' => 'ᬆ', 'ᬈ' => 'ᬈ', 'ᬊ' => 'ᬊ', 'ᬌ' => 'ᬌ', 'ᬎ' => 'ᬎ', 'ᬒ' => 'ᬒ', 'ᬻ' => 'ᬻ', 'ᬽ' => 'ᬽ', 'ᭀ' => 'ᭀ', 'ᭁ' => 'ᭁ', 'ᭃ' => 'ᭃ', 'Ḁ' => 'Ḁ', 'ḁ' => 'ḁ', 'Ḃ' => 'Ḃ', 'ḃ' => 'ḃ', 'Ḅ' => 'Ḅ', 'ḅ' => 'ḅ', 'Ḇ' => 'Ḇ', 'ḇ' => 'ḇ', 'Ḉ' => 'Ḉ', 'ḉ' => 'ḉ', 'Ḋ' => 'Ḋ', 'ḋ' => 'ḋ', 'Ḍ' => 'Ḍ', 'ḍ' => 'ḍ', 'Ḏ' => 'Ḏ', 'ḏ' => 'ḏ', 'Ḑ' => 'Ḑ', 'ḑ' => 'ḑ', 'Ḓ' => 'Ḓ', 'ḓ' => 'ḓ', 'Ḕ' => 'Ḕ', 'ḕ' => 'ḕ', 'Ḗ' => 'Ḗ', 'ḗ' => 'ḗ', 'Ḙ' => 'Ḙ', 'ḙ' => 'ḙ', 'Ḛ' => 'Ḛ', 'ḛ' => 'ḛ', 'Ḝ' => 'Ḝ', 'ḝ' => 'ḝ', 'Ḟ' => 'Ḟ', 'ḟ' => 'ḟ', 'Ḡ' => 'Ḡ', 'ḡ' => 'ḡ', 'Ḣ' => 'Ḣ', 'ḣ' => 'ḣ', 'Ḥ' => 'Ḥ', 'ḥ' => 'ḥ', 'Ḧ' => 'Ḧ', 'ḧ' => 'ḧ', 'Ḩ' => 'Ḩ', 'ḩ' => 'ḩ', 'Ḫ' => 'Ḫ', 'ḫ' => 'ḫ', 'Ḭ' => 'Ḭ', 'ḭ' => 'ḭ', 'Ḯ' => 'Ḯ', 'ḯ' => 'ḯ', 'Ḱ' => 'Ḱ', 'ḱ' => 'ḱ', 'Ḳ' => 'Ḳ', 'ḳ' => 'ḳ', 'Ḵ' => 'Ḵ', 'ḵ' => 'ḵ', 'Ḷ' => 'Ḷ', 'ḷ' => 'ḷ', 'Ḹ' => 'Ḹ', 'ḹ' => 'ḹ', 'Ḻ' => 'Ḻ', 'ḻ' => 'ḻ', 'Ḽ' => 'Ḽ', 'ḽ' => 'ḽ', 'Ḿ' => 'Ḿ', 'ḿ' => 'ḿ', 'Ṁ' => 'Ṁ', 'ṁ' => 'ṁ', 'Ṃ' => 'Ṃ', 'ṃ' => 'ṃ', 'Ṅ' => 'Ṅ', 'ṅ' => 'ṅ', 'Ṇ' => 'Ṇ', 'ṇ' => 'ṇ', 'Ṉ' => 'Ṉ', 'ṉ' => 'ṉ', 'Ṋ' => 'Ṋ', 'ṋ' => 'ṋ', 'Ṍ' => 'Ṍ', 'ṍ' => 'ṍ', 'Ṏ' => 'Ṏ', 'ṏ' => 'ṏ', 'Ṑ' => 'Ṑ', 'ṑ' => 'ṑ', 'Ṓ' => 'Ṓ', 'ṓ' => 'ṓ', 'Ṕ' => 'Ṕ', 'ṕ' => 'ṕ', 'Ṗ' => 'Ṗ', 'ṗ' => 'ṗ', 'Ṙ' => 'Ṙ', 'ṙ' => 'ṙ', 'Ṛ' => 'Ṛ', 'ṛ' => 'ṛ', 'Ṝ' => 'Ṝ', 'ṝ' => 'ṝ', 'Ṟ' => 'Ṟ', 'ṟ' => 'ṟ', 'Ṡ' => 'Ṡ', 'ṡ' => 'ṡ', 'Ṣ' => 'Ṣ', 'ṣ' => 'ṣ', 'Ṥ' => 'Ṥ', 'ṥ' => 'ṥ', 'Ṧ' => 'Ṧ', 'ṧ' => 'ṧ', 'Ṩ' => 'Ṩ', 'ṩ' => 'ṩ', 'Ṫ' => 'Ṫ', 'ṫ' => 'ṫ', 'Ṭ' => 'Ṭ', 'ṭ' => 'ṭ', 'Ṯ' => 'Ṯ', 'ṯ' => 'ṯ', 'Ṱ' => 'Ṱ', 'ṱ' => 'ṱ', 'Ṳ' => 'Ṳ', 'ṳ' => 'ṳ', 'Ṵ' => 'Ṵ', 'ṵ' => 'ṵ', 'Ṷ' => 'Ṷ', 'ṷ' => 'ṷ', 'Ṹ' => 'Ṹ', 'ṹ' => 'ṹ', 'Ṻ' => 'Ṻ', 'ṻ' => 'ṻ', 'Ṽ' => 'Ṽ', 'ṽ' => 'ṽ', 'Ṿ' => 'Ṿ', 'ṿ' => 'ṿ', 'Ẁ' => 'Ẁ', 'ẁ' => 'ẁ', 'Ẃ' => 'Ẃ', 'ẃ' => 'ẃ', 'Ẅ' => 'Ẅ', 'ẅ' => 'ẅ', 'Ẇ' => 'Ẇ', 'ẇ' => 'ẇ', 'Ẉ' => 'Ẉ', 'ẉ' => 'ẉ', 'Ẋ' => 'Ẋ', 'ẋ' => 'ẋ', 'Ẍ' => 'Ẍ', 'ẍ' => 'ẍ', 'Ẏ' => 'Ẏ', 'ẏ' => 'ẏ', 'Ẑ' => 'Ẑ', 'ẑ' => 'ẑ', 'Ẓ' => 'Ẓ', 'ẓ' => 'ẓ', 'Ẕ' => 'Ẕ', 'ẕ' => 'ẕ', 'ẖ' => 'ẖ', 'ẗ' => 'ẗ', 'ẘ' => 'ẘ', 'ẙ' => 'ẙ', 'ẛ' => 'ẛ', 'Ạ' => 'Ạ', 'ạ' => 'ạ', 'Ả' => 'Ả', 'ả' => 'ả', 'Ấ' => 'Ấ', 'ấ' => 'ấ', 'Ầ' => 'Ầ', 'ầ' => 'ầ', 'Ẩ' => 'Ẩ', 'ẩ' => 'ẩ', 'Ẫ' => 'Ẫ', 'ẫ' => 'ẫ', 'Ậ' => 'Ậ', 'ậ' => 'ậ', 'Ắ' => 'Ắ', 'ắ' => 'ắ', 'Ằ' => 'Ằ', 'ằ' => 'ằ', 'Ẳ' => 'Ẳ', 'ẳ' => 'ẳ', 'Ẵ' => 'Ẵ', 'ẵ' => 'ẵ', 'Ặ' => 'Ặ', 'ặ' => 'ặ', 'Ẹ' => 'Ẹ', 'ẹ' => 'ẹ', 'Ẻ' => 'Ẻ', 'ẻ' => 'ẻ', 'Ẽ' => 'Ẽ', 'ẽ' => 'ẽ', 'Ế' => 'Ế', 'ế' => 'ế', 'Ề' => 'Ề', 'ề' => 'ề', 'Ể' => 'Ể', 'ể' => 'ể', 'Ễ' => 'Ễ', 'ễ' => 'ễ', 'Ệ' => 'Ệ', 'ệ' => 'ệ', 'Ỉ' => 'Ỉ', 'ỉ' => 'ỉ', 'Ị' => 'Ị', 'ị' => 'ị', 'Ọ' => 'Ọ', 'ọ' => 'ọ', 'Ỏ' => 'Ỏ', 'ỏ' => 'ỏ', 'Ố' => 'Ố', 'ố' => 'ố', 'Ồ' => 'Ồ', 'ồ' => 'ồ', 'Ổ' => 'Ổ', 'ổ' => 'ổ', 'Ỗ' => 'Ỗ', 'ỗ' => 'ỗ', 'Ộ' => 'Ộ', 'ộ' => 'ộ', 'Ớ' => 'Ớ', 'ớ' => 'ớ', 'Ờ' => 'Ờ', 'ờ' => 'ờ', 'Ở' => 'Ở', 'ở' => 'ở', 'Ỡ' => 'Ỡ', 'ỡ' => 'ỡ', 'Ợ' => 'Ợ', 'ợ' => 'ợ', 'Ụ' => 'Ụ', 'ụ' => 'ụ', 'Ủ' => 'Ủ', 'ủ' => 'ủ', 'Ứ' => 'Ứ', 'ứ' => 'ứ', 'Ừ' => 'Ừ', 'ừ' => 'ừ', 'Ử' => 'Ử', 'ử' => 'ử', 'Ữ' => 'Ữ', 'ữ' => 'ữ', 'Ự' => 'Ự', 'ự' => 'ự', 'Ỳ' => 'Ỳ', 'ỳ' => 'ỳ', 'Ỵ' => 'Ỵ', 'ỵ' => 'ỵ', 'Ỷ' => 'Ỷ', 'ỷ' => 'ỷ', 'Ỹ' => 'Ỹ', 'ỹ' => 'ỹ', 'ἀ' => 'ἀ', 'ἁ' => 'ἁ', 'ἂ' => 'ἂ', 'ἃ' => 'ἃ', 'ἄ' => 'ἄ', 'ἅ' => 'ἅ', 'ἆ' => 'ἆ', 'ἇ' => 'ἇ', 'Ἀ' => 'Ἀ', 'Ἁ' => 'Ἁ', 'Ἂ' => 'Ἂ', 'Ἃ' => 'Ἃ', 'Ἄ' => 'Ἄ', 'Ἅ' => 'Ἅ', 'Ἆ' => 'Ἆ', 'Ἇ' => 'Ἇ', 'ἐ' => 'ἐ', 'ἑ' => 'ἑ', 'ἒ' => 'ἒ', 'ἓ' => 'ἓ', 'ἔ' => 'ἔ', 'ἕ' => 'ἕ', 'Ἐ' => 'Ἐ', 'Ἑ' => 'Ἑ', 'Ἒ' => 'Ἒ', 'Ἓ' => 'Ἓ', 'Ἔ' => 'Ἔ', 'Ἕ' => 'Ἕ', 'ἠ' => 'ἠ', 'ἡ' => 'ἡ', 'ἢ' => 'ἢ', 'ἣ' => 'ἣ', 'ἤ' => 'ἤ', 'ἥ' => 'ἥ', 'ἦ' => 'ἦ', 'ἧ' => 'ἧ', 'Ἠ' => 'Ἠ', 'Ἡ' => 'Ἡ', 'Ἢ' => 'Ἢ', 'Ἣ' => 'Ἣ', 'Ἤ' => 'Ἤ', 'Ἥ' => 'Ἥ', 'Ἦ' => 'Ἦ', 'Ἧ' => 'Ἧ', 'ἰ' => 'ἰ', 'ἱ' => 'ἱ', 'ἲ' => 'ἲ', 'ἳ' => 'ἳ', 'ἴ' => 'ἴ', 'ἵ' => 'ἵ', 'ἶ' => 'ἶ', 'ἷ' => 'ἷ', 'Ἰ' => 'Ἰ', 'Ἱ' => 'Ἱ', 'Ἲ' => 'Ἲ', 'Ἳ' => 'Ἳ', 'Ἴ' => 'Ἴ', 'Ἵ' => 'Ἵ', 'Ἶ' => 'Ἶ', 'Ἷ' => 'Ἷ', 'ὀ' => 'ὀ', 'ὁ' => 'ὁ', 'ὂ' => 'ὂ', 'ὃ' => 'ὃ', 'ὄ' => 'ὄ', 'ὅ' => 'ὅ', 'Ὀ' => 'Ὀ', 'Ὁ' => 'Ὁ', 'Ὂ' => 'Ὂ', 'Ὃ' => 'Ὃ', 'Ὄ' => 'Ὄ', 'Ὅ' => 'Ὅ', 'ὐ' => 'ὐ', 'ὑ' => 'ὑ', 'ὒ' => 'ὒ', 'ὓ' => 'ὓ', 'ὔ' => 'ὔ', 'ὕ' => 'ὕ', 'ὖ' => 'ὖ', 'ὗ' => 'ὗ', 'Ὑ' => 'Ὑ', 'Ὓ' => 'Ὓ', 'Ὕ' => 'Ὕ', 'Ὗ' => 'Ὗ', 'ὠ' => 'ὠ', 'ὡ' => 'ὡ', 'ὢ' => 'ὢ', 'ὣ' => 'ὣ', 'ὤ' => 'ὤ', 'ὥ' => 'ὥ', 'ὦ' => 'ὦ', 'ὧ' => 'ὧ', 'Ὠ' => 'Ὠ', 'Ὡ' => 'Ὡ', 'Ὢ' => 'Ὢ', 'Ὣ' => 'Ὣ', 'Ὤ' => 'Ὤ', 'Ὥ' => 'Ὥ', 'Ὦ' => 'Ὦ', 'Ὧ' => 'Ὧ', 'ὰ' => 'ὰ', 'ά' => 'ά', 'ὲ' => 'ὲ', 'έ' => 'έ', 'ὴ' => 'ὴ', 'ή' => 'ή', 'ὶ' => 'ὶ', 'ί' => 'ί', 'ὸ' => 'ὸ', 'ό' => 'ό', 'ὺ' => 'ὺ', 'ύ' => 'ύ', 'ὼ' => 'ὼ', 'ώ' => 'ώ', 'ᾀ' => 'ᾀ', 'ᾁ' => 'ᾁ', 'ᾂ' => 'ᾂ', 'ᾃ' => 'ᾃ', 'ᾄ' => 'ᾄ', 'ᾅ' => 'ᾅ', 'ᾆ' => 'ᾆ', 'ᾇ' => 'ᾇ', 'ᾈ' => 'ᾈ', 'ᾉ' => 'ᾉ', 'ᾊ' => 'ᾊ', 'ᾋ' => 'ᾋ', 'ᾌ' => 'ᾌ', 'ᾍ' => 'ᾍ', 'ᾎ' => 'ᾎ', 'ᾏ' => 'ᾏ', 'ᾐ' => 'ᾐ', 'ᾑ' => 'ᾑ', 'ᾒ' => 'ᾒ', 'ᾓ' => 'ᾓ', 'ᾔ' => 'ᾔ', 'ᾕ' => 'ᾕ', 'ᾖ' => 'ᾖ', 'ᾗ' => 'ᾗ', 'ᾘ' => 'ᾘ', 'ᾙ' => 'ᾙ', 'ᾚ' => 'ᾚ', 'ᾛ' => 'ᾛ', 'ᾜ' => 'ᾜ', 'ᾝ' => 'ᾝ', 'ᾞ' => 'ᾞ', 'ᾟ' => 'ᾟ', 'ᾠ' => 'ᾠ', 'ᾡ' => 'ᾡ', 'ᾢ' => 'ᾢ', 'ᾣ' => 'ᾣ', 'ᾤ' => 'ᾤ', 'ᾥ' => 'ᾥ', 'ᾦ' => 'ᾦ', 'ᾧ' => 'ᾧ', 'ᾨ' => 'ᾨ', 'ᾩ' => 'ᾩ', 'ᾪ' => 'ᾪ', 'ᾫ' => 'ᾫ', 'ᾬ' => 'ᾬ', 'ᾭ' => 'ᾭ', 'ᾮ' => 'ᾮ', 'ᾯ' => 'ᾯ', 'ᾰ' => 'ᾰ', 'ᾱ' => 'ᾱ', 'ᾲ' => 'ᾲ', 'ᾳ' => 'ᾳ', 'ᾴ' => 'ᾴ', 'ᾶ' => 'ᾶ', 'ᾷ' => 'ᾷ', 'Ᾰ' => 'Ᾰ', 'Ᾱ' => 'Ᾱ', 'Ὰ' => 'Ὰ', 'Ά' => 'Ά', 'ᾼ' => 'ᾼ', 'ι' => 'ι', '῁' => '῁', 'ῂ' => 'ῂ', 'ῃ' => 'ῃ', 'ῄ' => 'ῄ', 'ῆ' => 'ῆ', 'ῇ' => 'ῇ', 'Ὲ' => 'Ὲ', 'Έ' => 'Έ', 'Ὴ' => 'Ὴ', 'Ή' => 'Ή', 'ῌ' => 'ῌ', '῍' => '῍', '῎' => '῎', '῏' => '῏', 'ῐ' => 'ῐ', 'ῑ' => 'ῑ', 'ῒ' => 'ῒ', 'ΐ' => 'ΐ', 'ῖ' => 'ῖ', 'ῗ' => 'ῗ', 'Ῐ' => 'Ῐ', 'Ῑ' => 'Ῑ', 'Ὶ' => 'Ὶ', 'Ί' => 'Ί', '῝' => '῝', '῞' => '῞', '῟' => '῟', 'ῠ' => 'ῠ', 'ῡ' => 'ῡ', 'ῢ' => 'ῢ', 'ΰ' => 'ΰ', 'ῤ' => 'ῤ', 'ῥ' => 'ῥ', 'ῦ' => 'ῦ', 'ῧ' => 'ῧ', 'Ῠ' => 'Ῠ', 'Ῡ' => 'Ῡ', 'Ὺ' => 'Ὺ', 'Ύ' => 'Ύ', 'Ῥ' => 'Ῥ', '῭' => '῭', '΅' => '΅', '`' => '`', 'ῲ' => 'ῲ', 'ῳ' => 'ῳ', 'ῴ' => 'ῴ', 'ῶ' => 'ῶ', 'ῷ' => 'ῷ', 'Ὸ' => 'Ὸ', 'Ό' => 'Ό', 'Ὼ' => 'Ὼ', 'Ώ' => 'Ώ', 'ῼ' => 'ῼ', '´' => '´', ' ' => ' ', ' ' => ' ', 'Ω' => 'Ω', 'K' => 'K', 'Å' => 'Å', '↚' => '↚', '↛' => '↛', '↮' => '↮', '⇍' => '⇍', '⇎' => '⇎', '⇏' => '⇏', '∄' => '∄', '∉' => '∉', '∌' => '∌', '∤' => '∤', '∦' => '∦', '≁' => '≁', '≄' => '≄', '≇' => '≇', '≉' => '≉', '≠' => '≠', '≢' => '≢', '≭' => '≭', '≮' => '≮', '≯' => '≯', '≰' => '≰', '≱' => '≱', '≴' => '≴', '≵' => '≵', '≸' => '≸', '≹' => '≹', '⊀' => '⊀', '⊁' => '⊁', '⊄' => '⊄', '⊅' => '⊅', '⊈' => '⊈', '⊉' => '⊉', '⊬' => '⊬', '⊭' => '⊭', '⊮' => '⊮', '⊯' => '⊯', '⋠' => '⋠', '⋡' => '⋡', '⋢' => '⋢', '⋣' => '⋣', '⋪' => '⋪', '⋫' => '⋫', '⋬' => '⋬', '⋭' => '⋭', '〈' => '〈', '〉' => '〉', '⫝̸' => '⫝̸', 'が' => 'が', 'ぎ' => 'ぎ', 'ぐ' => 'ぐ', 'げ' => 'げ', 'ご' => 'ご', 'ざ' => 'ざ', 'じ' => 'じ', 'ず' => 'ず', 'ぜ' => 'ぜ', 'ぞ' => 'ぞ', 'だ' => 'だ', 'ぢ' => 'ぢ', 'づ' => 'づ', 'で' => 'で', 'ど' => 'ど', 'ば' => 'ば', 'ぱ' => 'ぱ', 'び' => 'び', 'ぴ' => 'ぴ', 'ぶ' => 'ぶ', 'ぷ' => 'ぷ', 'べ' => 'べ', 'ぺ' => 'ぺ', 'ぼ' => 'ぼ', 'ぽ' => 'ぽ', 'ゔ' => 'ゔ', 'ゞ' => 'ゞ', 'ガ' => 'ガ', 'ギ' => 'ギ', 'グ' => 'グ', 'ゲ' => 'ゲ', 'ゴ' => 'ゴ', 'ザ' => 'ザ', 'ジ' => 'ジ', 'ズ' => 'ズ', 'ゼ' => 'ゼ', 'ゾ' => 'ゾ', 'ダ' => 'ダ', 'ヂ' => 'ヂ', 'ヅ' => 'ヅ', 'デ' => 'デ', 'ド' => 'ド', 'バ' => 'バ', 'パ' => 'パ', 'ビ' => 'ビ', 'ピ' => 'ピ', 'ブ' => 'ブ', 'プ' => 'プ', 'ベ' => 'ベ', 'ペ' => 'ペ', 'ボ' => 'ボ', 'ポ' => 'ポ', 'ヴ' => 'ヴ', 'ヷ' => 'ヷ', 'ヸ' => 'ヸ', 'ヹ' => 'ヹ', 'ヺ' => 'ヺ', 'ヾ' => 'ヾ', '豈' => '豈', '更' => '更', '車' => '車', '賈' => '賈', '滑' => '滑', '串' => '串', '句' => '句', '龜' => '龜', '龜' => '龜', '契' => '契', '金' => '金', '喇' => '喇', '奈' => '奈', '懶' => '懶', '癩' => '癩', '羅' => '羅', '蘿' => '蘿', '螺' => '螺', '裸' => '裸', '邏' => '邏', '樂' => '樂', '洛' => '洛', '烙' => '烙', '珞' => '珞', '落' => '落', '酪' => '酪', '駱' => '駱', '亂' => '亂', '卵' => '卵', '欄' => '欄', '爛' => '爛', '蘭' => '蘭', '鸞' => '鸞', '嵐' => '嵐', '濫' => '濫', '藍' => '藍', '襤' => '襤', '拉' => '拉', '臘' => '臘', '蠟' => '蠟', '廊' => '廊', '朗' => '朗', '浪' => '浪', '狼' => '狼', '郎' => '郎', '來' => '來', '冷' => '冷', '勞' => '勞', '擄' => '擄', '櫓' => '櫓', '爐' => '爐', '盧' => '盧', '老' => '老', '蘆' => '蘆', '虜' => '虜', '路' => '路', '露' => '露', '魯' => '魯', '鷺' => '鷺', '碌' => '碌', '祿' => '祿', '綠' => '綠', '菉' => '菉', '錄' => '錄', '鹿' => '鹿', '論' => '論', '壟' => '壟', '弄' => '弄', '籠' => '籠', '聾' => '聾', '牢' => '牢', '磊' => '磊', '賂' => '賂', '雷' => '雷', '壘' => '壘', '屢' => '屢', '樓' => '樓', '淚' => '淚', '漏' => '漏', '累' => '累', '縷' => '縷', '陋' => '陋', '勒' => '勒', '肋' => '肋', '凜' => '凜', '凌' => '凌', '稜' => '稜', '綾' => '綾', '菱' => '菱', '陵' => '陵', '讀' => '讀', '拏' => '拏', '樂' => '樂', '諾' => '諾', '丹' => '丹', '寧' => '寧', '怒' => '怒', '率' => '率', '異' => '異', '北' => '北', '磻' => '磻', '便' => '便', '復' => '復', '不' => '不', '泌' => '泌', '數' => '數', '索' => '索', '參' => '參', '塞' => '塞', '省' => '省', '葉' => '葉', '說' => '說', '殺' => '殺', '辰' => '辰', '沈' => '沈', '拾' => '拾', '若' => '若', '掠' => '掠', '略' => '略', '亮' => '亮', '兩' => '兩', '凉' => '凉', '梁' => '梁', '糧' => '糧', '良' => '良', '諒' => '諒', '量' => '量', '勵' => '勵', '呂' => '呂', '女' => '女', '廬' => '廬', '旅' => '旅', '濾' => '濾', '礪' => '礪', '閭' => '閭', '驪' => '驪', '麗' => '麗', '黎' => '黎', '力' => '力', '曆' => '曆', '歷' => '歷', '轢' => '轢', '年' => '年', '憐' => '憐', '戀' => '戀', '撚' => '撚', '漣' => '漣', '煉' => '煉', '璉' => '璉', '秊' => '秊', '練' => '練', '聯' => '聯', '輦' => '輦', '蓮' => '蓮', '連' => '連', '鍊' => '鍊', '列' => '列', '劣' => '劣', '咽' => '咽', '烈' => '烈', '裂' => '裂', '說' => '說', '廉' => '廉', '念' => '念', '捻' => '捻', '殮' => '殮', '簾' => '簾', '獵' => '獵', '令' => '令', '囹' => '囹', '寧' => '寧', '嶺' => '嶺', '怜' => '怜', '玲' => '玲', '瑩' => '瑩', '羚' => '羚', '聆' => '聆', '鈴' => '鈴', '零' => '零', '靈' => '靈', '領' => '領', '例' => '例', '禮' => '禮', '醴' => '醴', '隸' => '隸', '惡' => '惡', '了' => '了', '僚' => '僚', '寮' => '寮', '尿' => '尿', '料' => '料', '樂' => '樂', '燎' => '燎', '療' => '療', '蓼' => '蓼', '遼' => '遼', '龍' => '龍', '暈' => '暈', '阮' => '阮', '劉' => '劉', '杻' => '杻', '柳' => '柳', '流' => '流', '溜' => '溜', '琉' => '琉', '留' => '留', '硫' => '硫', '紐' => '紐', '類' => '類', '六' => '六', '戮' => '戮', '陸' => '陸', '倫' => '倫', '崙' => '崙', '淪' => '淪', '輪' => '輪', '律' => '律', '慄' => '慄', '栗' => '栗', '率' => '率', '隆' => '隆', '利' => '利', '吏' => '吏', '履' => '履', '易' => '易', '李' => '李', '梨' => '梨', '泥' => '泥', '理' => '理', '痢' => '痢', '罹' => '罹', '裏' => '裏', '裡' => '裡', '里' => '里', '離' => '離', '匿' => '匿', '溺' => '溺', '吝' => '吝', '燐' => '燐', '璘' => '璘', '藺' => '藺', '隣' => '隣', '鱗' => '鱗', '麟' => '麟', '林' => '林', '淋' => '淋', '臨' => '臨', '立' => '立', '笠' => '笠', '粒' => '粒', '狀' => '狀', '炙' => '炙', '識' => '識', '什' => '什', '茶' => '茶', '刺' => '刺', '切' => '切', '度' => '度', '拓' => '拓', '糖' => '糖', '宅' => '宅', '洞' => '洞', '暴' => '暴', '輻' => '輻', '行' => '行', '降' => '降', '見' => '見', '廓' => '廓', '兀' => '兀', '嗀' => '嗀', '塚' => '塚', '晴' => '晴', '凞' => '凞', '猪' => '猪', '益' => '益', '礼' => '礼', '神' => '神', '祥' => '祥', '福' => '福', '靖' => '靖', '精' => '精', '羽' => '羽', '蘒' => '蘒', '諸' => '諸', '逸' => '逸', '都' => '都', '飯' => '飯', '飼' => '飼', '館' => '館', '鶴' => '鶴', '郞' => '郞', '隷' => '隷', '侮' => '侮', '僧' => '僧', '免' => '免', '勉' => '勉', '勤' => '勤', '卑' => '卑', '喝' => '喝', '嘆' => '嘆', '器' => '器', '塀' => '塀', '墨' => '墨', '層' => '層', '屮' => '屮', '悔' => '悔', '慨' => '慨', '憎' => '憎', '懲' => '懲', '敏' => '敏', '既' => '既', '暑' => '暑', '梅' => '梅', '海' => '海', '渚' => '渚', '漢' => '漢', '煮' => '煮', '爫' => '爫', '琢' => '琢', '碑' => '碑', '社' => '社', '祉' => '祉', '祈' => '祈', '祐' => '祐', '祖' => '祖', '祝' => '祝', '禍' => '禍', '禎' => '禎', '穀' => '穀', '突' => '突', '節' => '節', '練' => '練', '縉' => '縉', '繁' => '繁', '署' => '署', '者' => '者', '臭' => '臭', '艹' => '艹', '艹' => '艹', '著' => '著', '褐' => '褐', '視' => '視', '謁' => '謁', '謹' => '謹', '賓' => '賓', '贈' => '贈', '辶' => '辶', '逸' => '逸', '難' => '難', '響' => '響', '頻' => '頻', '恵' => '恵', '𤋮' => '𤋮', '舘' => '舘', '並' => '並', '况' => '况', '全' => '全', '侀' => '侀', '充' => '充', '冀' => '冀', '勇' => '勇', '勺' => '勺', '喝' => '喝', '啕' => '啕', '喙' => '喙', '嗢' => '嗢', '塚' => '塚', '墳' => '墳', '奄' => '奄', '奔' => '奔', '婢' => '婢', '嬨' => '嬨', '廒' => '廒', '廙' => '廙', '彩' => '彩', '徭' => '徭', '惘' => '惘', '慎' => '慎', '愈' => '愈', '憎' => '憎', '慠' => '慠', '懲' => '懲', '戴' => '戴', '揄' => '揄', '搜' => '搜', '摒' => '摒', '敖' => '敖', '晴' => '晴', '朗' => '朗', '望' => '望', '杖' => '杖', '歹' => '歹', '殺' => '殺', '流' => '流', '滛' => '滛', '滋' => '滋', '漢' => '漢', '瀞' => '瀞', '煮' => '煮', '瞧' => '瞧', '爵' => '爵', '犯' => '犯', '猪' => '猪', '瑱' => '瑱', '甆' => '甆', '画' => '画', '瘝' => '瘝', '瘟' => '瘟', '益' => '益', '盛' => '盛', '直' => '直', '睊' => '睊', '着' => '着', '磌' => '磌', '窱' => '窱', '節' => '節', '类' => '类', '絛' => '絛', '練' => '練', '缾' => '缾', '者' => '者', '荒' => '荒', '華' => '華', '蝹' => '蝹', '襁' => '襁', '覆' => '覆', '視' => '視', '調' => '調', '諸' => '諸', '請' => '請', '謁' => '謁', '諾' => '諾', '諭' => '諭', '謹' => '謹', '變' => '變', '贈' => '贈', '輸' => '輸', '遲' => '遲', '醙' => '醙', '鉶' => '鉶', '陼' => '陼', '難' => '難', '靖' => '靖', '韛' => '韛', '響' => '響', '頋' => '頋', '頻' => '頻', '鬒' => '鬒', '龜' => '龜', '𢡊' => '𢡊', '𢡄' => '𢡄', '𣏕' => '𣏕', '㮝' => '㮝', '䀘' => '䀘', '䀹' => '䀹', '𥉉' => '𥉉', '𥳐' => '𥳐', '𧻓' => '𧻓', '齃' => '齃', '龎' => '龎', 'יִ' => 'יִ', 'ײַ' => 'ײַ', 'שׁ' => 'שׁ', 'שׂ' => 'שׂ', 'שּׁ' => 'שּׁ', 'שּׂ' => 'שּׂ', 'אַ' => 'אַ', 'אָ' => 'אָ', 'אּ' => 'אּ', 'בּ' => 'בּ', 'גּ' => 'גּ', 'דּ' => 'דּ', 'הּ' => 'הּ', 'וּ' => 'וּ', 'זּ' => 'זּ', 'טּ' => 'טּ', 'יּ' => 'יּ', 'ךּ' => 'ךּ', 'כּ' => 'כּ', 'לּ' => 'לּ', 'מּ' => 'מּ', 'נּ' => 'נּ', 'סּ' => 'סּ', 'ףּ' => 'ףּ', 'פּ' => 'פּ', 'צּ' => 'צּ', 'קּ' => 'קּ', 'רּ' => 'רּ', 'שּ' => 'שּ', 'תּ' => 'תּ', 'וֹ' => 'וֹ', 'בֿ' => 'בֿ', 'כֿ' => 'כֿ', 'פֿ' => 'פֿ', '𑂚' => '𑂚', '𑂜' => '𑂜', '𑂫' => '𑂫', '𑄮' => '𑄮', '𑄯' => '𑄯', '𑍋' => '𑍋', '𑍌' => '𑍌', '𑒻' => '𑒻', '𑒼' => '𑒼', '𑒾' => '𑒾', '𑖺' => '𑖺', '𑖻' => '𑖻', '𑤸' => '𑤸', '𝅗𝅥' => '𝅗𝅥', '𝅘𝅥' => '𝅘𝅥', '𝅘𝅥𝅮' => '𝅘𝅥𝅮', '𝅘𝅥𝅯' => '𝅘𝅥𝅯', '𝅘𝅥𝅰' => '𝅘𝅥𝅰', '𝅘𝅥𝅱' => '𝅘𝅥𝅱', '𝅘𝅥𝅲' => '𝅘𝅥𝅲', '𝆹𝅥' => '𝆹𝅥', '𝆺𝅥' => '𝆺𝅥', '𝆹𝅥𝅮' => '𝆹𝅥𝅮', '𝆺𝅥𝅮' => '𝆺𝅥𝅮', '𝆹𝅥𝅯' => '𝆹𝅥𝅯', '𝆺𝅥𝅯' => '𝆺𝅥𝅯', '丽' => '丽', '丸' => '丸', '乁' => '乁', '𠄢' => '𠄢', '你' => '你', '侮' => '侮', '侻' => '侻', '倂' => '倂', '偺' => '偺', '備' => '備', '僧' => '僧', '像' => '像', '㒞' => '㒞', '𠘺' => '𠘺', '免' => '免', '兔' => '兔', '兤' => '兤', '具' => '具', '𠔜' => '𠔜', '㒹' => '㒹', '內' => '內', '再' => '再', '𠕋' => '𠕋', '冗' => '冗', '冤' => '冤', '仌' => '仌', '冬' => '冬', '况' => '况', '𩇟' => '𩇟', '凵' => '凵', '刃' => '刃', '㓟' => '㓟', '刻' => '刻', '剆' => '剆', '割' => '割', '剷' => '剷', '㔕' => '㔕', '勇' => '勇', '勉' => '勉', '勤' => '勤', '勺' => '勺', '包' => '包', '匆' => '匆', '北' => '北', '卉' => '卉', '卑' => '卑', '博' => '博', '即' => '即', '卽' => '卽', '卿' => '卿', '卿' => '卿', '卿' => '卿', '𠨬' => '𠨬', '灰' => '灰', '及' => '及', '叟' => '叟', '𠭣' => '𠭣', '叫' => '叫', '叱' => '叱', '吆' => '吆', '咞' => '咞', '吸' => '吸', '呈' => '呈', '周' => '周', '咢' => '咢', '哶' => '哶', '唐' => '唐', '啓' => '啓', '啣' => '啣', '善' => '善', '善' => '善', '喙' => '喙', '喫' => '喫', '喳' => '喳', '嗂' => '嗂', '圖' => '圖', '嘆' => '嘆', '圗' => '圗', '噑' => '噑', '噴' => '噴', '切' => '切', '壮' => '壮', '城' => '城', '埴' => '埴', '堍' => '堍', '型' => '型', '堲' => '堲', '報' => '報', '墬' => '墬', '𡓤' => '𡓤', '売' => '売', '壷' => '壷', '夆' => '夆', '多' => '多', '夢' => '夢', '奢' => '奢', '𡚨' => '𡚨', '𡛪' => '𡛪', '姬' => '姬', '娛' => '娛', '娧' => '娧', '姘' => '姘', '婦' => '婦', '㛮' => '㛮', '㛼' => '㛼', '嬈' => '嬈', '嬾' => '嬾', '嬾' => '嬾', '𡧈' => '𡧈', '寃' => '寃', '寘' => '寘', '寧' => '寧', '寳' => '寳', '𡬘' => '𡬘', '寿' => '寿', '将' => '将', '当' => '当', '尢' => '尢', '㞁' => '㞁', '屠' => '屠', '屮' => '屮', '峀' => '峀', '岍' => '岍', '𡷤' => '𡷤', '嵃' => '嵃', '𡷦' => '𡷦', '嵮' => '嵮', '嵫' => '嵫', '嵼' => '嵼', '巡' => '巡', '巢' => '巢', '㠯' => '㠯', '巽' => '巽', '帨' => '帨', '帽' => '帽', '幩' => '幩', '㡢' => '㡢', '𢆃' => '𢆃', '㡼' => '㡼', '庰' => '庰', '庳' => '庳', '庶' => '庶', '廊' => '廊', '𪎒' => '𪎒', '廾' => '廾', '𢌱' => '𢌱', '𢌱' => '𢌱', '舁' => '舁', '弢' => '弢', '弢' => '弢', '㣇' => '㣇', '𣊸' => '𣊸', '𦇚' => '𦇚', '形' => '形', '彫' => '彫', '㣣' => '㣣', '徚' => '徚', '忍' => '忍', '志' => '志', '忹' => '忹', '悁' => '悁', '㤺' => '㤺', '㤜' => '㤜', '悔' => '悔', '𢛔' => '𢛔', '惇' => '惇', '慈' => '慈', '慌' => '慌', '慎' => '慎', '慌' => '慌', '慺' => '慺', '憎' => '憎', '憲' => '憲', '憤' => '憤', '憯' => '憯', '懞' => '懞', '懲' => '懲', '懶' => '懶', '成' => '成', '戛' => '戛', '扝' => '扝', '抱' => '抱', '拔' => '拔', '捐' => '捐', '𢬌' => '𢬌', '挽' => '挽', '拼' => '拼', '捨' => '捨', '掃' => '掃', '揤' => '揤', '𢯱' => '𢯱', '搢' => '搢', '揅' => '揅', '掩' => '掩', '㨮' => '㨮', '摩' => '摩', '摾' => '摾', '撝' => '撝', '摷' => '摷', '㩬' => '㩬', '敏' => '敏', '敬' => '敬', '𣀊' => '𣀊', '旣' => '旣', '書' => '書', '晉' => '晉', '㬙' => '㬙', '暑' => '暑', '㬈' => '㬈', '㫤' => '㫤', '冒' => '冒', '冕' => '冕', '最' => '最', '暜' => '暜', '肭' => '肭', '䏙' => '䏙', '朗' => '朗', '望' => '望', '朡' => '朡', '杞' => '杞', '杓' => '杓', '𣏃' => '𣏃', '㭉' => '㭉', '柺' => '柺', '枅' => '枅', '桒' => '桒', '梅' => '梅', '𣑭' => '𣑭', '梎' => '梎', '栟' => '栟', '椔' => '椔', '㮝' => '㮝', '楂' => '楂', '榣' => '榣', '槪' => '槪', '檨' => '檨', '𣚣' => '𣚣', '櫛' => '櫛', '㰘' => '㰘', '次' => '次', '𣢧' => '𣢧', '歔' => '歔', '㱎' => '㱎', '歲' => '歲', '殟' => '殟', '殺' => '殺', '殻' => '殻', '𣪍' => '𣪍', '𡴋' => '𡴋', '𣫺' => '𣫺', '汎' => '汎', '𣲼' => '𣲼', '沿' => '沿', '泍' => '泍', '汧' => '汧', '洖' => '洖', '派' => '派', '海' => '海', '流' => '流', '浩' => '浩', '浸' => '浸', '涅' => '涅', '𣴞' => '𣴞', '洴' => '洴', '港' => '港', '湮' => '湮', '㴳' => '㴳', '滋' => '滋', '滇' => '滇', '𣻑' => '𣻑', '淹' => '淹', '潮' => '潮', '𣽞' => '𣽞', '𣾎' => '𣾎', '濆' => '濆', '瀹' => '瀹', '瀞' => '瀞', '瀛' => '瀛', '㶖' => '㶖', '灊' => '灊', '災' => '災', '灷' => '灷', '炭' => '炭', '𠔥' => '𠔥', '煅' => '煅', '𤉣' => '𤉣', '熜' => '熜', '𤎫' => '𤎫', '爨' => '爨', '爵' => '爵', '牐' => '牐', '𤘈' => '𤘈', '犀' => '犀', '犕' => '犕', '𤜵' => '𤜵', '𤠔' => '𤠔', '獺' => '獺', '王' => '王', '㺬' => '㺬', '玥' => '玥', '㺸' => '㺸', '㺸' => '㺸', '瑇' => '瑇', '瑜' => '瑜', '瑱' => '瑱', '璅' => '璅', '瓊' => '瓊', '㼛' => '㼛', '甤' => '甤', '𤰶' => '𤰶', '甾' => '甾', '𤲒' => '𤲒', '異' => '異', '𢆟' => '𢆟', '瘐' => '瘐', '𤾡' => '𤾡', '𤾸' => '𤾸', '𥁄' => '𥁄', '㿼' => '㿼', '䀈' => '䀈', '直' => '直', '𥃳' => '𥃳', '𥃲' => '𥃲', '𥄙' => '𥄙', '𥄳' => '𥄳', '眞' => '眞', '真' => '真', '真' => '真', '睊' => '睊', '䀹' => '䀹', '瞋' => '瞋', '䁆' => '䁆', '䂖' => '䂖', '𥐝' => '𥐝', '硎' => '硎', '碌' => '碌', '磌' => '磌', '䃣' => '䃣', '𥘦' => '𥘦', '祖' => '祖', '𥚚' => '𥚚', '𥛅' => '𥛅', '福' => '福', '秫' => '秫', '䄯' => '䄯', '穀' => '穀', '穊' => '穊', '穏' => '穏', '𥥼' => '𥥼', '𥪧' => '𥪧', '𥪧' => '𥪧', '竮' => '竮', '䈂' => '䈂', '𥮫' => '𥮫', '篆' => '篆', '築' => '築', '䈧' => '䈧', '𥲀' => '𥲀', '糒' => '糒', '䊠' => '䊠', '糨' => '糨', '糣' => '糣', '紀' => '紀', '𥾆' => '𥾆', '絣' => '絣', '䌁' => '䌁', '緇' => '緇', '縂' => '縂', '繅' => '繅', '䌴' => '䌴', '𦈨' => '𦈨', '𦉇' => '𦉇', '䍙' => '䍙', '𦋙' => '𦋙', '罺' => '罺', '𦌾' => '𦌾', '羕' => '羕', '翺' => '翺', '者' => '者', '𦓚' => '𦓚', '𦔣' => '𦔣', '聠' => '聠', '𦖨' => '𦖨', '聰' => '聰', '𣍟' => '𣍟', '䏕' => '䏕', '育' => '育', '脃' => '脃', '䐋' => '䐋', '脾' => '脾', '媵' => '媵', '𦞧' => '𦞧', '𦞵' => '𦞵', '𣎓' => '𣎓', '𣎜' => '𣎜', '舁' => '舁', '舄' => '舄', '辞' => '辞', '䑫' => '䑫', '芑' => '芑', '芋' => '芋', '芝' => '芝', '劳' => '劳', '花' => '花', '芳' => '芳', '芽' => '芽', '苦' => '苦', '𦬼' => '𦬼', '若' => '若', '茝' => '茝', '荣' => '荣', '莭' => '莭', '茣' => '茣', '莽' => '莽', '菧' => '菧', '著' => '著', '荓' => '荓', '菊' => '菊', '菌' => '菌', '菜' => '菜', '𦰶' => '𦰶', '𦵫' => '𦵫', '𦳕' => '𦳕', '䔫' => '䔫', '蓱' => '蓱', '蓳' => '蓳', '蔖' => '蔖', '𧏊' => '𧏊', '蕤' => '蕤', '𦼬' => '𦼬', '䕝' => '䕝', '䕡' => '䕡', '𦾱' => '𦾱', '𧃒' => '𧃒', '䕫' => '䕫', '虐' => '虐', '虜' => '虜', '虧' => '虧', '虩' => '虩', '蚩' => '蚩', '蚈' => '蚈', '蜎' => '蜎', '蛢' => '蛢', '蝹' => '蝹', '蜨' => '蜨', '蝫' => '蝫', '螆' => '螆', '䗗' => '䗗', '蟡' => '蟡', '蠁' => '蠁', '䗹' => '䗹', '衠' => '衠', '衣' => '衣', '𧙧' => '𧙧', '裗' => '裗', '裞' => '裞', '䘵' => '䘵', '裺' => '裺', '㒻' => '㒻', '𧢮' => '𧢮', '𧥦' => '𧥦', '䚾' => '䚾', '䛇' => '䛇', '誠' => '誠', '諭' => '諭', '變' => '變', '豕' => '豕', '𧲨' => '𧲨', '貫' => '貫', '賁' => '賁', '贛' => '贛', '起' => '起', '𧼯' => '𧼯', '𠠄' => '𠠄', '跋' => '跋', '趼' => '趼', '跰' => '跰', '𠣞' => '𠣞', '軔' => '軔', '輸' => '輸', '𨗒' => '𨗒', '𨗭' => '𨗭', '邔' => '邔', '郱' => '郱', '鄑' => '鄑', '𨜮' => '𨜮', '鄛' => '鄛', '鈸' => '鈸', '鋗' => '鋗', '鋘' => '鋘', '鉼' => '鉼', '鏹' => '鏹', '鐕' => '鐕', '𨯺' => '𨯺', '開' => '開', '䦕' => '䦕', '閷' => '閷', '𨵷' => '𨵷', '䧦' => '䧦', '雃' => '雃', '嶲' => '嶲', '霣' => '霣', '𩅅' => '𩅅', '𩈚' => '𩈚', '䩮' => '䩮', '䩶' => '䩶', '韠' => '韠', '𩐊' => '𩐊', '䪲' => '䪲', '𩒖' => '𩒖', '頋' => '頋', '頋' => '頋', '頩' => '頩', '𩖶' => '𩖶', '飢' => '飢', '䬳' => '䬳', '餩' => '餩', '馧' => '馧', '駂' => '駂', '駾' => '駾', '䯎' => '䯎', '𩬰' => '𩬰', '鬒' => '鬒', '鱀' => '鱀', '鳽' => '鳽', '䳎' => '䳎', '䳭' => '䳭', '鵧' => '鵧', '𪃎' => '𪃎', '䳸' => '䳸', '𪄅' => '𪄅', '𪈎' => '𪈎', '𪊑' => '𪊑', '麻' => '麻', '䵖' => '䵖', '黹' => '黹', '黾' => '黾', '鼅' => '鼅', '鼏' => '鼏', '鼖' => '鼖', '鼻' => '鼻', '𪘀' => '𪘀', ); polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php 0000644 00000202557 15025017654 0023260 0 ustar 00 <?php return array ( ' ' => ' ', '¨' => ' ̈', 'ª' => 'a', '¯' => ' ̄', '²' => '2', '³' => '3', '´' => ' ́', 'µ' => 'μ', '¸' => ' ̧', '¹' => '1', 'º' => 'o', '¼' => '1⁄4', '½' => '1⁄2', '¾' => '3⁄4', 'IJ' => 'IJ', 'ij' => 'ij', 'Ŀ' => 'L·', 'ŀ' => 'l·', 'ʼn' => 'ʼn', 'ſ' => 's', 'DŽ' => 'DŽ', 'Dž' => 'Dž', 'dž' => 'dž', 'LJ' => 'LJ', 'Lj' => 'Lj', 'lj' => 'lj', 'NJ' => 'NJ', 'Nj' => 'Nj', 'nj' => 'nj', 'DZ' => 'DZ', 'Dz' => 'Dz', 'dz' => 'dz', 'ʰ' => 'h', 'ʱ' => 'ɦ', 'ʲ' => 'j', 'ʳ' => 'r', 'ʴ' => 'ɹ', 'ʵ' => 'ɻ', 'ʶ' => 'ʁ', 'ʷ' => 'w', 'ʸ' => 'y', '˘' => ' ̆', '˙' => ' ̇', '˚' => ' ̊', '˛' => ' ̨', '˜' => ' ̃', '˝' => ' ̋', 'ˠ' => 'ɣ', 'ˡ' => 'l', 'ˢ' => 's', 'ˣ' => 'x', 'ˤ' => 'ʕ', 'ͺ' => ' ͅ', '΄' => ' ́', '΅' => ' ̈́', 'ϐ' => 'β', 'ϑ' => 'θ', 'ϒ' => 'Υ', 'ϓ' => 'Ύ', 'ϔ' => 'Ϋ', 'ϕ' => 'φ', 'ϖ' => 'π', 'ϰ' => 'κ', 'ϱ' => 'ρ', 'ϲ' => 'ς', 'ϴ' => 'Θ', 'ϵ' => 'ε', 'Ϲ' => 'Σ', 'և' => 'եւ', 'ٵ' => 'اٴ', 'ٶ' => 'وٴ', 'ٷ' => 'ۇٴ', 'ٸ' => 'يٴ', 'ำ' => 'ํา', 'ຳ' => 'ໍາ', 'ໜ' => 'ຫນ', 'ໝ' => 'ຫມ', '༌' => '་', 'ཷ' => 'ྲཱྀ', 'ཹ' => 'ླཱྀ', 'ჼ' => 'ნ', 'ᴬ' => 'A', 'ᴭ' => 'Æ', 'ᴮ' => 'B', 'ᴰ' => 'D', 'ᴱ' => 'E', 'ᴲ' => 'Ǝ', 'ᴳ' => 'G', 'ᴴ' => 'H', 'ᴵ' => 'I', 'ᴶ' => 'J', 'ᴷ' => 'K', 'ᴸ' => 'L', 'ᴹ' => 'M', 'ᴺ' => 'N', 'ᴼ' => 'O', 'ᴽ' => 'Ȣ', 'ᴾ' => 'P', 'ᴿ' => 'R', 'ᵀ' => 'T', 'ᵁ' => 'U', 'ᵂ' => 'W', 'ᵃ' => 'a', 'ᵄ' => 'ɐ', 'ᵅ' => 'ɑ', 'ᵆ' => 'ᴂ', 'ᵇ' => 'b', 'ᵈ' => 'd', 'ᵉ' => 'e', 'ᵊ' => 'ə', 'ᵋ' => 'ɛ', 'ᵌ' => 'ɜ', 'ᵍ' => 'g', 'ᵏ' => 'k', 'ᵐ' => 'm', 'ᵑ' => 'ŋ', 'ᵒ' => 'o', 'ᵓ' => 'ɔ', 'ᵔ' => 'ᴖ', 'ᵕ' => 'ᴗ', 'ᵖ' => 'p', 'ᵗ' => 't', 'ᵘ' => 'u', 'ᵙ' => 'ᴝ', 'ᵚ' => 'ɯ', 'ᵛ' => 'v', 'ᵜ' => 'ᴥ', 'ᵝ' => 'β', 'ᵞ' => 'γ', 'ᵟ' => 'δ', 'ᵠ' => 'φ', 'ᵡ' => 'χ', 'ᵢ' => 'i', 'ᵣ' => 'r', 'ᵤ' => 'u', 'ᵥ' => 'v', 'ᵦ' => 'β', 'ᵧ' => 'γ', 'ᵨ' => 'ρ', 'ᵩ' => 'φ', 'ᵪ' => 'χ', 'ᵸ' => 'н', 'ᶛ' => 'ɒ', 'ᶜ' => 'c', 'ᶝ' => 'ɕ', 'ᶞ' => 'ð', 'ᶟ' => 'ɜ', 'ᶠ' => 'f', 'ᶡ' => 'ɟ', 'ᶢ' => 'ɡ', 'ᶣ' => 'ɥ', 'ᶤ' => 'ɨ', 'ᶥ' => 'ɩ', 'ᶦ' => 'ɪ', 'ᶧ' => 'ᵻ', 'ᶨ' => 'ʝ', 'ᶩ' => 'ɭ', 'ᶪ' => 'ᶅ', 'ᶫ' => 'ʟ', 'ᶬ' => 'ɱ', 'ᶭ' => 'ɰ', 'ᶮ' => 'ɲ', 'ᶯ' => 'ɳ', 'ᶰ' => 'ɴ', 'ᶱ' => 'ɵ', 'ᶲ' => 'ɸ', 'ᶳ' => 'ʂ', 'ᶴ' => 'ʃ', 'ᶵ' => 'ƫ', 'ᶶ' => 'ʉ', 'ᶷ' => 'ʊ', 'ᶸ' => 'ᴜ', 'ᶹ' => 'ʋ', 'ᶺ' => 'ʌ', 'ᶻ' => 'z', 'ᶼ' => 'ʐ', 'ᶽ' => 'ʑ', 'ᶾ' => 'ʒ', 'ᶿ' => 'θ', 'ẚ' => 'aʾ', 'ẛ' => 'ṡ', '᾽' => ' ̓', '᾿' => ' ̓', '῀' => ' ͂', '῁' => ' ̈͂', '῍' => ' ̓̀', '῎' => ' ̓́', '῏' => ' ̓͂', '῝' => ' ̔̀', '῞' => ' ̔́', '῟' => ' ̔͂', '῭' => ' ̈̀', '΅' => ' ̈́', '´' => ' ́', '῾' => ' ̔', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', '‑' => '‐', '‗' => ' ̳', '․' => '.', '‥' => '..', '…' => '...', ' ' => ' ', '″' => '′′', '‴' => '′′′', '‶' => '‵‵', '‷' => '‵‵‵', '‼' => '!!', '‾' => ' ̅', '⁇' => '??', '⁈' => '?!', '⁉' => '!?', '⁗' => '′′′′', ' ' => ' ', '⁰' => '0', 'ⁱ' => 'i', '⁴' => '4', '⁵' => '5', '⁶' => '6', '⁷' => '7', '⁸' => '8', '⁹' => '9', '⁺' => '+', '⁻' => '−', '⁼' => '=', '⁽' => '(', '⁾' => ')', 'ⁿ' => 'n', '₀' => '0', '₁' => '1', '₂' => '2', '₃' => '3', '₄' => '4', '₅' => '5', '₆' => '6', '₇' => '7', '₈' => '8', '₉' => '9', '₊' => '+', '₋' => '−', '₌' => '=', '₍' => '(', '₎' => ')', 'ₐ' => 'a', 'ₑ' => 'e', 'ₒ' => 'o', 'ₓ' => 'x', 'ₔ' => 'ə', 'ₕ' => 'h', 'ₖ' => 'k', 'ₗ' => 'l', 'ₘ' => 'm', 'ₙ' => 'n', 'ₚ' => 'p', 'ₛ' => 's', 'ₜ' => 't', '₨' => 'Rs', '℀' => 'a/c', '℁' => 'a/s', 'ℂ' => 'C', '℃' => '°C', '℅' => 'c/o', '℆' => 'c/u', 'ℇ' => 'Ɛ', '℉' => '°F', 'ℊ' => 'g', 'ℋ' => 'H', 'ℌ' => 'H', 'ℍ' => 'H', 'ℎ' => 'h', 'ℏ' => 'ħ', 'ℐ' => 'I', 'ℑ' => 'I', 'ℒ' => 'L', 'ℓ' => 'l', 'ℕ' => 'N', '№' => 'No', 'ℙ' => 'P', 'ℚ' => 'Q', 'ℛ' => 'R', 'ℜ' => 'R', 'ℝ' => 'R', '℠' => 'SM', '℡' => 'TEL', '™' => 'TM', 'ℤ' => 'Z', 'ℨ' => 'Z', 'ℬ' => 'B', 'ℭ' => 'C', 'ℯ' => 'e', 'ℰ' => 'E', 'ℱ' => 'F', 'ℳ' => 'M', 'ℴ' => 'o', 'ℵ' => 'א', 'ℶ' => 'ב', 'ℷ' => 'ג', 'ℸ' => 'ד', 'ℹ' => 'i', '℻' => 'FAX', 'ℼ' => 'π', 'ℽ' => 'γ', 'ℾ' => 'Γ', 'ℿ' => 'Π', '⅀' => '∑', 'ⅅ' => 'D', 'ⅆ' => 'd', 'ⅇ' => 'e', 'ⅈ' => 'i', 'ⅉ' => 'j', '⅐' => '1⁄7', '⅑' => '1⁄9', '⅒' => '1⁄10', '⅓' => '1⁄3', '⅔' => '2⁄3', '⅕' => '1⁄5', '⅖' => '2⁄5', '⅗' => '3⁄5', '⅘' => '4⁄5', '⅙' => '1⁄6', '⅚' => '5⁄6', '⅛' => '1⁄8', '⅜' => '3⁄8', '⅝' => '5⁄8', '⅞' => '7⁄8', '⅟' => '1⁄', 'Ⅰ' => 'I', 'Ⅱ' => 'II', 'Ⅲ' => 'III', 'Ⅳ' => 'IV', 'Ⅴ' => 'V', 'Ⅵ' => 'VI', 'Ⅶ' => 'VII', 'Ⅷ' => 'VIII', 'Ⅸ' => 'IX', 'Ⅹ' => 'X', 'Ⅺ' => 'XI', 'Ⅻ' => 'XII', 'Ⅼ' => 'L', 'Ⅽ' => 'C', 'Ⅾ' => 'D', 'Ⅿ' => 'M', 'ⅰ' => 'i', 'ⅱ' => 'ii', 'ⅲ' => 'iii', 'ⅳ' => 'iv', 'ⅴ' => 'v', 'ⅵ' => 'vi', 'ⅶ' => 'vii', 'ⅷ' => 'viii', 'ⅸ' => 'ix', 'ⅹ' => 'x', 'ⅺ' => 'xi', 'ⅻ' => 'xii', 'ⅼ' => 'l', 'ⅽ' => 'c', 'ⅾ' => 'd', 'ⅿ' => 'm', '↉' => '0⁄3', '∬' => '∫∫', '∭' => '∫∫∫', '∯' => '∮∮', '∰' => '∮∮∮', '①' => '1', '②' => '2', '③' => '3', '④' => '4', '⑤' => '5', '⑥' => '6', '⑦' => '7', '⑧' => '8', '⑨' => '9', '⑩' => '10', '⑪' => '11', '⑫' => '12', '⑬' => '13', '⑭' => '14', '⑮' => '15', '⑯' => '16', '⑰' => '17', '⑱' => '18', '⑲' => '19', '⑳' => '20', '⑴' => '(1)', '⑵' => '(2)', '⑶' => '(3)', '⑷' => '(4)', '⑸' => '(5)', '⑹' => '(6)', '⑺' => '(7)', '⑻' => '(8)', '⑼' => '(9)', '⑽' => '(10)', '⑾' => '(11)', '⑿' => '(12)', '⒀' => '(13)', '⒁' => '(14)', '⒂' => '(15)', '⒃' => '(16)', '⒄' => '(17)', '⒅' => '(18)', '⒆' => '(19)', '⒇' => '(20)', '⒈' => '1.', '⒉' => '2.', '⒊' => '3.', '⒋' => '4.', '⒌' => '5.', '⒍' => '6.', '⒎' => '7.', '⒏' => '8.', '⒐' => '9.', '⒑' => '10.', '⒒' => '11.', '⒓' => '12.', '⒔' => '13.', '⒕' => '14.', '⒖' => '15.', '⒗' => '16.', '⒘' => '17.', '⒙' => '18.', '⒚' => '19.', '⒛' => '20.', '⒜' => '(a)', '⒝' => '(b)', '⒞' => '(c)', '⒟' => '(d)', '⒠' => '(e)', '⒡' => '(f)', '⒢' => '(g)', '⒣' => '(h)', '⒤' => '(i)', '⒥' => '(j)', '⒦' => '(k)', '⒧' => '(l)', '⒨' => '(m)', '⒩' => '(n)', '⒪' => '(o)', '⒫' => '(p)', '⒬' => '(q)', '⒭' => '(r)', '⒮' => '(s)', '⒯' => '(t)', '⒰' => '(u)', '⒱' => '(v)', '⒲' => '(w)', '⒳' => '(x)', '⒴' => '(y)', '⒵' => '(z)', 'Ⓐ' => 'A', 'Ⓑ' => 'B', 'Ⓒ' => 'C', 'Ⓓ' => 'D', 'Ⓔ' => 'E', 'Ⓕ' => 'F', 'Ⓖ' => 'G', 'Ⓗ' => 'H', 'Ⓘ' => 'I', 'Ⓙ' => 'J', 'Ⓚ' => 'K', 'Ⓛ' => 'L', 'Ⓜ' => 'M', 'Ⓝ' => 'N', 'Ⓞ' => 'O', 'Ⓟ' => 'P', 'Ⓠ' => 'Q', 'Ⓡ' => 'R', 'Ⓢ' => 'S', 'Ⓣ' => 'T', 'Ⓤ' => 'U', 'Ⓥ' => 'V', 'Ⓦ' => 'W', 'Ⓧ' => 'X', 'Ⓨ' => 'Y', 'Ⓩ' => 'Z', 'ⓐ' => 'a', 'ⓑ' => 'b', 'ⓒ' => 'c', 'ⓓ' => 'd', 'ⓔ' => 'e', 'ⓕ' => 'f', 'ⓖ' => 'g', 'ⓗ' => 'h', 'ⓘ' => 'i', 'ⓙ' => 'j', 'ⓚ' => 'k', 'ⓛ' => 'l', 'ⓜ' => 'm', 'ⓝ' => 'n', 'ⓞ' => 'o', 'ⓟ' => 'p', 'ⓠ' => 'q', 'ⓡ' => 'r', 'ⓢ' => 's', 'ⓣ' => 't', 'ⓤ' => 'u', 'ⓥ' => 'v', 'ⓦ' => 'w', 'ⓧ' => 'x', 'ⓨ' => 'y', 'ⓩ' => 'z', '⓪' => '0', '⨌' => '∫∫∫∫', '⩴' => '::=', '⩵' => '==', '⩶' => '===', 'ⱼ' => 'j', 'ⱽ' => 'V', 'ⵯ' => 'ⵡ', '⺟' => '母', '⻳' => '龟', '⼀' => '一', '⼁' => '丨', '⼂' => '丶', '⼃' => '丿', '⼄' => '乙', '⼅' => '亅', '⼆' => '二', '⼇' => '亠', '⼈' => '人', '⼉' => '儿', '⼊' => '入', '⼋' => '八', '⼌' => '冂', '⼍' => '冖', '⼎' => '冫', '⼏' => '几', '⼐' => '凵', '⼑' => '刀', '⼒' => '力', '⼓' => '勹', '⼔' => '匕', '⼕' => '匚', '⼖' => '匸', '⼗' => '十', '⼘' => '卜', '⼙' => '卩', '⼚' => '厂', '⼛' => '厶', '⼜' => '又', '⼝' => '口', '⼞' => '囗', '⼟' => '土', '⼠' => '士', '⼡' => '夂', '⼢' => '夊', '⼣' => '夕', '⼤' => '大', '⼥' => '女', '⼦' => '子', '⼧' => '宀', '⼨' => '寸', '⼩' => '小', '⼪' => '尢', '⼫' => '尸', '⼬' => '屮', '⼭' => '山', '⼮' => '巛', '⼯' => '工', '⼰' => '己', '⼱' => '巾', '⼲' => '干', '⼳' => '幺', '⼴' => '广', '⼵' => '廴', '⼶' => '廾', '⼷' => '弋', '⼸' => '弓', '⼹' => '彐', '⼺' => '彡', '⼻' => '彳', '⼼' => '心', '⼽' => '戈', '⼾' => '戶', '⼿' => '手', '⽀' => '支', '⽁' => '攴', '⽂' => '文', '⽃' => '斗', '⽄' => '斤', '⽅' => '方', '⽆' => '无', '⽇' => '日', '⽈' => '曰', '⽉' => '月', '⽊' => '木', '⽋' => '欠', '⽌' => '止', '⽍' => '歹', '⽎' => '殳', '⽏' => '毋', '⽐' => '比', '⽑' => '毛', '⽒' => '氏', '⽓' => '气', '⽔' => '水', '⽕' => '火', '⽖' => '爪', '⽗' => '父', '⽘' => '爻', '⽙' => '爿', '⽚' => '片', '⽛' => '牙', '⽜' => '牛', '⽝' => '犬', '⽞' => '玄', '⽟' => '玉', '⽠' => '瓜', '⽡' => '瓦', '⽢' => '甘', '⽣' => '生', '⽤' => '用', '⽥' => '田', '⽦' => '疋', '⽧' => '疒', '⽨' => '癶', '⽩' => '白', '⽪' => '皮', '⽫' => '皿', '⽬' => '目', '⽭' => '矛', '⽮' => '矢', '⽯' => '石', '⽰' => '示', '⽱' => '禸', '⽲' => '禾', '⽳' => '穴', '⽴' => '立', '⽵' => '竹', '⽶' => '米', '⽷' => '糸', '⽸' => '缶', '⽹' => '网', '⽺' => '羊', '⽻' => '羽', '⽼' => '老', '⽽' => '而', '⽾' => '耒', '⽿' => '耳', '⾀' => '聿', '⾁' => '肉', '⾂' => '臣', '⾃' => '自', '⾄' => '至', '⾅' => '臼', '⾆' => '舌', '⾇' => '舛', '⾈' => '舟', '⾉' => '艮', '⾊' => '色', '⾋' => '艸', '⾌' => '虍', '⾍' => '虫', '⾎' => '血', '⾏' => '行', '⾐' => '衣', '⾑' => '襾', '⾒' => '見', '⾓' => '角', '⾔' => '言', '⾕' => '谷', '⾖' => '豆', '⾗' => '豕', '⾘' => '豸', '⾙' => '貝', '⾚' => '赤', '⾛' => '走', '⾜' => '足', '⾝' => '身', '⾞' => '車', '⾟' => '辛', '⾠' => '辰', '⾡' => '辵', '⾢' => '邑', '⾣' => '酉', '⾤' => '釆', '⾥' => '里', '⾦' => '金', '⾧' => '長', '⾨' => '門', '⾩' => '阜', '⾪' => '隶', '⾫' => '隹', '⾬' => '雨', '⾭' => '靑', '⾮' => '非', '⾯' => '面', '⾰' => '革', '⾱' => '韋', '⾲' => '韭', '⾳' => '音', '⾴' => '頁', '⾵' => '風', '⾶' => '飛', '⾷' => '食', '⾸' => '首', '⾹' => '香', '⾺' => '馬', '⾻' => '骨', '⾼' => '高', '⾽' => '髟', '⾾' => '鬥', '⾿' => '鬯', '⿀' => '鬲', '⿁' => '鬼', '⿂' => '魚', '⿃' => '鳥', '⿄' => '鹵', '⿅' => '鹿', '⿆' => '麥', '⿇' => '麻', '⿈' => '黃', '⿉' => '黍', '⿊' => '黑', '⿋' => '黹', '⿌' => '黽', '⿍' => '鼎', '⿎' => '鼓', '⿏' => '鼠', '⿐' => '鼻', '⿑' => '齊', '⿒' => '齒', '⿓' => '龍', '⿔' => '龜', '⿕' => '龠', ' ' => ' ', '〶' => '〒', '〸' => '十', '〹' => '卄', '〺' => '卅', '゛' => ' ゙', '゜' => ' ゚', 'ゟ' => 'より', 'ヿ' => 'コト', 'ㄱ' => 'ᄀ', 'ㄲ' => 'ᄁ', 'ㄳ' => 'ᆪ', 'ㄴ' => 'ᄂ', 'ㄵ' => 'ᆬ', 'ㄶ' => 'ᆭ', 'ㄷ' => 'ᄃ', 'ㄸ' => 'ᄄ', 'ㄹ' => 'ᄅ', 'ㄺ' => 'ᆰ', 'ㄻ' => 'ᆱ', 'ㄼ' => 'ᆲ', 'ㄽ' => 'ᆳ', 'ㄾ' => 'ᆴ', 'ㄿ' => 'ᆵ', 'ㅀ' => 'ᄚ', 'ㅁ' => 'ᄆ', 'ㅂ' => 'ᄇ', 'ㅃ' => 'ᄈ', 'ㅄ' => 'ᄡ', 'ㅅ' => 'ᄉ', 'ㅆ' => 'ᄊ', 'ㅇ' => 'ᄋ', 'ㅈ' => 'ᄌ', 'ㅉ' => 'ᄍ', 'ㅊ' => 'ᄎ', 'ㅋ' => 'ᄏ', 'ㅌ' => 'ᄐ', 'ㅍ' => 'ᄑ', 'ㅎ' => 'ᄒ', 'ㅏ' => 'ᅡ', 'ㅐ' => 'ᅢ', 'ㅑ' => 'ᅣ', 'ㅒ' => 'ᅤ', 'ㅓ' => 'ᅥ', 'ㅔ' => 'ᅦ', 'ㅕ' => 'ᅧ', 'ㅖ' => 'ᅨ', 'ㅗ' => 'ᅩ', 'ㅘ' => 'ᅪ', 'ㅙ' => 'ᅫ', 'ㅚ' => 'ᅬ', 'ㅛ' => 'ᅭ', 'ㅜ' => 'ᅮ', 'ㅝ' => 'ᅯ', 'ㅞ' => 'ᅰ', 'ㅟ' => 'ᅱ', 'ㅠ' => 'ᅲ', 'ㅡ' => 'ᅳ', 'ㅢ' => 'ᅴ', 'ㅣ' => 'ᅵ', 'ㅤ' => 'ᅠ', 'ㅥ' => 'ᄔ', 'ㅦ' => 'ᄕ', 'ㅧ' => 'ᇇ', 'ㅨ' => 'ᇈ', 'ㅩ' => 'ᇌ', 'ㅪ' => 'ᇎ', 'ㅫ' => 'ᇓ', 'ㅬ' => 'ᇗ', 'ㅭ' => 'ᇙ', 'ㅮ' => 'ᄜ', 'ㅯ' => 'ᇝ', 'ㅰ' => 'ᇟ', 'ㅱ' => 'ᄝ', 'ㅲ' => 'ᄞ', 'ㅳ' => 'ᄠ', 'ㅴ' => 'ᄢ', 'ㅵ' => 'ᄣ', 'ㅶ' => 'ᄧ', 'ㅷ' => 'ᄩ', 'ㅸ' => 'ᄫ', 'ㅹ' => 'ᄬ', 'ㅺ' => 'ᄭ', 'ㅻ' => 'ᄮ', 'ㅼ' => 'ᄯ', 'ㅽ' => 'ᄲ', 'ㅾ' => 'ᄶ', 'ㅿ' => 'ᅀ', 'ㆀ' => 'ᅇ', 'ㆁ' => 'ᅌ', 'ㆂ' => 'ᇱ', 'ㆃ' => 'ᇲ', 'ㆄ' => 'ᅗ', 'ㆅ' => 'ᅘ', 'ㆆ' => 'ᅙ', 'ㆇ' => 'ᆄ', 'ㆈ' => 'ᆅ', 'ㆉ' => 'ᆈ', 'ㆊ' => 'ᆑ', 'ㆋ' => 'ᆒ', 'ㆌ' => 'ᆔ', 'ㆍ' => 'ᆞ', 'ㆎ' => 'ᆡ', '㆒' => '一', '㆓' => '二', '㆔' => '三', '㆕' => '四', '㆖' => '上', '㆗' => '中', '㆘' => '下', '㆙' => '甲', '㆚' => '乙', '㆛' => '丙', '㆜' => '丁', '㆝' => '天', '㆞' => '地', '㆟' => '人', '㈀' => '(ᄀ)', '㈁' => '(ᄂ)', '㈂' => '(ᄃ)', '㈃' => '(ᄅ)', '㈄' => '(ᄆ)', '㈅' => '(ᄇ)', '㈆' => '(ᄉ)', '㈇' => '(ᄋ)', '㈈' => '(ᄌ)', '㈉' => '(ᄎ)', '㈊' => '(ᄏ)', '㈋' => '(ᄐ)', '㈌' => '(ᄑ)', '㈍' => '(ᄒ)', '㈎' => '(가)', '㈏' => '(나)', '㈐' => '(다)', '㈑' => '(라)', '㈒' => '(마)', '㈓' => '(바)', '㈔' => '(사)', '㈕' => '(아)', '㈖' => '(자)', '㈗' => '(차)', '㈘' => '(카)', '㈙' => '(타)', '㈚' => '(파)', '㈛' => '(하)', '㈜' => '(주)', '㈝' => '(오전)', '㈞' => '(오후)', '㈠' => '(一)', '㈡' => '(二)', '㈢' => '(三)', '㈣' => '(四)', '㈤' => '(五)', '㈥' => '(六)', '㈦' => '(七)', '㈧' => '(八)', '㈨' => '(九)', '㈩' => '(十)', '㈪' => '(月)', '㈫' => '(火)', '㈬' => '(水)', '㈭' => '(木)', '㈮' => '(金)', '㈯' => '(土)', '㈰' => '(日)', '㈱' => '(株)', '㈲' => '(有)', '㈳' => '(社)', '㈴' => '(名)', '㈵' => '(特)', '㈶' => '(財)', '㈷' => '(祝)', '㈸' => '(労)', '㈹' => '(代)', '㈺' => '(呼)', '㈻' => '(学)', '㈼' => '(監)', '㈽' => '(企)', '㈾' => '(資)', '㈿' => '(協)', '㉀' => '(祭)', '㉁' => '(休)', '㉂' => '(自)', '㉃' => '(至)', '㉄' => '問', '㉅' => '幼', '㉆' => '文', '㉇' => '箏', '㉐' => 'PTE', '㉑' => '21', '㉒' => '22', '㉓' => '23', '㉔' => '24', '㉕' => '25', '㉖' => '26', '㉗' => '27', '㉘' => '28', '㉙' => '29', '㉚' => '30', '㉛' => '31', '㉜' => '32', '㉝' => '33', '㉞' => '34', '㉟' => '35', '㉠' => 'ᄀ', '㉡' => 'ᄂ', '㉢' => 'ᄃ', '㉣' => 'ᄅ', '㉤' => 'ᄆ', '㉥' => 'ᄇ', '㉦' => 'ᄉ', '㉧' => 'ᄋ', '㉨' => 'ᄌ', '㉩' => 'ᄎ', '㉪' => 'ᄏ', '㉫' => 'ᄐ', '㉬' => 'ᄑ', '㉭' => 'ᄒ', '㉮' => '가', '㉯' => '나', '㉰' => '다', '㉱' => '라', '㉲' => '마', '㉳' => '바', '㉴' => '사', '㉵' => '아', '㉶' => '자', '㉷' => '차', '㉸' => '카', '㉹' => '타', '㉺' => '파', '㉻' => '하', '㉼' => '참고', '㉽' => '주의', '㉾' => '우', '㊀' => '一', '㊁' => '二', '㊂' => '三', '㊃' => '四', '㊄' => '五', '㊅' => '六', '㊆' => '七', '㊇' => '八', '㊈' => '九', '㊉' => '十', '㊊' => '月', '㊋' => '火', '㊌' => '水', '㊍' => '木', '㊎' => '金', '㊏' => '土', '㊐' => '日', '㊑' => '株', '㊒' => '有', '㊓' => '社', '㊔' => '名', '㊕' => '特', '㊖' => '財', '㊗' => '祝', '㊘' => '労', '㊙' => '秘', '㊚' => '男', '㊛' => '女', '㊜' => '適', '㊝' => '優', '㊞' => '印', '㊟' => '注', '㊠' => '項', '㊡' => '休', '㊢' => '写', '㊣' => '正', '㊤' => '上', '㊥' => '中', '㊦' => '下', '㊧' => '左', '㊨' => '右', '㊩' => '医', '㊪' => '宗', '㊫' => '学', '㊬' => '監', '㊭' => '企', '㊮' => '資', '㊯' => '協', '㊰' => '夜', '㊱' => '36', '㊲' => '37', '㊳' => '38', '㊴' => '39', '㊵' => '40', '㊶' => '41', '㊷' => '42', '㊸' => '43', '㊹' => '44', '㊺' => '45', '㊻' => '46', '㊼' => '47', '㊽' => '48', '㊾' => '49', '㊿' => '50', '㋀' => '1月', '㋁' => '2月', '㋂' => '3月', '㋃' => '4月', '㋄' => '5月', '㋅' => '6月', '㋆' => '7月', '㋇' => '8月', '㋈' => '9月', '㋉' => '10月', '㋊' => '11月', '㋋' => '12月', '㋌' => 'Hg', '㋍' => 'erg', '㋎' => 'eV', '㋏' => 'LTD', '㋐' => 'ア', '㋑' => 'イ', '㋒' => 'ウ', '㋓' => 'エ', '㋔' => 'オ', '㋕' => 'カ', '㋖' => 'キ', '㋗' => 'ク', '㋘' => 'ケ', '㋙' => 'コ', '㋚' => 'サ', '㋛' => 'シ', '㋜' => 'ス', '㋝' => 'セ', '㋞' => 'ソ', '㋟' => 'タ', '㋠' => 'チ', '㋡' => 'ツ', '㋢' => 'テ', '㋣' => 'ト', '㋤' => 'ナ', '㋥' => 'ニ', '㋦' => 'ヌ', '㋧' => 'ネ', '㋨' => 'ノ', '㋩' => 'ハ', '㋪' => 'ヒ', '㋫' => 'フ', '㋬' => 'ヘ', '㋭' => 'ホ', '㋮' => 'マ', '㋯' => 'ミ', '㋰' => 'ム', '㋱' => 'メ', '㋲' => 'モ', '㋳' => 'ヤ', '㋴' => 'ユ', '㋵' => 'ヨ', '㋶' => 'ラ', '㋷' => 'リ', '㋸' => 'ル', '㋹' => 'レ', '㋺' => 'ロ', '㋻' => 'ワ', '㋼' => 'ヰ', '㋽' => 'ヱ', '㋾' => 'ヲ', '㋿' => '令和', '㌀' => 'アパート', '㌁' => 'アルファ', '㌂' => 'アンペア', '㌃' => 'アール', '㌄' => 'イニング', '㌅' => 'インチ', '㌆' => 'ウォン', '㌇' => 'エスクード', '㌈' => 'エーカー', '㌉' => 'オンス', '㌊' => 'オーム', '㌋' => 'カイリ', '㌌' => 'カラット', '㌍' => 'カロリー', '㌎' => 'ガロン', '㌏' => 'ガンマ', '㌐' => 'ギガ', '㌑' => 'ギニー', '㌒' => 'キュリー', '㌓' => 'ギルダー', '㌔' => 'キロ', '㌕' => 'キログラム', '㌖' => 'キロメートル', '㌗' => 'キロワット', '㌘' => 'グラム', '㌙' => 'グラムトン', '㌚' => 'クルゼイロ', '㌛' => 'クローネ', '㌜' => 'ケース', '㌝' => 'コルナ', '㌞' => 'コーポ', '㌟' => 'サイクル', '㌠' => 'サンチーム', '㌡' => 'シリング', '㌢' => 'センチ', '㌣' => 'セント', '㌤' => 'ダース', '㌥' => 'デシ', '㌦' => 'ドル', '㌧' => 'トン', '㌨' => 'ナノ', '㌩' => 'ノット', '㌪' => 'ハイツ', '㌫' => 'パーセント', '㌬' => 'パーツ', '㌭' => 'バーレル', '㌮' => 'ピアストル', '㌯' => 'ピクル', '㌰' => 'ピコ', '㌱' => 'ビル', '㌲' => 'ファラッド', '㌳' => 'フィート', '㌴' => 'ブッシェル', '㌵' => 'フラン', '㌶' => 'ヘクタール', '㌷' => 'ペソ', '㌸' => 'ペニヒ', '㌹' => 'ヘルツ', '㌺' => 'ペンス', '㌻' => 'ページ', '㌼' => 'ベータ', '㌽' => 'ポイント', '㌾' => 'ボルト', '㌿' => 'ホン', '㍀' => 'ポンド', '㍁' => 'ホール', '㍂' => 'ホーン', '㍃' => 'マイクロ', '㍄' => 'マイル', '㍅' => 'マッハ', '㍆' => 'マルク', '㍇' => 'マンション', '㍈' => 'ミクロン', '㍉' => 'ミリ', '㍊' => 'ミリバール', '㍋' => 'メガ', '㍌' => 'メガトン', '㍍' => 'メートル', '㍎' => 'ヤード', '㍏' => 'ヤール', '㍐' => 'ユアン', '㍑' => 'リットル', '㍒' => 'リラ', '㍓' => 'ルピー', '㍔' => 'ルーブル', '㍕' => 'レム', '㍖' => 'レントゲン', '㍗' => 'ワット', '㍘' => '0点', '㍙' => '1点', '㍚' => '2点', '㍛' => '3点', '㍜' => '4点', '㍝' => '5点', '㍞' => '6点', '㍟' => '7点', '㍠' => '8点', '㍡' => '9点', '㍢' => '10点', '㍣' => '11点', '㍤' => '12点', '㍥' => '13点', '㍦' => '14点', '㍧' => '15点', '㍨' => '16点', '㍩' => '17点', '㍪' => '18点', '㍫' => '19点', '㍬' => '20点', '㍭' => '21点', '㍮' => '22点', '㍯' => '23点', '㍰' => '24点', '㍱' => 'hPa', '㍲' => 'da', '㍳' => 'AU', '㍴' => 'bar', '㍵' => 'oV', '㍶' => 'pc', '㍷' => 'dm', '㍸' => 'dm2', '㍹' => 'dm3', '㍺' => 'IU', '㍻' => '平成', '㍼' => '昭和', '㍽' => '大正', '㍾' => '明治', '㍿' => '株式会社', '㎀' => 'pA', '㎁' => 'nA', '㎂' => 'μA', '㎃' => 'mA', '㎄' => 'kA', '㎅' => 'KB', '㎆' => 'MB', '㎇' => 'GB', '㎈' => 'cal', '㎉' => 'kcal', '㎊' => 'pF', '㎋' => 'nF', '㎌' => 'μF', '㎍' => 'μg', '㎎' => 'mg', '㎏' => 'kg', '㎐' => 'Hz', '㎑' => 'kHz', '㎒' => 'MHz', '㎓' => 'GHz', '㎔' => 'THz', '㎕' => 'μl', '㎖' => 'ml', '㎗' => 'dl', '㎘' => 'kl', '㎙' => 'fm', '㎚' => 'nm', '㎛' => 'μm', '㎜' => 'mm', '㎝' => 'cm', '㎞' => 'km', '㎟' => 'mm2', '㎠' => 'cm2', '㎡' => 'm2', '㎢' => 'km2', '㎣' => 'mm3', '㎤' => 'cm3', '㎥' => 'm3', '㎦' => 'km3', '㎧' => 'm∕s', '㎨' => 'm∕s2', '㎩' => 'Pa', '㎪' => 'kPa', '㎫' => 'MPa', '㎬' => 'GPa', '㎭' => 'rad', '㎮' => 'rad∕s', '㎯' => 'rad∕s2', '㎰' => 'ps', '㎱' => 'ns', '㎲' => 'μs', '㎳' => 'ms', '㎴' => 'pV', '㎵' => 'nV', '㎶' => 'μV', '㎷' => 'mV', '㎸' => 'kV', '㎹' => 'MV', '㎺' => 'pW', '㎻' => 'nW', '㎼' => 'μW', '㎽' => 'mW', '㎾' => 'kW', '㎿' => 'MW', '㏀' => 'kΩ', '㏁' => 'MΩ', '㏂' => 'a.m.', '㏃' => 'Bq', '㏄' => 'cc', '㏅' => 'cd', '㏆' => 'C∕kg', '㏇' => 'Co.', '㏈' => 'dB', '㏉' => 'Gy', '㏊' => 'ha', '㏋' => 'HP', '㏌' => 'in', '㏍' => 'KK', '㏎' => 'KM', '㏏' => 'kt', '㏐' => 'lm', '㏑' => 'ln', '㏒' => 'log', '㏓' => 'lx', '㏔' => 'mb', '㏕' => 'mil', '㏖' => 'mol', '㏗' => 'PH', '㏘' => 'p.m.', '㏙' => 'PPM', '㏚' => 'PR', '㏛' => 'sr', '㏜' => 'Sv', '㏝' => 'Wb', '㏞' => 'V∕m', '㏟' => 'A∕m', '㏠' => '1日', '㏡' => '2日', '㏢' => '3日', '㏣' => '4日', '㏤' => '5日', '㏥' => '6日', '㏦' => '7日', '㏧' => '8日', '㏨' => '9日', '㏩' => '10日', '㏪' => '11日', '㏫' => '12日', '㏬' => '13日', '㏭' => '14日', '㏮' => '15日', '㏯' => '16日', '㏰' => '17日', '㏱' => '18日', '㏲' => '19日', '㏳' => '20日', '㏴' => '21日', '㏵' => '22日', '㏶' => '23日', '㏷' => '24日', '㏸' => '25日', '㏹' => '26日', '㏺' => '27日', '㏻' => '28日', '㏼' => '29日', '㏽' => '30日', '㏾' => '31日', '㏿' => 'gal', 'ꚜ' => 'ъ', 'ꚝ' => 'ь', 'ꝰ' => 'ꝯ', 'ꟸ' => 'Ħ', 'ꟹ' => 'œ', 'ꭜ' => 'ꜧ', 'ꭝ' => 'ꬷ', 'ꭞ' => 'ɫ', 'ꭟ' => 'ꭒ', 'ꭩ' => 'ʍ', 'ff' => 'ff', 'fi' => 'fi', 'fl' => 'fl', 'ffi' => 'ffi', 'ffl' => 'ffl', 'ſt' => 'st', 'st' => 'st', 'ﬓ' => 'մն', 'ﬔ' => 'մե', 'ﬕ' => 'մի', 'ﬖ' => 'վն', 'ﬗ' => 'մխ', 'ﬠ' => 'ע', 'ﬡ' => 'א', 'ﬢ' => 'ד', 'ﬣ' => 'ה', 'ﬤ' => 'כ', 'ﬥ' => 'ל', 'ﬦ' => 'ם', 'ﬧ' => 'ר', 'ﬨ' => 'ת', '﬩' => '+', 'ﭏ' => 'אל', 'ﭐ' => 'ٱ', 'ﭑ' => 'ٱ', 'ﭒ' => 'ٻ', 'ﭓ' => 'ٻ', 'ﭔ' => 'ٻ', 'ﭕ' => 'ٻ', 'ﭖ' => 'پ', 'ﭗ' => 'پ', 'ﭘ' => 'پ', 'ﭙ' => 'پ', 'ﭚ' => 'ڀ', 'ﭛ' => 'ڀ', 'ﭜ' => 'ڀ', 'ﭝ' => 'ڀ', 'ﭞ' => 'ٺ', 'ﭟ' => 'ٺ', 'ﭠ' => 'ٺ', 'ﭡ' => 'ٺ', 'ﭢ' => 'ٿ', 'ﭣ' => 'ٿ', 'ﭤ' => 'ٿ', 'ﭥ' => 'ٿ', 'ﭦ' => 'ٹ', 'ﭧ' => 'ٹ', 'ﭨ' => 'ٹ', 'ﭩ' => 'ٹ', 'ﭪ' => 'ڤ', 'ﭫ' => 'ڤ', 'ﭬ' => 'ڤ', 'ﭭ' => 'ڤ', 'ﭮ' => 'ڦ', 'ﭯ' => 'ڦ', 'ﭰ' => 'ڦ', 'ﭱ' => 'ڦ', 'ﭲ' => 'ڄ', 'ﭳ' => 'ڄ', 'ﭴ' => 'ڄ', 'ﭵ' => 'ڄ', 'ﭶ' => 'ڃ', 'ﭷ' => 'ڃ', 'ﭸ' => 'ڃ', 'ﭹ' => 'ڃ', 'ﭺ' => 'چ', 'ﭻ' => 'چ', 'ﭼ' => 'چ', 'ﭽ' => 'چ', 'ﭾ' => 'ڇ', 'ﭿ' => 'ڇ', 'ﮀ' => 'ڇ', 'ﮁ' => 'ڇ', 'ﮂ' => 'ڍ', 'ﮃ' => 'ڍ', 'ﮄ' => 'ڌ', 'ﮅ' => 'ڌ', 'ﮆ' => 'ڎ', 'ﮇ' => 'ڎ', 'ﮈ' => 'ڈ', 'ﮉ' => 'ڈ', 'ﮊ' => 'ژ', 'ﮋ' => 'ژ', 'ﮌ' => 'ڑ', 'ﮍ' => 'ڑ', 'ﮎ' => 'ک', 'ﮏ' => 'ک', 'ﮐ' => 'ک', 'ﮑ' => 'ک', 'ﮒ' => 'گ', 'ﮓ' => 'گ', 'ﮔ' => 'گ', 'ﮕ' => 'گ', 'ﮖ' => 'ڳ', 'ﮗ' => 'ڳ', 'ﮘ' => 'ڳ', 'ﮙ' => 'ڳ', 'ﮚ' => 'ڱ', 'ﮛ' => 'ڱ', 'ﮜ' => 'ڱ', 'ﮝ' => 'ڱ', 'ﮞ' => 'ں', 'ﮟ' => 'ں', 'ﮠ' => 'ڻ', 'ﮡ' => 'ڻ', 'ﮢ' => 'ڻ', 'ﮣ' => 'ڻ', 'ﮤ' => 'ۀ', 'ﮥ' => 'ۀ', 'ﮦ' => 'ہ', 'ﮧ' => 'ہ', 'ﮨ' => 'ہ', 'ﮩ' => 'ہ', 'ﮪ' => 'ھ', 'ﮫ' => 'ھ', 'ﮬ' => 'ھ', 'ﮭ' => 'ھ', 'ﮮ' => 'ے', 'ﮯ' => 'ے', 'ﮰ' => 'ۓ', 'ﮱ' => 'ۓ', 'ﯓ' => 'ڭ', 'ﯔ' => 'ڭ', 'ﯕ' => 'ڭ', 'ﯖ' => 'ڭ', 'ﯗ' => 'ۇ', 'ﯘ' => 'ۇ', 'ﯙ' => 'ۆ', 'ﯚ' => 'ۆ', 'ﯛ' => 'ۈ', 'ﯜ' => 'ۈ', 'ﯝ' => 'ۇٴ', 'ﯞ' => 'ۋ', 'ﯟ' => 'ۋ', 'ﯠ' => 'ۅ', 'ﯡ' => 'ۅ', 'ﯢ' => 'ۉ', 'ﯣ' => 'ۉ', 'ﯤ' => 'ې', 'ﯥ' => 'ې', 'ﯦ' => 'ې', 'ﯧ' => 'ې', 'ﯨ' => 'ى', 'ﯩ' => 'ى', 'ﯪ' => 'ئا', 'ﯫ' => 'ئا', 'ﯬ' => 'ئە', 'ﯭ' => 'ئە', 'ﯮ' => 'ئو', 'ﯯ' => 'ئو', 'ﯰ' => 'ئۇ', 'ﯱ' => 'ئۇ', 'ﯲ' => 'ئۆ', 'ﯳ' => 'ئۆ', 'ﯴ' => 'ئۈ', 'ﯵ' => 'ئۈ', 'ﯶ' => 'ئې', 'ﯷ' => 'ئې', 'ﯸ' => 'ئې', 'ﯹ' => 'ئى', 'ﯺ' => 'ئى', 'ﯻ' => 'ئى', 'ﯼ' => 'ی', 'ﯽ' => 'ی', 'ﯾ' => 'ی', 'ﯿ' => 'ی', 'ﰀ' => 'ئج', 'ﰁ' => 'ئح', 'ﰂ' => 'ئم', 'ﰃ' => 'ئى', 'ﰄ' => 'ئي', 'ﰅ' => 'بج', 'ﰆ' => 'بح', 'ﰇ' => 'بخ', 'ﰈ' => 'بم', 'ﰉ' => 'بى', 'ﰊ' => 'بي', 'ﰋ' => 'تج', 'ﰌ' => 'تح', 'ﰍ' => 'تخ', 'ﰎ' => 'تم', 'ﰏ' => 'تى', 'ﰐ' => 'تي', 'ﰑ' => 'ثج', 'ﰒ' => 'ثم', 'ﰓ' => 'ثى', 'ﰔ' => 'ثي', 'ﰕ' => 'جح', 'ﰖ' => 'جم', 'ﰗ' => 'حج', 'ﰘ' => 'حم', 'ﰙ' => 'خج', 'ﰚ' => 'خح', 'ﰛ' => 'خم', 'ﰜ' => 'سج', 'ﰝ' => 'سح', 'ﰞ' => 'سخ', 'ﰟ' => 'سم', 'ﰠ' => 'صح', 'ﰡ' => 'صم', 'ﰢ' => 'ضج', 'ﰣ' => 'ضح', 'ﰤ' => 'ضخ', 'ﰥ' => 'ضم', 'ﰦ' => 'طح', 'ﰧ' => 'طم', 'ﰨ' => 'ظم', 'ﰩ' => 'عج', 'ﰪ' => 'عم', 'ﰫ' => 'غج', 'ﰬ' => 'غم', 'ﰭ' => 'فج', 'ﰮ' => 'فح', 'ﰯ' => 'فخ', 'ﰰ' => 'فم', 'ﰱ' => 'فى', 'ﰲ' => 'في', 'ﰳ' => 'قح', 'ﰴ' => 'قم', 'ﰵ' => 'قى', 'ﰶ' => 'قي', 'ﰷ' => 'كا', 'ﰸ' => 'كج', 'ﰹ' => 'كح', 'ﰺ' => 'كخ', 'ﰻ' => 'كل', 'ﰼ' => 'كم', 'ﰽ' => 'كى', 'ﰾ' => 'كي', 'ﰿ' => 'لج', 'ﱀ' => 'لح', 'ﱁ' => 'لخ', 'ﱂ' => 'لم', 'ﱃ' => 'لى', 'ﱄ' => 'لي', 'ﱅ' => 'مج', 'ﱆ' => 'مح', 'ﱇ' => 'مخ', 'ﱈ' => 'مم', 'ﱉ' => 'مى', 'ﱊ' => 'مي', 'ﱋ' => 'نج', 'ﱌ' => 'نح', 'ﱍ' => 'نخ', 'ﱎ' => 'نم', 'ﱏ' => 'نى', 'ﱐ' => 'ني', 'ﱑ' => 'هج', 'ﱒ' => 'هم', 'ﱓ' => 'هى', 'ﱔ' => 'هي', 'ﱕ' => 'يج', 'ﱖ' => 'يح', 'ﱗ' => 'يخ', 'ﱘ' => 'يم', 'ﱙ' => 'يى', 'ﱚ' => 'يي', 'ﱛ' => 'ذٰ', 'ﱜ' => 'رٰ', 'ﱝ' => 'ىٰ', 'ﱞ' => ' ٌّ', 'ﱟ' => ' ٍّ', 'ﱠ' => ' َّ', 'ﱡ' => ' ُّ', 'ﱢ' => ' ِّ', 'ﱣ' => ' ّٰ', 'ﱤ' => 'ئر', 'ﱥ' => 'ئز', 'ﱦ' => 'ئم', 'ﱧ' => 'ئن', 'ﱨ' => 'ئى', 'ﱩ' => 'ئي', 'ﱪ' => 'بر', 'ﱫ' => 'بز', 'ﱬ' => 'بم', 'ﱭ' => 'بن', 'ﱮ' => 'بى', 'ﱯ' => 'بي', 'ﱰ' => 'تر', 'ﱱ' => 'تز', 'ﱲ' => 'تم', 'ﱳ' => 'تن', 'ﱴ' => 'تى', 'ﱵ' => 'تي', 'ﱶ' => 'ثر', 'ﱷ' => 'ثز', 'ﱸ' => 'ثم', 'ﱹ' => 'ثن', 'ﱺ' => 'ثى', 'ﱻ' => 'ثي', 'ﱼ' => 'فى', 'ﱽ' => 'في', 'ﱾ' => 'قى', 'ﱿ' => 'قي', 'ﲀ' => 'كا', 'ﲁ' => 'كل', 'ﲂ' => 'كم', 'ﲃ' => 'كى', 'ﲄ' => 'كي', 'ﲅ' => 'لم', 'ﲆ' => 'لى', 'ﲇ' => 'لي', 'ﲈ' => 'ما', 'ﲉ' => 'مم', 'ﲊ' => 'نر', 'ﲋ' => 'نز', 'ﲌ' => 'نم', 'ﲍ' => 'نن', 'ﲎ' => 'نى', 'ﲏ' => 'ني', 'ﲐ' => 'ىٰ', 'ﲑ' => 'ير', 'ﲒ' => 'يز', 'ﲓ' => 'يم', 'ﲔ' => 'ين', 'ﲕ' => 'يى', 'ﲖ' => 'يي', 'ﲗ' => 'ئج', 'ﲘ' => 'ئح', 'ﲙ' => 'ئخ', 'ﲚ' => 'ئم', 'ﲛ' => 'ئه', 'ﲜ' => 'بج', 'ﲝ' => 'بح', 'ﲞ' => 'بخ', 'ﲟ' => 'بم', 'ﲠ' => 'به', 'ﲡ' => 'تج', 'ﲢ' => 'تح', 'ﲣ' => 'تخ', 'ﲤ' => 'تم', 'ﲥ' => 'ته', 'ﲦ' => 'ثم', 'ﲧ' => 'جح', 'ﲨ' => 'جم', 'ﲩ' => 'حج', 'ﲪ' => 'حم', 'ﲫ' => 'خج', 'ﲬ' => 'خم', 'ﲭ' => 'سج', 'ﲮ' => 'سح', 'ﲯ' => 'سخ', 'ﲰ' => 'سم', 'ﲱ' => 'صح', 'ﲲ' => 'صخ', 'ﲳ' => 'صم', 'ﲴ' => 'ضج', 'ﲵ' => 'ضح', 'ﲶ' => 'ضخ', 'ﲷ' => 'ضم', 'ﲸ' => 'طح', 'ﲹ' => 'ظم', 'ﲺ' => 'عج', 'ﲻ' => 'عم', 'ﲼ' => 'غج', 'ﲽ' => 'غم', 'ﲾ' => 'فج', 'ﲿ' => 'فح', 'ﳀ' => 'فخ', 'ﳁ' => 'فم', 'ﳂ' => 'قح', 'ﳃ' => 'قم', 'ﳄ' => 'كج', 'ﳅ' => 'كح', 'ﳆ' => 'كخ', 'ﳇ' => 'كل', 'ﳈ' => 'كم', 'ﳉ' => 'لج', 'ﳊ' => 'لح', 'ﳋ' => 'لخ', 'ﳌ' => 'لم', 'ﳍ' => 'له', 'ﳎ' => 'مج', 'ﳏ' => 'مح', 'ﳐ' => 'مخ', 'ﳑ' => 'مم', 'ﳒ' => 'نج', 'ﳓ' => 'نح', 'ﳔ' => 'نخ', 'ﳕ' => 'نم', 'ﳖ' => 'نه', 'ﳗ' => 'هج', 'ﳘ' => 'هم', 'ﳙ' => 'هٰ', 'ﳚ' => 'يج', 'ﳛ' => 'يح', 'ﳜ' => 'يخ', 'ﳝ' => 'يم', 'ﳞ' => 'يه', 'ﳟ' => 'ئم', 'ﳠ' => 'ئه', 'ﳡ' => 'بم', 'ﳢ' => 'به', 'ﳣ' => 'تم', 'ﳤ' => 'ته', 'ﳥ' => 'ثم', 'ﳦ' => 'ثه', 'ﳧ' => 'سم', 'ﳨ' => 'سه', 'ﳩ' => 'شم', 'ﳪ' => 'شه', 'ﳫ' => 'كل', 'ﳬ' => 'كم', 'ﳭ' => 'لم', 'ﳮ' => 'نم', 'ﳯ' => 'نه', 'ﳰ' => 'يم', 'ﳱ' => 'يه', 'ﳲ' => 'ـَّ', 'ﳳ' => 'ـُّ', 'ﳴ' => 'ـِّ', 'ﳵ' => 'طى', 'ﳶ' => 'طي', 'ﳷ' => 'عى', 'ﳸ' => 'عي', 'ﳹ' => 'غى', 'ﳺ' => 'غي', 'ﳻ' => 'سى', 'ﳼ' => 'سي', 'ﳽ' => 'شى', 'ﳾ' => 'شي', 'ﳿ' => 'حى', 'ﴀ' => 'حي', 'ﴁ' => 'جى', 'ﴂ' => 'جي', 'ﴃ' => 'خى', 'ﴄ' => 'خي', 'ﴅ' => 'صى', 'ﴆ' => 'صي', 'ﴇ' => 'ضى', 'ﴈ' => 'ضي', 'ﴉ' => 'شج', 'ﴊ' => 'شح', 'ﴋ' => 'شخ', 'ﴌ' => 'شم', 'ﴍ' => 'شر', 'ﴎ' => 'سر', 'ﴏ' => 'صر', 'ﴐ' => 'ضر', 'ﴑ' => 'طى', 'ﴒ' => 'طي', 'ﴓ' => 'عى', 'ﴔ' => 'عي', 'ﴕ' => 'غى', 'ﴖ' => 'غي', 'ﴗ' => 'سى', 'ﴘ' => 'سي', 'ﴙ' => 'شى', 'ﴚ' => 'شي', 'ﴛ' => 'حى', 'ﴜ' => 'حي', 'ﴝ' => 'جى', 'ﴞ' => 'جي', 'ﴟ' => 'خى', 'ﴠ' => 'خي', 'ﴡ' => 'صى', 'ﴢ' => 'صي', 'ﴣ' => 'ضى', 'ﴤ' => 'ضي', 'ﴥ' => 'شج', 'ﴦ' => 'شح', 'ﴧ' => 'شخ', 'ﴨ' => 'شم', 'ﴩ' => 'شر', 'ﴪ' => 'سر', 'ﴫ' => 'صر', 'ﴬ' => 'ضر', 'ﴭ' => 'شج', 'ﴮ' => 'شح', 'ﴯ' => 'شخ', 'ﴰ' => 'شم', 'ﴱ' => 'سه', 'ﴲ' => 'شه', 'ﴳ' => 'طم', 'ﴴ' => 'سج', 'ﴵ' => 'سح', 'ﴶ' => 'سخ', 'ﴷ' => 'شج', 'ﴸ' => 'شح', 'ﴹ' => 'شخ', 'ﴺ' => 'طم', 'ﴻ' => 'ظم', 'ﴼ' => 'اً', 'ﴽ' => 'اً', 'ﵐ' => 'تجم', 'ﵑ' => 'تحج', 'ﵒ' => 'تحج', 'ﵓ' => 'تحم', 'ﵔ' => 'تخم', 'ﵕ' => 'تمج', 'ﵖ' => 'تمح', 'ﵗ' => 'تمخ', 'ﵘ' => 'جمح', 'ﵙ' => 'جمح', 'ﵚ' => 'حمي', 'ﵛ' => 'حمى', 'ﵜ' => 'سحج', 'ﵝ' => 'سجح', 'ﵞ' => 'سجى', 'ﵟ' => 'سمح', 'ﵠ' => 'سمح', 'ﵡ' => 'سمج', 'ﵢ' => 'سمم', 'ﵣ' => 'سمم', 'ﵤ' => 'صحح', 'ﵥ' => 'صحح', 'ﵦ' => 'صمم', 'ﵧ' => 'شحم', 'ﵨ' => 'شحم', 'ﵩ' => 'شجي', 'ﵪ' => 'شمخ', 'ﵫ' => 'شمخ', 'ﵬ' => 'شمم', 'ﵭ' => 'شمم', 'ﵮ' => 'ضحى', 'ﵯ' => 'ضخم', 'ﵰ' => 'ضخم', 'ﵱ' => 'طمح', 'ﵲ' => 'طمح', 'ﵳ' => 'طمم', 'ﵴ' => 'طمي', 'ﵵ' => 'عجم', 'ﵶ' => 'عمم', 'ﵷ' => 'عمم', 'ﵸ' => 'عمى', 'ﵹ' => 'غمم', 'ﵺ' => 'غمي', 'ﵻ' => 'غمى', 'ﵼ' => 'فخم', 'ﵽ' => 'فخم', 'ﵾ' => 'قمح', 'ﵿ' => 'قمم', 'ﶀ' => 'لحم', 'ﶁ' => 'لحي', 'ﶂ' => 'لحى', 'ﶃ' => 'لجج', 'ﶄ' => 'لجج', 'ﶅ' => 'لخم', 'ﶆ' => 'لخم', 'ﶇ' => 'لمح', 'ﶈ' => 'لمح', 'ﶉ' => 'محج', 'ﶊ' => 'محم', 'ﶋ' => 'محي', 'ﶌ' => 'مجح', 'ﶍ' => 'مجم', 'ﶎ' => 'مخج', 'ﶏ' => 'مخم', 'ﶒ' => 'مجخ', 'ﶓ' => 'همج', 'ﶔ' => 'همم', 'ﶕ' => 'نحم', 'ﶖ' => 'نحى', 'ﶗ' => 'نجم', 'ﶘ' => 'نجم', 'ﶙ' => 'نجى', 'ﶚ' => 'نمي', 'ﶛ' => 'نمى', 'ﶜ' => 'يمم', 'ﶝ' => 'يمم', 'ﶞ' => 'بخي', 'ﶟ' => 'تجي', 'ﶠ' => 'تجى', 'ﶡ' => 'تخي', 'ﶢ' => 'تخى', 'ﶣ' => 'تمي', 'ﶤ' => 'تمى', 'ﶥ' => 'جمي', 'ﶦ' => 'جحى', 'ﶧ' => 'جمى', 'ﶨ' => 'سخى', 'ﶩ' => 'صحي', 'ﶪ' => 'شحي', 'ﶫ' => 'ضحي', 'ﶬ' => 'لجي', 'ﶭ' => 'لمي', 'ﶮ' => 'يحي', 'ﶯ' => 'يجي', 'ﶰ' => 'يمي', 'ﶱ' => 'ممي', 'ﶲ' => 'قمي', 'ﶳ' => 'نحي', 'ﶴ' => 'قمح', 'ﶵ' => 'لحم', 'ﶶ' => 'عمي', 'ﶷ' => 'كمي', 'ﶸ' => 'نجح', 'ﶹ' => 'مخي', 'ﶺ' => 'لجم', 'ﶻ' => 'كمم', 'ﶼ' => 'لجم', 'ﶽ' => 'نجح', 'ﶾ' => 'جحي', 'ﶿ' => 'حجي', 'ﷀ' => 'مجي', 'ﷁ' => 'فمي', 'ﷂ' => 'بحي', 'ﷃ' => 'كمم', 'ﷄ' => 'عجم', 'ﷅ' => 'صمم', 'ﷆ' => 'سخي', 'ﷇ' => 'نجي', 'ﷰ' => 'صلے', 'ﷱ' => 'قلے', 'ﷲ' => 'الله', 'ﷳ' => 'اكبر', 'ﷴ' => 'محمد', 'ﷵ' => 'صلعم', 'ﷶ' => 'رسول', 'ﷷ' => 'عليه', 'ﷸ' => 'وسلم', 'ﷹ' => 'صلى', 'ﷺ' => 'صلى الله عليه وسلم', 'ﷻ' => 'جل جلاله', '﷼' => 'ریال', '︐' => ',', '︑' => '、', '︒' => '。', '︓' => ':', '︔' => ';', '︕' => '!', '︖' => '?', '︗' => '〖', '︘' => '〗', '︙' => '...', '︰' => '..', '︱' => '—', '︲' => '–', '︳' => '_', '︴' => '_', '︵' => '(', '︶' => ')', '︷' => '{', '︸' => '}', '︹' => '〔', '︺' => '〕', '︻' => '【', '︼' => '】', '︽' => '《', '︾' => '》', '︿' => '〈', '﹀' => '〉', '﹁' => '「', '﹂' => '」', '﹃' => '『', '﹄' => '』', '﹇' => '[', '﹈' => ']', '﹉' => ' ̅', '﹊' => ' ̅', '﹋' => ' ̅', '﹌' => ' ̅', '﹍' => '_', '﹎' => '_', '﹏' => '_', '﹐' => ',', '﹑' => '、', '﹒' => '.', '﹔' => ';', '﹕' => ':', '﹖' => '?', '﹗' => '!', '﹘' => '—', '﹙' => '(', '﹚' => ')', '﹛' => '{', '﹜' => '}', '﹝' => '〔', '﹞' => '〕', '﹟' => '#', '﹠' => '&', '﹡' => '*', '﹢' => '+', '﹣' => '-', '﹤' => '<', '﹥' => '>', '﹦' => '=', '﹨' => '\\', '﹩' => '$', '﹪' => '%', '﹫' => '@', 'ﹰ' => ' ً', 'ﹱ' => 'ـً', 'ﹲ' => ' ٌ', 'ﹴ' => ' ٍ', 'ﹶ' => ' َ', 'ﹷ' => 'ـَ', 'ﹸ' => ' ُ', 'ﹹ' => 'ـُ', 'ﹺ' => ' ِ', 'ﹻ' => 'ـِ', 'ﹼ' => ' ّ', 'ﹽ' => 'ـّ', 'ﹾ' => ' ْ', 'ﹿ' => 'ـْ', 'ﺀ' => 'ء', 'ﺁ' => 'آ', 'ﺂ' => 'آ', 'ﺃ' => 'أ', 'ﺄ' => 'أ', 'ﺅ' => 'ؤ', 'ﺆ' => 'ؤ', 'ﺇ' => 'إ', 'ﺈ' => 'إ', 'ﺉ' => 'ئ', 'ﺊ' => 'ئ', 'ﺋ' => 'ئ', 'ﺌ' => 'ئ', 'ﺍ' => 'ا', 'ﺎ' => 'ا', 'ﺏ' => 'ب', 'ﺐ' => 'ب', 'ﺑ' => 'ب', 'ﺒ' => 'ب', 'ﺓ' => 'ة', 'ﺔ' => 'ة', 'ﺕ' => 'ت', 'ﺖ' => 'ت', 'ﺗ' => 'ت', 'ﺘ' => 'ت', 'ﺙ' => 'ث', 'ﺚ' => 'ث', 'ﺛ' => 'ث', 'ﺜ' => 'ث', 'ﺝ' => 'ج', 'ﺞ' => 'ج', 'ﺟ' => 'ج', 'ﺠ' => 'ج', 'ﺡ' => 'ح', 'ﺢ' => 'ح', 'ﺣ' => 'ح', 'ﺤ' => 'ح', 'ﺥ' => 'خ', 'ﺦ' => 'خ', 'ﺧ' => 'خ', 'ﺨ' => 'خ', 'ﺩ' => 'د', 'ﺪ' => 'د', 'ﺫ' => 'ذ', 'ﺬ' => 'ذ', 'ﺭ' => 'ر', 'ﺮ' => 'ر', 'ﺯ' => 'ز', 'ﺰ' => 'ز', 'ﺱ' => 'س', 'ﺲ' => 'س', 'ﺳ' => 'س', 'ﺴ' => 'س', 'ﺵ' => 'ش', 'ﺶ' => 'ش', 'ﺷ' => 'ش', 'ﺸ' => 'ش', 'ﺹ' => 'ص', 'ﺺ' => 'ص', 'ﺻ' => 'ص', 'ﺼ' => 'ص', 'ﺽ' => 'ض', 'ﺾ' => 'ض', 'ﺿ' => 'ض', 'ﻀ' => 'ض', 'ﻁ' => 'ط', 'ﻂ' => 'ط', 'ﻃ' => 'ط', 'ﻄ' => 'ط', 'ﻅ' => 'ظ', 'ﻆ' => 'ظ', 'ﻇ' => 'ظ', 'ﻈ' => 'ظ', 'ﻉ' => 'ع', 'ﻊ' => 'ع', 'ﻋ' => 'ع', 'ﻌ' => 'ع', 'ﻍ' => 'غ', 'ﻎ' => 'غ', 'ﻏ' => 'غ', 'ﻐ' => 'غ', 'ﻑ' => 'ف', 'ﻒ' => 'ف', 'ﻓ' => 'ف', 'ﻔ' => 'ف', 'ﻕ' => 'ق', 'ﻖ' => 'ق', 'ﻗ' => 'ق', 'ﻘ' => 'ق', 'ﻙ' => 'ك', 'ﻚ' => 'ك', 'ﻛ' => 'ك', 'ﻜ' => 'ك', 'ﻝ' => 'ل', 'ﻞ' => 'ل', 'ﻟ' => 'ل', 'ﻠ' => 'ل', 'ﻡ' => 'م', 'ﻢ' => 'م', 'ﻣ' => 'م', 'ﻤ' => 'م', 'ﻥ' => 'ن', 'ﻦ' => 'ن', 'ﻧ' => 'ن', 'ﻨ' => 'ن', 'ﻩ' => 'ه', 'ﻪ' => 'ه', 'ﻫ' => 'ه', 'ﻬ' => 'ه', 'ﻭ' => 'و', 'ﻮ' => 'و', 'ﻯ' => 'ى', 'ﻰ' => 'ى', 'ﻱ' => 'ي', 'ﻲ' => 'ي', 'ﻳ' => 'ي', 'ﻴ' => 'ي', 'ﻵ' => 'لآ', 'ﻶ' => 'لآ', 'ﻷ' => 'لأ', 'ﻸ' => 'لأ', 'ﻹ' => 'لإ', 'ﻺ' => 'لإ', 'ﻻ' => 'لا', 'ﻼ' => 'لا', '!' => '!', '"' => '"', '#' => '#', '$' => '$', '%' => '%', '&' => '&', ''' => '\'', '(' => '(', ')' => ')', '*' => '*', '+' => '+', ',' => ',', '-' => '-', '.' => '.', '/' => '/', '0' => '0', '1' => '1', '2' => '2', '3' => '3', '4' => '4', '5' => '5', '6' => '6', '7' => '7', '8' => '8', '9' => '9', ':' => ':', ';' => ';', '<' => '<', '=' => '=', '>' => '>', '?' => '?', '@' => '@', 'A' => 'A', 'B' => 'B', 'C' => 'C', 'D' => 'D', 'E' => 'E', 'F' => 'F', 'G' => 'G', 'H' => 'H', 'I' => 'I', 'J' => 'J', 'K' => 'K', 'L' => 'L', 'M' => 'M', 'N' => 'N', 'O' => 'O', 'P' => 'P', 'Q' => 'Q', 'R' => 'R', 'S' => 'S', 'T' => 'T', 'U' => 'U', 'V' => 'V', 'W' => 'W', 'X' => 'X', 'Y' => 'Y', 'Z' => 'Z', '[' => '[', '\' => '\\', ']' => ']', '^' => '^', '_' => '_', '`' => '`', 'a' => 'a', 'b' => 'b', 'c' => 'c', 'd' => 'd', 'e' => 'e', 'f' => 'f', 'g' => 'g', 'h' => 'h', 'i' => 'i', 'j' => 'j', 'k' => 'k', 'l' => 'l', 'm' => 'm', 'n' => 'n', 'o' => 'o', 'p' => 'p', 'q' => 'q', 'r' => 'r', 's' => 's', 't' => 't', 'u' => 'u', 'v' => 'v', 'w' => 'w', 'x' => 'x', 'y' => 'y', 'z' => 'z', '{' => '{', '|' => '|', '}' => '}', '~' => '~', '⦅' => '⦅', '⦆' => '⦆', '。' => '。', '「' => '「', '」' => '」', '、' => '、', '・' => '・', 'ヲ' => 'ヲ', 'ァ' => 'ァ', 'ィ' => 'ィ', 'ゥ' => 'ゥ', 'ェ' => 'ェ', 'ォ' => 'ォ', 'ャ' => 'ャ', 'ュ' => 'ュ', 'ョ' => 'ョ', 'ッ' => 'ッ', 'ー' => 'ー', 'ア' => 'ア', 'イ' => 'イ', 'ウ' => 'ウ', 'エ' => 'エ', 'オ' => 'オ', 'カ' => 'カ', 'キ' => 'キ', 'ク' => 'ク', 'ケ' => 'ケ', 'コ' => 'コ', 'サ' => 'サ', 'シ' => 'シ', 'ス' => 'ス', 'セ' => 'セ', 'ソ' => 'ソ', 'タ' => 'タ', 'チ' => 'チ', 'ツ' => 'ツ', 'テ' => 'テ', 'ト' => 'ト', 'ナ' => 'ナ', 'ニ' => 'ニ', 'ヌ' => 'ヌ', 'ネ' => 'ネ', 'ノ' => 'ノ', 'ハ' => 'ハ', 'ヒ' => 'ヒ', 'フ' => 'フ', 'ヘ' => 'ヘ', 'ホ' => 'ホ', 'マ' => 'マ', 'ミ' => 'ミ', 'ム' => 'ム', 'メ' => 'メ', 'モ' => 'モ', 'ヤ' => 'ヤ', 'ユ' => 'ユ', 'ヨ' => 'ヨ', 'ラ' => 'ラ', 'リ' => 'リ', 'ル' => 'ル', 'レ' => 'レ', 'ロ' => 'ロ', 'ワ' => 'ワ', 'ン' => 'ン', '゙' => '゙', '゚' => '゚', 'ᅠ' => 'ᅠ', 'ᄀ' => 'ᄀ', 'ᄁ' => 'ᄁ', 'ᆪ' => 'ᆪ', 'ᄂ' => 'ᄂ', 'ᆬ' => 'ᆬ', 'ᆭ' => 'ᆭ', 'ᄃ' => 'ᄃ', 'ᄄ' => 'ᄄ', 'ᄅ' => 'ᄅ', 'ᆰ' => 'ᆰ', 'ᆱ' => 'ᆱ', 'ᆲ' => 'ᆲ', 'ᆳ' => 'ᆳ', 'ᆴ' => 'ᆴ', 'ᆵ' => 'ᆵ', 'ᄚ' => 'ᄚ', 'ᄆ' => 'ᄆ', 'ᄇ' => 'ᄇ', 'ᄈ' => 'ᄈ', 'ᄡ' => 'ᄡ', 'ᄉ' => 'ᄉ', 'ᄊ' => 'ᄊ', 'ᄋ' => 'ᄋ', 'ᄌ' => 'ᄌ', 'ᄍ' => 'ᄍ', 'ᄎ' => 'ᄎ', 'ᄏ' => 'ᄏ', 'ᄐ' => 'ᄐ', 'ᄑ' => 'ᄑ', 'ᄒ' => 'ᄒ', 'ᅡ' => 'ᅡ', 'ᅢ' => 'ᅢ', 'ᅣ' => 'ᅣ', 'ᅤ' => 'ᅤ', 'ᅥ' => 'ᅥ', 'ᅦ' => 'ᅦ', 'ᅧ' => 'ᅧ', 'ᅨ' => 'ᅨ', 'ᅩ' => 'ᅩ', 'ᅪ' => 'ᅪ', 'ᅫ' => 'ᅫ', 'ᅬ' => 'ᅬ', 'ᅭ' => 'ᅭ', 'ᅮ' => 'ᅮ', 'ᅯ' => 'ᅯ', 'ᅰ' => 'ᅰ', 'ᅱ' => 'ᅱ', 'ᅲ' => 'ᅲ', 'ᅳ' => 'ᅳ', 'ᅴ' => 'ᅴ', 'ᅵ' => 'ᅵ', '¢' => '¢', '£' => '£', '¬' => '¬', ' ̄' => ' ̄', '¦' => '¦', '¥' => '¥', '₩' => '₩', '│' => '│', '←' => '←', '↑' => '↑', '→' => '→', '↓' => '↓', '■' => '■', '○' => '○', '𝐀' => 'A', '𝐁' => 'B', '𝐂' => 'C', '𝐃' => 'D', '𝐄' => 'E', '𝐅' => 'F', '𝐆' => 'G', '𝐇' => 'H', '𝐈' => 'I', '𝐉' => 'J', '𝐊' => 'K', '𝐋' => 'L', '𝐌' => 'M', '𝐍' => 'N', '𝐎' => 'O', '𝐏' => 'P', '𝐐' => 'Q', '𝐑' => 'R', '𝐒' => 'S', '𝐓' => 'T', '𝐔' => 'U', '𝐕' => 'V', '𝐖' => 'W', '𝐗' => 'X', '𝐘' => 'Y', '𝐙' => 'Z', '𝐚' => 'a', '𝐛' => 'b', '𝐜' => 'c', '𝐝' => 'd', '𝐞' => 'e', '𝐟' => 'f', '𝐠' => 'g', '𝐡' => 'h', '𝐢' => 'i', '𝐣' => 'j', '𝐤' => 'k', '𝐥' => 'l', '𝐦' => 'm', '𝐧' => 'n', '𝐨' => 'o', '𝐩' => 'p', '𝐪' => 'q', '𝐫' => 'r', '𝐬' => 's', '𝐭' => 't', '𝐮' => 'u', '𝐯' => 'v', '𝐰' => 'w', '𝐱' => 'x', '𝐲' => 'y', '𝐳' => 'z', '𝐴' => 'A', '𝐵' => 'B', '𝐶' => 'C', '𝐷' => 'D', '𝐸' => 'E', '𝐹' => 'F', '𝐺' => 'G', '𝐻' => 'H', '𝐼' => 'I', '𝐽' => 'J', '𝐾' => 'K', '𝐿' => 'L', '𝑀' => 'M', '𝑁' => 'N', '𝑂' => 'O', '𝑃' => 'P', '𝑄' => 'Q', '𝑅' => 'R', '𝑆' => 'S', '𝑇' => 'T', '𝑈' => 'U', '𝑉' => 'V', '𝑊' => 'W', '𝑋' => 'X', '𝑌' => 'Y', '𝑍' => 'Z', '𝑎' => 'a', '𝑏' => 'b', '𝑐' => 'c', '𝑑' => 'd', '𝑒' => 'e', '𝑓' => 'f', '𝑔' => 'g', '𝑖' => 'i', '𝑗' => 'j', '𝑘' => 'k', '𝑙' => 'l', '𝑚' => 'm', '𝑛' => 'n', '𝑜' => 'o', '𝑝' => 'p', '𝑞' => 'q', '𝑟' => 'r', '𝑠' => 's', '𝑡' => 't', '𝑢' => 'u', '𝑣' => 'v', '𝑤' => 'w', '𝑥' => 'x', '𝑦' => 'y', '𝑧' => 'z', '𝑨' => 'A', '𝑩' => 'B', '𝑪' => 'C', '𝑫' => 'D', '𝑬' => 'E', '𝑭' => 'F', '𝑮' => 'G', '𝑯' => 'H', '𝑰' => 'I', '𝑱' => 'J', '𝑲' => 'K', '𝑳' => 'L', '𝑴' => 'M', '𝑵' => 'N', '𝑶' => 'O', '𝑷' => 'P', '𝑸' => 'Q', '𝑹' => 'R', '𝑺' => 'S', '𝑻' => 'T', '𝑼' => 'U', '𝑽' => 'V', '𝑾' => 'W', '𝑿' => 'X', '𝒀' => 'Y', '𝒁' => 'Z', '𝒂' => 'a', '𝒃' => 'b', '𝒄' => 'c', '𝒅' => 'd', '𝒆' => 'e', '𝒇' => 'f', '𝒈' => 'g', '𝒉' => 'h', '𝒊' => 'i', '𝒋' => 'j', '𝒌' => 'k', '𝒍' => 'l', '𝒎' => 'm', '𝒏' => 'n', '𝒐' => 'o', '𝒑' => 'p', '𝒒' => 'q', '𝒓' => 'r', '𝒔' => 's', '𝒕' => 't', '𝒖' => 'u', '𝒗' => 'v', '𝒘' => 'w', '𝒙' => 'x', '𝒚' => 'y', '𝒛' => 'z', '𝒜' => 'A', '𝒞' => 'C', '𝒟' => 'D', '𝒢' => 'G', '𝒥' => 'J', '𝒦' => 'K', '𝒩' => 'N', '𝒪' => 'O', '𝒫' => 'P', '𝒬' => 'Q', '𝒮' => 'S', '𝒯' => 'T', '𝒰' => 'U', '𝒱' => 'V', '𝒲' => 'W', '𝒳' => 'X', '𝒴' => 'Y', '𝒵' => 'Z', '𝒶' => 'a', '𝒷' => 'b', '𝒸' => 'c', '𝒹' => 'd', '𝒻' => 'f', '𝒽' => 'h', '𝒾' => 'i', '𝒿' => 'j', '𝓀' => 'k', '𝓁' => 'l', '𝓂' => 'm', '𝓃' => 'n', '𝓅' => 'p', '𝓆' => 'q', '𝓇' => 'r', '𝓈' => 's', '𝓉' => 't', '𝓊' => 'u', '𝓋' => 'v', '𝓌' => 'w', '𝓍' => 'x', '𝓎' => 'y', '𝓏' => 'z', '𝓐' => 'A', '𝓑' => 'B', '𝓒' => 'C', '𝓓' => 'D', '𝓔' => 'E', '𝓕' => 'F', '𝓖' => 'G', '𝓗' => 'H', '𝓘' => 'I', '𝓙' => 'J', '𝓚' => 'K', '𝓛' => 'L', '𝓜' => 'M', '𝓝' => 'N', '𝓞' => 'O', '𝓟' => 'P', '𝓠' => 'Q', '𝓡' => 'R', '𝓢' => 'S', '𝓣' => 'T', '𝓤' => 'U', '𝓥' => 'V', '𝓦' => 'W', '𝓧' => 'X', '𝓨' => 'Y', '𝓩' => 'Z', '𝓪' => 'a', '𝓫' => 'b', '𝓬' => 'c', '𝓭' => 'd', '𝓮' => 'e', '𝓯' => 'f', '𝓰' => 'g', '𝓱' => 'h', '𝓲' => 'i', '𝓳' => 'j', '𝓴' => 'k', '𝓵' => 'l', '𝓶' => 'm', '𝓷' => 'n', '𝓸' => 'o', '𝓹' => 'p', '𝓺' => 'q', '𝓻' => 'r', '𝓼' => 's', '𝓽' => 't', '𝓾' => 'u', '𝓿' => 'v', '𝔀' => 'w', '𝔁' => 'x', '𝔂' => 'y', '𝔃' => 'z', '𝔄' => 'A', '𝔅' => 'B', '𝔇' => 'D', '𝔈' => 'E', '𝔉' => 'F', '𝔊' => 'G', '𝔍' => 'J', '𝔎' => 'K', '𝔏' => 'L', '𝔐' => 'M', '𝔑' => 'N', '𝔒' => 'O', '𝔓' => 'P', '𝔔' => 'Q', '𝔖' => 'S', '𝔗' => 'T', '𝔘' => 'U', '𝔙' => 'V', '𝔚' => 'W', '𝔛' => 'X', '𝔜' => 'Y', '𝔞' => 'a', '𝔟' => 'b', '𝔠' => 'c', '𝔡' => 'd', '𝔢' => 'e', '𝔣' => 'f', '𝔤' => 'g', '𝔥' => 'h', '𝔦' => 'i', '𝔧' => 'j', '𝔨' => 'k', '𝔩' => 'l', '𝔪' => 'm', '𝔫' => 'n', '𝔬' => 'o', '𝔭' => 'p', '𝔮' => 'q', '𝔯' => 'r', '𝔰' => 's', '𝔱' => 't', '𝔲' => 'u', '𝔳' => 'v', '𝔴' => 'w', '𝔵' => 'x', '𝔶' => 'y', '𝔷' => 'z', '𝔸' => 'A', '𝔹' => 'B', '𝔻' => 'D', '𝔼' => 'E', '𝔽' => 'F', '𝔾' => 'G', '𝕀' => 'I', '𝕁' => 'J', '𝕂' => 'K', '𝕃' => 'L', '𝕄' => 'M', '𝕆' => 'O', '𝕊' => 'S', '𝕋' => 'T', '𝕌' => 'U', '𝕍' => 'V', '𝕎' => 'W', '𝕏' => 'X', '𝕐' => 'Y', '𝕒' => 'a', '𝕓' => 'b', '𝕔' => 'c', '𝕕' => 'd', '𝕖' => 'e', '𝕗' => 'f', '𝕘' => 'g', '𝕙' => 'h', '𝕚' => 'i', '𝕛' => 'j', '𝕜' => 'k', '𝕝' => 'l', '𝕞' => 'm', '𝕟' => 'n', '𝕠' => 'o', '𝕡' => 'p', '𝕢' => 'q', '𝕣' => 'r', '𝕤' => 's', '𝕥' => 't', '𝕦' => 'u', '𝕧' => 'v', '𝕨' => 'w', '𝕩' => 'x', '𝕪' => 'y', '𝕫' => 'z', '𝕬' => 'A', '𝕭' => 'B', '𝕮' => 'C', '𝕯' => 'D', '𝕰' => 'E', '𝕱' => 'F', '𝕲' => 'G', '𝕳' => 'H', '𝕴' => 'I', '𝕵' => 'J', '𝕶' => 'K', '𝕷' => 'L', '𝕸' => 'M', '𝕹' => 'N', '𝕺' => 'O', '𝕻' => 'P', '𝕼' => 'Q', '𝕽' => 'R', '𝕾' => 'S', '𝕿' => 'T', '𝖀' => 'U', '𝖁' => 'V', '𝖂' => 'W', '𝖃' => 'X', '𝖄' => 'Y', '𝖅' => 'Z', '𝖆' => 'a', '𝖇' => 'b', '𝖈' => 'c', '𝖉' => 'd', '𝖊' => 'e', '𝖋' => 'f', '𝖌' => 'g', '𝖍' => 'h', '𝖎' => 'i', '𝖏' => 'j', '𝖐' => 'k', '𝖑' => 'l', '𝖒' => 'm', '𝖓' => 'n', '𝖔' => 'o', '𝖕' => 'p', '𝖖' => 'q', '𝖗' => 'r', '𝖘' => 's', '𝖙' => 't', '𝖚' => 'u', '𝖛' => 'v', '𝖜' => 'w', '𝖝' => 'x', '𝖞' => 'y', '𝖟' => 'z', '𝖠' => 'A', '𝖡' => 'B', '𝖢' => 'C', '𝖣' => 'D', '𝖤' => 'E', '𝖥' => 'F', '𝖦' => 'G', '𝖧' => 'H', '𝖨' => 'I', '𝖩' => 'J', '𝖪' => 'K', '𝖫' => 'L', '𝖬' => 'M', '𝖭' => 'N', '𝖮' => 'O', '𝖯' => 'P', '𝖰' => 'Q', '𝖱' => 'R', '𝖲' => 'S', '𝖳' => 'T', '𝖴' => 'U', '𝖵' => 'V', '𝖶' => 'W', '𝖷' => 'X', '𝖸' => 'Y', '𝖹' => 'Z', '𝖺' => 'a', '𝖻' => 'b', '𝖼' => 'c', '𝖽' => 'd', '𝖾' => 'e', '𝖿' => 'f', '𝗀' => 'g', '𝗁' => 'h', '𝗂' => 'i', '𝗃' => 'j', '𝗄' => 'k', '𝗅' => 'l', '𝗆' => 'm', '𝗇' => 'n', '𝗈' => 'o', '𝗉' => 'p', '𝗊' => 'q', '𝗋' => 'r', '𝗌' => 's', '𝗍' => 't', '𝗎' => 'u', '𝗏' => 'v', '𝗐' => 'w', '𝗑' => 'x', '𝗒' => 'y', '𝗓' => 'z', '𝗔' => 'A', '𝗕' => 'B', '𝗖' => 'C', '𝗗' => 'D', '𝗘' => 'E', '𝗙' => 'F', '𝗚' => 'G', '𝗛' => 'H', '𝗜' => 'I', '𝗝' => 'J', '𝗞' => 'K', '𝗟' => 'L', '𝗠' => 'M', '𝗡' => 'N', '𝗢' => 'O', '𝗣' => 'P', '𝗤' => 'Q', '𝗥' => 'R', '𝗦' => 'S', '𝗧' => 'T', '𝗨' => 'U', '𝗩' => 'V', '𝗪' => 'W', '𝗫' => 'X', '𝗬' => 'Y', '𝗭' => 'Z', '𝗮' => 'a', '𝗯' => 'b', '𝗰' => 'c', '𝗱' => 'd', '𝗲' => 'e', '𝗳' => 'f', '𝗴' => 'g', '𝗵' => 'h', '𝗶' => 'i', '𝗷' => 'j', '𝗸' => 'k', '𝗹' => 'l', '𝗺' => 'm', '𝗻' => 'n', '𝗼' => 'o', '𝗽' => 'p', '𝗾' => 'q', '𝗿' => 'r', '𝘀' => 's', '𝘁' => 't', '𝘂' => 'u', '𝘃' => 'v', '𝘄' => 'w', '𝘅' => 'x', '𝘆' => 'y', '𝘇' => 'z', '𝘈' => 'A', '𝘉' => 'B', '𝘊' => 'C', '𝘋' => 'D', '𝘌' => 'E', '𝘍' => 'F', '𝘎' => 'G', '𝘏' => 'H', '𝘐' => 'I', '𝘑' => 'J', '𝘒' => 'K', '𝘓' => 'L', '𝘔' => 'M', '𝘕' => 'N', '𝘖' => 'O', '𝘗' => 'P', '𝘘' => 'Q', '𝘙' => 'R', '𝘚' => 'S', '𝘛' => 'T', '𝘜' => 'U', '𝘝' => 'V', '𝘞' => 'W', '𝘟' => 'X', '𝘠' => 'Y', '𝘡' => 'Z', '𝘢' => 'a', '𝘣' => 'b', '𝘤' => 'c', '𝘥' => 'd', '𝘦' => 'e', '𝘧' => 'f', '𝘨' => 'g', '𝘩' => 'h', '𝘪' => 'i', '𝘫' => 'j', '𝘬' => 'k', '𝘭' => 'l', '𝘮' => 'm', '𝘯' => 'n', '𝘰' => 'o', '𝘱' => 'p', '𝘲' => 'q', '𝘳' => 'r', '𝘴' => 's', '𝘵' => 't', '𝘶' => 'u', '𝘷' => 'v', '𝘸' => 'w', '𝘹' => 'x', '𝘺' => 'y', '𝘻' => 'z', '𝘼' => 'A', '𝘽' => 'B', '𝘾' => 'C', '𝘿' => 'D', '𝙀' => 'E', '𝙁' => 'F', '𝙂' => 'G', '𝙃' => 'H', '𝙄' => 'I', '𝙅' => 'J', '𝙆' => 'K', '𝙇' => 'L', '𝙈' => 'M', '𝙉' => 'N', '𝙊' => 'O', '𝙋' => 'P', '𝙌' => 'Q', '𝙍' => 'R', '𝙎' => 'S', '𝙏' => 'T', '𝙐' => 'U', '𝙑' => 'V', '𝙒' => 'W', '𝙓' => 'X', '𝙔' => 'Y', '𝙕' => 'Z', '𝙖' => 'a', '𝙗' => 'b', '𝙘' => 'c', '𝙙' => 'd', '𝙚' => 'e', '𝙛' => 'f', '𝙜' => 'g', '𝙝' => 'h', '𝙞' => 'i', '𝙟' => 'j', '𝙠' => 'k', '𝙡' => 'l', '𝙢' => 'm', '𝙣' => 'n', '𝙤' => 'o', '𝙥' => 'p', '𝙦' => 'q', '𝙧' => 'r', '𝙨' => 's', '𝙩' => 't', '𝙪' => 'u', '𝙫' => 'v', '𝙬' => 'w', '𝙭' => 'x', '𝙮' => 'y', '𝙯' => 'z', '𝙰' => 'A', '𝙱' => 'B', '𝙲' => 'C', '𝙳' => 'D', '𝙴' => 'E', '𝙵' => 'F', '𝙶' => 'G', '𝙷' => 'H', '𝙸' => 'I', '𝙹' => 'J', '𝙺' => 'K', '𝙻' => 'L', '𝙼' => 'M', '𝙽' => 'N', '𝙾' => 'O', '𝙿' => 'P', '𝚀' => 'Q', '𝚁' => 'R', '𝚂' => 'S', '𝚃' => 'T', '𝚄' => 'U', '𝚅' => 'V', '𝚆' => 'W', '𝚇' => 'X', '𝚈' => 'Y', '𝚉' => 'Z', '𝚊' => 'a', '𝚋' => 'b', '𝚌' => 'c', '𝚍' => 'd', '𝚎' => 'e', '𝚏' => 'f', '𝚐' => 'g', '𝚑' => 'h', '𝚒' => 'i', '𝚓' => 'j', '𝚔' => 'k', '𝚕' => 'l', '𝚖' => 'm', '𝚗' => 'n', '𝚘' => 'o', '𝚙' => 'p', '𝚚' => 'q', '𝚛' => 'r', '𝚜' => 's', '𝚝' => 't', '𝚞' => 'u', '𝚟' => 'v', '𝚠' => 'w', '𝚡' => 'x', '𝚢' => 'y', '𝚣' => 'z', '𝚤' => 'ı', '𝚥' => 'ȷ', '𝚨' => 'Α', '𝚩' => 'Β', '𝚪' => 'Γ', '𝚫' => 'Δ', '𝚬' => 'Ε', '𝚭' => 'Ζ', '𝚮' => 'Η', '𝚯' => 'Θ', '𝚰' => 'Ι', '𝚱' => 'Κ', '𝚲' => 'Λ', '𝚳' => 'Μ', '𝚴' => 'Ν', '𝚵' => 'Ξ', '𝚶' => 'Ο', '𝚷' => 'Π', '𝚸' => 'Ρ', '𝚹' => 'Θ', '𝚺' => 'Σ', '𝚻' => 'Τ', '𝚼' => 'Υ', '𝚽' => 'Φ', '𝚾' => 'Χ', '𝚿' => 'Ψ', '𝛀' => 'Ω', '𝛁' => '∇', '𝛂' => 'α', '𝛃' => 'β', '𝛄' => 'γ', '𝛅' => 'δ', '𝛆' => 'ε', '𝛇' => 'ζ', '𝛈' => 'η', '𝛉' => 'θ', '𝛊' => 'ι', '𝛋' => 'κ', '𝛌' => 'λ', '𝛍' => 'μ', '𝛎' => 'ν', '𝛏' => 'ξ', '𝛐' => 'ο', '𝛑' => 'π', '𝛒' => 'ρ', '𝛓' => 'ς', '𝛔' => 'σ', '𝛕' => 'τ', '𝛖' => 'υ', '𝛗' => 'φ', '𝛘' => 'χ', '𝛙' => 'ψ', '𝛚' => 'ω', '𝛛' => '∂', '𝛜' => 'ε', '𝛝' => 'θ', '𝛞' => 'κ', '𝛟' => 'φ', '𝛠' => 'ρ', '𝛡' => 'π', '𝛢' => 'Α', '𝛣' => 'Β', '𝛤' => 'Γ', '𝛥' => 'Δ', '𝛦' => 'Ε', '𝛧' => 'Ζ', '𝛨' => 'Η', '𝛩' => 'Θ', '𝛪' => 'Ι', '𝛫' => 'Κ', '𝛬' => 'Λ', '𝛭' => 'Μ', '𝛮' => 'Ν', '𝛯' => 'Ξ', '𝛰' => 'Ο', '𝛱' => 'Π', '𝛲' => 'Ρ', '𝛳' => 'Θ', '𝛴' => 'Σ', '𝛵' => 'Τ', '𝛶' => 'Υ', '𝛷' => 'Φ', '𝛸' => 'Χ', '𝛹' => 'Ψ', '𝛺' => 'Ω', '𝛻' => '∇', '𝛼' => 'α', '𝛽' => 'β', '𝛾' => 'γ', '𝛿' => 'δ', '𝜀' => 'ε', '𝜁' => 'ζ', '𝜂' => 'η', '𝜃' => 'θ', '𝜄' => 'ι', '𝜅' => 'κ', '𝜆' => 'λ', '𝜇' => 'μ', '𝜈' => 'ν', '𝜉' => 'ξ', '𝜊' => 'ο', '𝜋' => 'π', '𝜌' => 'ρ', '𝜍' => 'ς', '𝜎' => 'σ', '𝜏' => 'τ', '𝜐' => 'υ', '𝜑' => 'φ', '𝜒' => 'χ', '𝜓' => 'ψ', '𝜔' => 'ω', '𝜕' => '∂', '𝜖' => 'ε', '𝜗' => 'θ', '𝜘' => 'κ', '𝜙' => 'φ', '𝜚' => 'ρ', '𝜛' => 'π', '𝜜' => 'Α', '𝜝' => 'Β', '𝜞' => 'Γ', '𝜟' => 'Δ', '𝜠' => 'Ε', '𝜡' => 'Ζ', '𝜢' => 'Η', '𝜣' => 'Θ', '𝜤' => 'Ι', '𝜥' => 'Κ', '𝜦' => 'Λ', '𝜧' => 'Μ', '𝜨' => 'Ν', '𝜩' => 'Ξ', '𝜪' => 'Ο', '𝜫' => 'Π', '𝜬' => 'Ρ', '𝜭' => 'Θ', '𝜮' => 'Σ', '𝜯' => 'Τ', '𝜰' => 'Υ', '𝜱' => 'Φ', '𝜲' => 'Χ', '𝜳' => 'Ψ', '𝜴' => 'Ω', '𝜵' => '∇', '𝜶' => 'α', '𝜷' => 'β', '𝜸' => 'γ', '𝜹' => 'δ', '𝜺' => 'ε', '𝜻' => 'ζ', '𝜼' => 'η', '𝜽' => 'θ', '𝜾' => 'ι', '𝜿' => 'κ', '𝝀' => 'λ', '𝝁' => 'μ', '𝝂' => 'ν', '𝝃' => 'ξ', '𝝄' => 'ο', '𝝅' => 'π', '𝝆' => 'ρ', '𝝇' => 'ς', '𝝈' => 'σ', '𝝉' => 'τ', '𝝊' => 'υ', '𝝋' => 'φ', '𝝌' => 'χ', '𝝍' => 'ψ', '𝝎' => 'ω', '𝝏' => '∂', '𝝐' => 'ε', '𝝑' => 'θ', '𝝒' => 'κ', '𝝓' => 'φ', '𝝔' => 'ρ', '𝝕' => 'π', '𝝖' => 'Α', '𝝗' => 'Β', '𝝘' => 'Γ', '𝝙' => 'Δ', '𝝚' => 'Ε', '𝝛' => 'Ζ', '𝝜' => 'Η', '𝝝' => 'Θ', '𝝞' => 'Ι', '𝝟' => 'Κ', '𝝠' => 'Λ', '𝝡' => 'Μ', '𝝢' => 'Ν', '𝝣' => 'Ξ', '𝝤' => 'Ο', '𝝥' => 'Π', '𝝦' => 'Ρ', '𝝧' => 'Θ', '𝝨' => 'Σ', '𝝩' => 'Τ', '𝝪' => 'Υ', '𝝫' => 'Φ', '𝝬' => 'Χ', '𝝭' => 'Ψ', '𝝮' => 'Ω', '𝝯' => '∇', '𝝰' => 'α', '𝝱' => 'β', '𝝲' => 'γ', '𝝳' => 'δ', '𝝴' => 'ε', '𝝵' => 'ζ', '𝝶' => 'η', '𝝷' => 'θ', '𝝸' => 'ι', '𝝹' => 'κ', '𝝺' => 'λ', '𝝻' => 'μ', '𝝼' => 'ν', '𝝽' => 'ξ', '𝝾' => 'ο', '𝝿' => 'π', '𝞀' => 'ρ', '𝞁' => 'ς', '𝞂' => 'σ', '𝞃' => 'τ', '𝞄' => 'υ', '𝞅' => 'φ', '𝞆' => 'χ', '𝞇' => 'ψ', '𝞈' => 'ω', '𝞉' => '∂', '𝞊' => 'ε', '𝞋' => 'θ', '𝞌' => 'κ', '𝞍' => 'φ', '𝞎' => 'ρ', '𝞏' => 'π', '𝞐' => 'Α', '𝞑' => 'Β', '𝞒' => 'Γ', '𝞓' => 'Δ', '𝞔' => 'Ε', '𝞕' => 'Ζ', '𝞖' => 'Η', '𝞗' => 'Θ', '𝞘' => 'Ι', '𝞙' => 'Κ', '𝞚' => 'Λ', '𝞛' => 'Μ', '𝞜' => 'Ν', '𝞝' => 'Ξ', '𝞞' => 'Ο', '𝞟' => 'Π', '𝞠' => 'Ρ', '𝞡' => 'Θ', '𝞢' => 'Σ', '𝞣' => 'Τ', '𝞤' => 'Υ', '𝞥' => 'Φ', '𝞦' => 'Χ', '𝞧' => 'Ψ', '𝞨' => 'Ω', '𝞩' => '∇', '𝞪' => 'α', '𝞫' => 'β', '𝞬' => 'γ', '𝞭' => 'δ', '𝞮' => 'ε', '𝞯' => 'ζ', '𝞰' => 'η', '𝞱' => 'θ', '𝞲' => 'ι', '𝞳' => 'κ', '𝞴' => 'λ', '𝞵' => 'μ', '𝞶' => 'ν', '𝞷' => 'ξ', '𝞸' => 'ο', '𝞹' => 'π', '𝞺' => 'ρ', '𝞻' => 'ς', '𝞼' => 'σ', '𝞽' => 'τ', '𝞾' => 'υ', '𝞿' => 'φ', '𝟀' => 'χ', '𝟁' => 'ψ', '𝟂' => 'ω', '𝟃' => '∂', '𝟄' => 'ε', '𝟅' => 'θ', '𝟆' => 'κ', '𝟇' => 'φ', '𝟈' => 'ρ', '𝟉' => 'π', '𝟊' => 'Ϝ', '𝟋' => 'ϝ', '𝟎' => '0', '𝟏' => '1', '𝟐' => '2', '𝟑' => '3', '𝟒' => '4', '𝟓' => '5', '𝟔' => '6', '𝟕' => '7', '𝟖' => '8', '𝟗' => '9', '𝟘' => '0', '𝟙' => '1', '𝟚' => '2', '𝟛' => '3', '𝟜' => '4', '𝟝' => '5', '𝟞' => '6', '𝟟' => '7', '𝟠' => '8', '𝟡' => '9', '𝟢' => '0', '𝟣' => '1', '𝟤' => '2', '𝟥' => '3', '𝟦' => '4', '𝟧' => '5', '𝟨' => '6', '𝟩' => '7', '𝟪' => '8', '𝟫' => '9', '𝟬' => '0', '𝟭' => '1', '𝟮' => '2', '𝟯' => '3', '𝟰' => '4', '𝟱' => '5', '𝟲' => '6', '𝟳' => '7', '𝟴' => '8', '𝟵' => '9', '𝟶' => '0', '𝟷' => '1', '𝟸' => '2', '𝟹' => '3', '𝟺' => '4', '𝟻' => '5', '𝟼' => '6', '𝟽' => '7', '𝟾' => '8', '𝟿' => '9', '𞸀' => 'ا', '𞸁' => 'ب', '𞸂' => 'ج', '𞸃' => 'د', '𞸅' => 'و', '𞸆' => 'ز', '𞸇' => 'ح', '𞸈' => 'ط', '𞸉' => 'ي', '𞸊' => 'ك', '𞸋' => 'ل', '𞸌' => 'م', '𞸍' => 'ن', '𞸎' => 'س', '𞸏' => 'ع', '𞸐' => 'ف', '𞸑' => 'ص', '𞸒' => 'ق', '𞸓' => 'ر', '𞸔' => 'ش', '𞸕' => 'ت', '𞸖' => 'ث', '𞸗' => 'خ', '𞸘' => 'ذ', '𞸙' => 'ض', '𞸚' => 'ظ', '𞸛' => 'غ', '𞸜' => 'ٮ', '𞸝' => 'ں', '𞸞' => 'ڡ', '𞸟' => 'ٯ', '𞸡' => 'ب', '𞸢' => 'ج', '𞸤' => 'ه', '𞸧' => 'ح', '𞸩' => 'ي', '𞸪' => 'ك', '𞸫' => 'ل', '𞸬' => 'م', '𞸭' => 'ن', '𞸮' => 'س', '𞸯' => 'ع', '𞸰' => 'ف', '𞸱' => 'ص', '𞸲' => 'ق', '𞸴' => 'ش', '𞸵' => 'ت', '𞸶' => 'ث', '𞸷' => 'خ', '𞸹' => 'ض', '𞸻' => 'غ', '𞹂' => 'ج', '𞹇' => 'ح', '𞹉' => 'ي', '𞹋' => 'ل', '𞹍' => 'ن', '𞹎' => 'س', '𞹏' => 'ع', '𞹑' => 'ص', '𞹒' => 'ق', '𞹔' => 'ش', '𞹗' => 'خ', '𞹙' => 'ض', '𞹛' => 'غ', '𞹝' => 'ں', '𞹟' => 'ٯ', '𞹡' => 'ب', '𞹢' => 'ج', '𞹤' => 'ه', '𞹧' => 'ح', '𞹨' => 'ط', '𞹩' => 'ي', '𞹪' => 'ك', '𞹬' => 'م', '𞹭' => 'ن', '𞹮' => 'س', '𞹯' => 'ع', '𞹰' => 'ف', '𞹱' => 'ص', '𞹲' => 'ق', '𞹴' => 'ش', '𞹵' => 'ت', '𞹶' => 'ث', '𞹷' => 'خ', '𞹹' => 'ض', '𞹺' => 'ظ', '𞹻' => 'غ', '𞹼' => 'ٮ', '𞹾' => 'ڡ', '𞺀' => 'ا', '𞺁' => 'ب', '𞺂' => 'ج', '𞺃' => 'د', '𞺄' => 'ه', '𞺅' => 'و', '𞺆' => 'ز', '𞺇' => 'ح', '𞺈' => 'ط', '𞺉' => 'ي', '𞺋' => 'ل', '𞺌' => 'م', '𞺍' => 'ن', '𞺎' => 'س', '𞺏' => 'ع', '𞺐' => 'ف', '𞺑' => 'ص', '𞺒' => 'ق', '𞺓' => 'ر', '𞺔' => 'ش', '𞺕' => 'ت', '𞺖' => 'ث', '𞺗' => 'خ', '𞺘' => 'ذ', '𞺙' => 'ض', '𞺚' => 'ظ', '𞺛' => 'غ', '𞺡' => 'ب', '𞺢' => 'ج', '𞺣' => 'د', '𞺥' => 'و', '𞺦' => 'ز', '𞺧' => 'ح', '𞺨' => 'ط', '𞺩' => 'ي', '𞺫' => 'ل', '𞺬' => 'م', '𞺭' => 'ن', '𞺮' => 'س', '𞺯' => 'ع', '𞺰' => 'ف', '𞺱' => 'ص', '𞺲' => 'ق', '𞺳' => 'ر', '𞺴' => 'ش', '𞺵' => 'ت', '𞺶' => 'ث', '𞺷' => 'خ', '𞺸' => 'ذ', '𞺹' => 'ض', '𞺺' => 'ظ', '𞺻' => 'غ', '🄀' => '0.', '🄁' => '0,', '🄂' => '1,', '🄃' => '2,', '🄄' => '3,', '🄅' => '4,', '🄆' => '5,', '🄇' => '6,', '🄈' => '7,', '🄉' => '8,', '🄊' => '9,', '🄐' => '(A)', '🄑' => '(B)', '🄒' => '(C)', '🄓' => '(D)', '🄔' => '(E)', '🄕' => '(F)', '🄖' => '(G)', '🄗' => '(H)', '🄘' => '(I)', '🄙' => '(J)', '🄚' => '(K)', '🄛' => '(L)', '🄜' => '(M)', '🄝' => '(N)', '🄞' => '(O)', '🄟' => '(P)', '🄠' => '(Q)', '🄡' => '(R)', '🄢' => '(S)', '🄣' => '(T)', '🄤' => '(U)', '🄥' => '(V)', '🄦' => '(W)', '🄧' => '(X)', '🄨' => '(Y)', '🄩' => '(Z)', '🄪' => '〔S〕', '🄫' => 'C', '🄬' => 'R', '🄭' => 'CD', '🄮' => 'WZ', '🄰' => 'A', '🄱' => 'B', '🄲' => 'C', '🄳' => 'D', '🄴' => 'E', '🄵' => 'F', '🄶' => 'G', '🄷' => 'H', '🄸' => 'I', '🄹' => 'J', '🄺' => 'K', '🄻' => 'L', '🄼' => 'M', '🄽' => 'N', '🄾' => 'O', '🄿' => 'P', '🅀' => 'Q', '🅁' => 'R', '🅂' => 'S', '🅃' => 'T', '🅄' => 'U', '🅅' => 'V', '🅆' => 'W', '🅇' => 'X', '🅈' => 'Y', '🅉' => 'Z', '🅊' => 'HV', '🅋' => 'MV', '🅌' => 'SD', '🅍' => 'SS', '🅎' => 'PPV', '🅏' => 'WC', '🅪' => 'MC', '🅫' => 'MD', '🅬' => 'MR', '🆐' => 'DJ', '🈀' => 'ほか', '🈁' => 'ココ', '🈂' => 'サ', '🈐' => '手', '🈑' => '字', '🈒' => '双', '🈓' => 'デ', '🈔' => '二', '🈕' => '多', '🈖' => '解', '🈗' => '天', '🈘' => '交', '🈙' => '映', '🈚' => '無', '🈛' => '料', '🈜' => '前', '🈝' => '後', '🈞' => '再', '🈟' => '新', '🈠' => '初', '🈡' => '終', '🈢' => '生', '🈣' => '販', '🈤' => '声', '🈥' => '吹', '🈦' => '演', '🈧' => '投', '🈨' => '捕', '🈩' => '一', '🈪' => '三', '🈫' => '遊', '🈬' => '左', '🈭' => '中', '🈮' => '右', '🈯' => '指', '🈰' => '走', '🈱' => '打', '🈲' => '禁', '🈳' => '空', '🈴' => '合', '🈵' => '満', '🈶' => '有', '🈷' => '月', '🈸' => '申', '🈹' => '割', '🈺' => '営', '🈻' => '配', '🉀' => '〔本〕', '🉁' => '〔三〕', '🉂' => '〔二〕', '🉃' => '〔安〕', '🉄' => '〔点〕', '🉅' => '〔打〕', '🉆' => '〔盗〕', '🉇' => '〔勝〕', '🉈' => '〔敗〕', '🉐' => '得', '🉑' => '可', '🯰' => '0', '🯱' => '1', '🯲' => '2', '🯳' => '3', '🯴' => '4', '🯵' => '5', '🯶' => '6', '🯷' => '7', '🯸' => '8', '🯹' => '9', ); polyfill-intl-normalizer/Resources/unidata/combiningClass.php 0000644 00000032504 15025017654 0020576 0 ustar 00 <?php return array ( '̀' => 230, '́' => 230, '̂' => 230, '̃' => 230, '̄' => 230, '̅' => 230, '̆' => 230, '̇' => 230, '̈' => 230, '̉' => 230, '̊' => 230, '̋' => 230, '̌' => 230, '̍' => 230, '̎' => 230, '̏' => 230, '̐' => 230, '̑' => 230, '̒' => 230, '̓' => 230, '̔' => 230, '̕' => 232, '̖' => 220, '̗' => 220, '̘' => 220, '̙' => 220, '̚' => 232, '̛' => 216, '̜' => 220, '̝' => 220, '̞' => 220, '̟' => 220, '̠' => 220, '̡' => 202, '̢' => 202, '̣' => 220, '̤' => 220, '̥' => 220, '̦' => 220, '̧' => 202, '̨' => 202, '̩' => 220, '̪' => 220, '̫' => 220, '̬' => 220, '̭' => 220, '̮' => 220, '̯' => 220, '̰' => 220, '̱' => 220, '̲' => 220, '̳' => 220, '̴' => 1, '̵' => 1, '̶' => 1, '̷' => 1, '̸' => 1, '̹' => 220, '̺' => 220, '̻' => 220, '̼' => 220, '̽' => 230, '̾' => 230, '̿' => 230, '̀' => 230, '́' => 230, '͂' => 230, '̓' => 230, '̈́' => 230, 'ͅ' => 240, '͆' => 230, '͇' => 220, '͈' => 220, '͉' => 220, '͊' => 230, '͋' => 230, '͌' => 230, '͍' => 220, '͎' => 220, '͐' => 230, '͑' => 230, '͒' => 230, '͓' => 220, '͔' => 220, '͕' => 220, '͖' => 220, '͗' => 230, '͘' => 232, '͙' => 220, '͚' => 220, '͛' => 230, '͜' => 233, '͝' => 234, '͞' => 234, '͟' => 233, '͠' => 234, '͡' => 234, '͢' => 233, 'ͣ' => 230, 'ͤ' => 230, 'ͥ' => 230, 'ͦ' => 230, 'ͧ' => 230, 'ͨ' => 230, 'ͩ' => 230, 'ͪ' => 230, 'ͫ' => 230, 'ͬ' => 230, 'ͭ' => 230, 'ͮ' => 230, 'ͯ' => 230, '҃' => 230, '҄' => 230, '҅' => 230, '҆' => 230, '҇' => 230, '֑' => 220, '֒' => 230, '֓' => 230, '֔' => 230, '֕' => 230, '֖' => 220, '֗' => 230, '֘' => 230, '֙' => 230, '֚' => 222, '֛' => 220, '֜' => 230, '֝' => 230, '֞' => 230, '֟' => 230, '֠' => 230, '֡' => 230, '֢' => 220, '֣' => 220, '֤' => 220, '֥' => 220, '֦' => 220, '֧' => 220, '֨' => 230, '֩' => 230, '֪' => 220, '֫' => 230, '֬' => 230, '֭' => 222, '֮' => 228, '֯' => 230, 'ְ' => 10, 'ֱ' => 11, 'ֲ' => 12, 'ֳ' => 13, 'ִ' => 14, 'ֵ' => 15, 'ֶ' => 16, 'ַ' => 17, 'ָ' => 18, 'ֹ' => 19, 'ֺ' => 19, 'ֻ' => 20, 'ּ' => 21, 'ֽ' => 22, 'ֿ' => 23, 'ׁ' => 24, 'ׂ' => 25, 'ׄ' => 230, 'ׅ' => 220, 'ׇ' => 18, 'ؐ' => 230, 'ؑ' => 230, 'ؒ' => 230, 'ؓ' => 230, 'ؔ' => 230, 'ؕ' => 230, 'ؖ' => 230, 'ؗ' => 230, 'ؘ' => 30, 'ؙ' => 31, 'ؚ' => 32, 'ً' => 27, 'ٌ' => 28, 'ٍ' => 29, 'َ' => 30, 'ُ' => 31, 'ِ' => 32, 'ّ' => 33, 'ْ' => 34, 'ٓ' => 230, 'ٔ' => 230, 'ٕ' => 220, 'ٖ' => 220, 'ٗ' => 230, '٘' => 230, 'ٙ' => 230, 'ٚ' => 230, 'ٛ' => 230, 'ٜ' => 220, 'ٝ' => 230, 'ٞ' => 230, 'ٟ' => 220, 'ٰ' => 35, 'ۖ' => 230, 'ۗ' => 230, 'ۘ' => 230, 'ۙ' => 230, 'ۚ' => 230, 'ۛ' => 230, 'ۜ' => 230, '۟' => 230, '۠' => 230, 'ۡ' => 230, 'ۢ' => 230, 'ۣ' => 220, 'ۤ' => 230, 'ۧ' => 230, 'ۨ' => 230, '۪' => 220, '۫' => 230, '۬' => 230, 'ۭ' => 220, 'ܑ' => 36, 'ܰ' => 230, 'ܱ' => 220, 'ܲ' => 230, 'ܳ' => 230, 'ܴ' => 220, 'ܵ' => 230, 'ܶ' => 230, 'ܷ' => 220, 'ܸ' => 220, 'ܹ' => 220, 'ܺ' => 230, 'ܻ' => 220, 'ܼ' => 220, 'ܽ' => 230, 'ܾ' => 220, 'ܿ' => 230, '݀' => 230, '݁' => 230, '݂' => 220, '݃' => 230, '݄' => 220, '݅' => 230, '݆' => 220, '݇' => 230, '݈' => 220, '݉' => 230, '݊' => 230, '߫' => 230, '߬' => 230, '߭' => 230, '߮' => 230, '߯' => 230, '߰' => 230, '߱' => 230, '߲' => 220, '߳' => 230, '߽' => 220, 'ࠖ' => 230, 'ࠗ' => 230, '࠘' => 230, '࠙' => 230, 'ࠛ' => 230, 'ࠜ' => 230, 'ࠝ' => 230, 'ࠞ' => 230, 'ࠟ' => 230, 'ࠠ' => 230, 'ࠡ' => 230, 'ࠢ' => 230, 'ࠣ' => 230, 'ࠥ' => 230, 'ࠦ' => 230, 'ࠧ' => 230, 'ࠩ' => 230, 'ࠪ' => 230, 'ࠫ' => 230, 'ࠬ' => 230, '࠭' => 230, '࡙' => 220, '࡚' => 220, '࡛' => 220, '࣓' => 220, 'ࣔ' => 230, 'ࣕ' => 230, 'ࣖ' => 230, 'ࣗ' => 230, 'ࣘ' => 230, 'ࣙ' => 230, 'ࣚ' => 230, 'ࣛ' => 230, 'ࣜ' => 230, 'ࣝ' => 230, 'ࣞ' => 230, 'ࣟ' => 230, '࣠' => 230, '࣡' => 230, 'ࣣ' => 220, 'ࣤ' => 230, 'ࣥ' => 230, 'ࣦ' => 220, 'ࣧ' => 230, 'ࣨ' => 230, 'ࣩ' => 220, '࣪' => 230, '࣫' => 230, '࣬' => 230, '࣭' => 220, '࣮' => 220, '࣯' => 220, 'ࣰ' => 27, 'ࣱ' => 28, 'ࣲ' => 29, 'ࣳ' => 230, 'ࣴ' => 230, 'ࣵ' => 230, 'ࣶ' => 220, 'ࣷ' => 230, 'ࣸ' => 230, 'ࣹ' => 220, 'ࣺ' => 220, 'ࣻ' => 230, 'ࣼ' => 230, 'ࣽ' => 230, 'ࣾ' => 230, 'ࣿ' => 230, '़' => 7, '्' => 9, '॑' => 230, '॒' => 220, '॓' => 230, '॔' => 230, '়' => 7, '্' => 9, '৾' => 230, '਼' => 7, '੍' => 9, '઼' => 7, '્' => 9, '଼' => 7, '୍' => 9, '்' => 9, '్' => 9, 'ౕ' => 84, 'ౖ' => 91, '಼' => 7, '್' => 9, '഻' => 9, '഼' => 9, '്' => 9, '්' => 9, 'ุ' => 103, 'ู' => 103, 'ฺ' => 9, '่' => 107, '้' => 107, '๊' => 107, '๋' => 107, 'ຸ' => 118, 'ູ' => 118, '຺' => 9, '່' => 122, '້' => 122, '໊' => 122, '໋' => 122, '༘' => 220, '༙' => 220, '༵' => 220, '༷' => 220, '༹' => 216, 'ཱ' => 129, 'ི' => 130, 'ུ' => 132, 'ེ' => 130, 'ཻ' => 130, 'ོ' => 130, 'ཽ' => 130, 'ྀ' => 130, 'ྂ' => 230, 'ྃ' => 230, '྄' => 9, '྆' => 230, '྇' => 230, '࿆' => 220, '့' => 7, '္' => 9, '်' => 9, 'ႍ' => 220, '፝' => 230, '፞' => 230, '፟' => 230, '᜔' => 9, '᜴' => 9, '្' => 9, '៝' => 230, 'ᢩ' => 228, '᤹' => 222, '᤺' => 230, '᤻' => 220, 'ᨗ' => 230, 'ᨘ' => 220, '᩠' => 9, '᩵' => 230, '᩶' => 230, '᩷' => 230, '᩸' => 230, '᩹' => 230, '᩺' => 230, '᩻' => 230, '᩼' => 230, '᩿' => 220, '᪰' => 230, '᪱' => 230, '᪲' => 230, '᪳' => 230, '᪴' => 230, '᪵' => 220, '᪶' => 220, '᪷' => 220, '᪸' => 220, '᪹' => 220, '᪺' => 220, '᪻' => 230, '᪼' => 230, '᪽' => 220, 'ᪿ' => 220, 'ᫀ' => 220, '᬴' => 7, '᭄' => 9, '᭫' => 230, '᭬' => 220, '᭭' => 230, '᭮' => 230, '᭯' => 230, '᭰' => 230, '᭱' => 230, '᭲' => 230, '᭳' => 230, '᮪' => 9, '᮫' => 9, '᯦' => 7, '᯲' => 9, '᯳' => 9, '᰷' => 7, '᳐' => 230, '᳑' => 230, '᳒' => 230, '᳔' => 1, '᳕' => 220, '᳖' => 220, '᳗' => 220, '᳘' => 220, '᳙' => 220, '᳚' => 230, '᳛' => 230, '᳜' => 220, '᳝' => 220, '᳞' => 220, '᳟' => 220, '᳠' => 230, '᳢' => 1, '᳣' => 1, '᳤' => 1, '᳥' => 1, '᳦' => 1, '᳧' => 1, '᳨' => 1, '᳭' => 220, '᳴' => 230, '᳸' => 230, '᳹' => 230, '᷀' => 230, '᷁' => 230, '᷂' => 220, '᷃' => 230, '᷄' => 230, '᷅' => 230, '᷆' => 230, '᷇' => 230, '᷈' => 230, '᷉' => 230, '᷊' => 220, '᷋' => 230, '᷌' => 230, '᷍' => 234, '᷎' => 214, '᷏' => 220, '᷐' => 202, '᷑' => 230, '᷒' => 230, 'ᷓ' => 230, 'ᷔ' => 230, 'ᷕ' => 230, 'ᷖ' => 230, 'ᷗ' => 230, 'ᷘ' => 230, 'ᷙ' => 230, 'ᷚ' => 230, 'ᷛ' => 230, 'ᷜ' => 230, 'ᷝ' => 230, 'ᷞ' => 230, 'ᷟ' => 230, 'ᷠ' => 230, 'ᷡ' => 230, 'ᷢ' => 230, 'ᷣ' => 230, 'ᷤ' => 230, 'ᷥ' => 230, 'ᷦ' => 230, 'ᷧ' => 230, 'ᷨ' => 230, 'ᷩ' => 230, 'ᷪ' => 230, 'ᷫ' => 230, 'ᷬ' => 230, 'ᷭ' => 230, 'ᷮ' => 230, 'ᷯ' => 230, 'ᷰ' => 230, 'ᷱ' => 230, 'ᷲ' => 230, 'ᷳ' => 230, 'ᷴ' => 230, '᷵' => 230, '᷶' => 232, '᷷' => 228, '᷸' => 228, '᷹' => 220, '᷻' => 230, '᷼' => 233, '᷽' => 220, '᷾' => 230, '᷿' => 220, '⃐' => 230, '⃑' => 230, '⃒' => 1, '⃓' => 1, '⃔' => 230, '⃕' => 230, '⃖' => 230, '⃗' => 230, '⃘' => 1, '⃙' => 1, '⃚' => 1, '⃛' => 230, '⃜' => 230, '⃡' => 230, '⃥' => 1, '⃦' => 1, '⃧' => 230, '⃨' => 220, '⃩' => 230, '⃪' => 1, '⃫' => 1, '⃬' => 220, '⃭' => 220, '⃮' => 220, '⃯' => 220, '⃰' => 230, '⳯' => 230, '⳰' => 230, '⳱' => 230, '⵿' => 9, 'ⷠ' => 230, 'ⷡ' => 230, 'ⷢ' => 230, 'ⷣ' => 230, 'ⷤ' => 230, 'ⷥ' => 230, 'ⷦ' => 230, 'ⷧ' => 230, 'ⷨ' => 230, 'ⷩ' => 230, 'ⷪ' => 230, 'ⷫ' => 230, 'ⷬ' => 230, 'ⷭ' => 230, 'ⷮ' => 230, 'ⷯ' => 230, 'ⷰ' => 230, 'ⷱ' => 230, 'ⷲ' => 230, 'ⷳ' => 230, 'ⷴ' => 230, 'ⷵ' => 230, 'ⷶ' => 230, 'ⷷ' => 230, 'ⷸ' => 230, 'ⷹ' => 230, 'ⷺ' => 230, 'ⷻ' => 230, 'ⷼ' => 230, 'ⷽ' => 230, 'ⷾ' => 230, 'ⷿ' => 230, '〪' => 218, '〫' => 228, '〬' => 232, '〭' => 222, '〮' => 224, '〯' => 224, '゙' => 8, '゚' => 8, '꙯' => 230, 'ꙴ' => 230, 'ꙵ' => 230, 'ꙶ' => 230, 'ꙷ' => 230, 'ꙸ' => 230, 'ꙹ' => 230, 'ꙺ' => 230, 'ꙻ' => 230, '꙼' => 230, '꙽' => 230, 'ꚞ' => 230, 'ꚟ' => 230, '꛰' => 230, '꛱' => 230, '꠆' => 9, '꠬' => 9, '꣄' => 9, '꣠' => 230, '꣡' => 230, '꣢' => 230, '꣣' => 230, '꣤' => 230, '꣥' => 230, '꣦' => 230, '꣧' => 230, '꣨' => 230, '꣩' => 230, '꣪' => 230, '꣫' => 230, '꣬' => 230, '꣭' => 230, '꣮' => 230, '꣯' => 230, '꣰' => 230, '꣱' => 230, '꤫' => 220, '꤬' => 220, '꤭' => 220, '꥓' => 9, '꦳' => 7, '꧀' => 9, 'ꪰ' => 230, 'ꪲ' => 230, 'ꪳ' => 230, 'ꪴ' => 220, 'ꪷ' => 230, 'ꪸ' => 230, 'ꪾ' => 230, '꪿' => 230, '꫁' => 230, '꫶' => 9, '꯭' => 9, 'ﬞ' => 26, '︠' => 230, '︡' => 230, '︢' => 230, '︣' => 230, '︤' => 230, '︥' => 230, '︦' => 230, '︧' => 220, '︨' => 220, '︩' => 220, '︪' => 220, '︫' => 220, '︬' => 220, '︭' => 220, '︮' => 230, '︯' => 230, '𐇽' => 220, '𐋠' => 220, '𐍶' => 230, '𐍷' => 230, '𐍸' => 230, '𐍹' => 230, '𐍺' => 230, '𐨍' => 220, '𐨏' => 230, '𐨸' => 230, '𐨹' => 1, '𐨺' => 220, '𐨿' => 9, '𐫥' => 230, '𐫦' => 220, '𐴤' => 230, '𐴥' => 230, '𐴦' => 230, '𐴧' => 230, '𐺫' => 230, '𐺬' => 230, '𐽆' => 220, '𐽇' => 220, '𐽈' => 230, '𐽉' => 230, '𐽊' => 230, '𐽋' => 220, '𐽌' => 230, '𐽍' => 220, '𐽎' => 220, '𐽏' => 220, '𐽐' => 220, '𑁆' => 9, '𑁿' => 9, '𑂹' => 9, '𑂺' => 7, '𑄀' => 230, '𑄁' => 230, '𑄂' => 230, '𑄳' => 9, '𑄴' => 9, '𑅳' => 7, '𑇀' => 9, '𑇊' => 7, '𑈵' => 9, '𑈶' => 7, '𑋩' => 7, '𑋪' => 9, '𑌻' => 7, '𑌼' => 7, '𑍍' => 9, '𑍦' => 230, '𑍧' => 230, '𑍨' => 230, '𑍩' => 230, '𑍪' => 230, '𑍫' => 230, '𑍬' => 230, '𑍰' => 230, '𑍱' => 230, '𑍲' => 230, '𑍳' => 230, '𑍴' => 230, '𑑂' => 9, '𑑆' => 7, '𑑞' => 230, '𑓂' => 9, '𑓃' => 7, '𑖿' => 9, '𑗀' => 7, '𑘿' => 9, '𑚶' => 9, '𑚷' => 7, '𑜫' => 9, '𑠹' => 9, '𑠺' => 7, '𑤽' => 9, '𑤾' => 9, '𑥃' => 7, '𑧠' => 9, '𑨴' => 9, '𑩇' => 9, '𑪙' => 9, '𑰿' => 9, '𑵂' => 7, '𑵄' => 9, '𑵅' => 9, '𑶗' => 9, '𖫰' => 1, '𖫱' => 1, '𖫲' => 1, '𖫳' => 1, '𖫴' => 1, '𖬰' => 230, '𖬱' => 230, '𖬲' => 230, '𖬳' => 230, '𖬴' => 230, '𖬵' => 230, '𖬶' => 230, '𖿰' => 6, '𖿱' => 6, '𛲞' => 1, '𝅥' => 216, '𝅦' => 216, '𝅧' => 1, '𝅨' => 1, '𝅩' => 1, '𝅭' => 226, '𝅮' => 216, '𝅯' => 216, '𝅰' => 216, '𝅱' => 216, '𝅲' => 216, '𝅻' => 220, '𝅼' => 220, '𝅽' => 220, '𝅾' => 220, '𝅿' => 220, '𝆀' => 220, '𝆁' => 220, '𝆂' => 220, '𝆅' => 230, '𝆆' => 230, '𝆇' => 230, '𝆈' => 230, '𝆉' => 230, '𝆊' => 220, '𝆋' => 220, '𝆪' => 230, '𝆫' => 230, '𝆬' => 230, '𝆭' => 230, '𝉂' => 230, '𝉃' => 230, '𝉄' => 230, '𞀀' => 230, '𞀁' => 230, '𞀂' => 230, '𞀃' => 230, '𞀄' => 230, '𞀅' => 230, '𞀆' => 230, '𞀈' => 230, '𞀉' => 230, '𞀊' => 230, '𞀋' => 230, '𞀌' => 230, '𞀍' => 230, '𞀎' => 230, '𞀏' => 230, '𞀐' => 230, '𞀑' => 230, '𞀒' => 230, '𞀓' => 230, '𞀔' => 230, '𞀕' => 230, '𞀖' => 230, '𞀗' => 230, '𞀘' => 230, '𞀛' => 230, '𞀜' => 230, '𞀝' => 230, '𞀞' => 230, '𞀟' => 230, '𞀠' => 230, '𞀡' => 230, '𞀣' => 230, '𞀤' => 230, '𞀦' => 230, '𞀧' => 230, '𞀨' => 230, '𞀩' => 230, '𞀪' => 230, '𞄰' => 230, '𞄱' => 230, '𞄲' => 230, '𞄳' => 230, '𞄴' => 230, '𞄵' => 230, '𞄶' => 230, '𞋬' => 230, '𞋭' => 230, '𞋮' => 230, '𞋯' => 230, '𞣐' => 220, '𞣑' => 220, '𞣒' => 220, '𞣓' => 220, '𞣔' => 220, '𞣕' => 220, '𞣖' => 220, '𞥄' => 230, '𞥅' => 230, '𞥆' => 230, '𞥇' => 230, '𞥈' => 230, '𞥉' => 230, '𞥊' => 7, ); polyfill-intl-normalizer/Resources/stubs/Normalizer.php 0000644 00000000624 15025017654 0017476 0 ustar 00 <?php class Normalizer extends Symfony\Polyfill\Intl\Normalizer\Normalizer { /** * @deprecated since ICU 56 and removed in PHP 8 */ public const NONE = 2; public const FORM_D = 4; public const FORM_KD = 8; public const FORM_C = 16; public const FORM_KC = 32; public const NFD = 4; public const NFKD = 8; public const NFC = 16; public const NFKC = 32; } polyfill-intl-grapheme/bootstrap.php 0000644 00000004345 15025017654 0013671 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Intl\Grapheme as p; if (extension_loaded('intl')) { return; } if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; } if (!defined('GRAPHEME_EXTR_COUNT')) { define('GRAPHEME_EXTR_COUNT', 0); } if (!defined('GRAPHEME_EXTR_MAXBYTES')) { define('GRAPHEME_EXTR_MAXBYTES', 1); } if (!defined('GRAPHEME_EXTR_MAXCHARS')) { define('GRAPHEME_EXTR_MAXCHARS', 2); } if (!function_exists('grapheme_extract')) { function grapheme_extract($haystack, $size, $type = 0, $start = 0, &$next = 0) { return p\Grapheme::grapheme_extract($haystack, $size, $type, $start, $next); } } if (!function_exists('grapheme_stripos')) { function grapheme_stripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_stripos($haystack, $needle, $offset); } } if (!function_exists('grapheme_stristr')) { function grapheme_stristr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_stristr($haystack, $needle, $beforeNeedle); } } if (!function_exists('grapheme_strlen')) { function grapheme_strlen($input) { return p\Grapheme::grapheme_strlen($input); } } if (!function_exists('grapheme_strpos')) { function grapheme_strpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strpos($haystack, $needle, $offset); } } if (!function_exists('grapheme_strripos')) { function grapheme_strripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strripos($haystack, $needle, $offset); } } if (!function_exists('grapheme_strrpos')) { function grapheme_strrpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strrpos($haystack, $needle, $offset); } } if (!function_exists('grapheme_strstr')) { function grapheme_strstr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_strstr($haystack, $needle, $beforeNeedle); } } if (!function_exists('grapheme_substr')) { function grapheme_substr($string, $offset, $length = null) { return p\Grapheme::grapheme_substr($string, $offset, $length); } } polyfill-intl-grapheme/composer.json 0000644 00000002000 15025017654 0013647 0 ustar 00 { "name": "symfony/polyfill-intl-grapheme", "type": "library", "description": "Symfony polyfill for intl's grapheme_* functions", "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "grapheme"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.1" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Intl\\Grapheme\\": "" }, "files": [ "bootstrap.php" ] }, "suggest": { "ext-intl": "For best performance" }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } polyfill-intl-grapheme/Grapheme.php 0000644 00000023070 15025017654 0013400 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Intl\Grapheme; \define('SYMFONY_GRAPHEME_CLUSTER_RX', ((float) \PCRE_VERSION < 10 ? (float) \PCRE_VERSION >= 8.32 : (float) \PCRE_VERSION >= 10.39) ? '\X' : Grapheme::GRAPHEME_CLUSTER_RX); /** * Partial intl implementation in pure PHP. * * Implemented: * - grapheme_extract - Extract a sequence of grapheme clusters from a text buffer, which must be encoded in UTF-8 * - grapheme_stripos - Find position (in grapheme units) of first occurrence of a case-insensitive string * - grapheme_stristr - Returns part of haystack string from the first occurrence of case-insensitive needle to the end of haystack * - grapheme_strlen - Get string length in grapheme units * - grapheme_strpos - Find position (in grapheme units) of first occurrence of a string * - grapheme_strripos - Find position (in grapheme units) of last occurrence of a case-insensitive string * - grapheme_strrpos - Find position (in grapheme units) of last occurrence of a string * - grapheme_strstr - Returns part of haystack string from the first occurrence of needle to the end of haystack * - grapheme_substr - Return part of a string * * @author Nicolas Grekas <p@tchwork.com> * * @internal */ final class Grapheme { // (CRLF|([ZWNJ-ZWJ]|T+|L*(LV?V+|LV|LVT)T*|L+|[^Control])[Extend]*|[Control]) // This regular expression is a work around for http://bugs.exim.org/1279 public const GRAPHEME_CLUSTER_RX = '(?:\r\n|(?:[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-ᅟ]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[ᅠ-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-ᅟ]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{200D}\x{1D165}\x{1D16E}-\x{1D172}]*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])'; private const CASE_FOLD = [ ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], ]; public static function grapheme_extract($s, $size, $type = \GRAPHEME_EXTR_COUNT, $start = 0, &$next = 0) { if (0 > $start) { $start = \strlen($s) + $start; } if (!\is_scalar($s)) { $hasError = false; set_error_handler(function () use (&$hasError) { $hasError = true; }); $next = substr($s, $start); restore_error_handler(); if ($hasError) { substr($s, $start); $s = ''; } else { $s = $next; } } else { $s = substr($s, $start); } $size = (int) $size; $type = (int) $type; $start = (int) $start; if (\GRAPHEME_EXTR_COUNT !== $type && \GRAPHEME_EXTR_MAXBYTES !== $type && \GRAPHEME_EXTR_MAXCHARS !== $type) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('grapheme_extract(): Argument #3 ($type) must be one of GRAPHEME_EXTR_COUNT, GRAPHEME_EXTR_MAXBYTES, or GRAPHEME_EXTR_MAXCHARS'); } if (!isset($s[0]) || 0 > $size || 0 > $start) { return false; } if (0 === $size) { return ''; } $next = $start; $s = preg_split('/('.SYMFONY_GRAPHEME_CLUSTER_RX.')/u', "\r\n".$s, $size + 1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); if (!isset($s[1])) { return false; } $i = 1; $ret = ''; do { if (\GRAPHEME_EXTR_COUNT === $type) { --$size; } elseif (\GRAPHEME_EXTR_MAXBYTES === $type) { $size -= \strlen($s[$i]); } else { $size -= iconv_strlen($s[$i], 'UTF-8//IGNORE'); } if ($size >= 0) { $ret .= $s[$i]; } } while (isset($s[++$i]) && $size > 0); $next += \strlen($ret); return $ret; } public static function grapheme_strlen($s) { preg_replace('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', '', $s, -1, $len); return 0 === $len && '' !== $s ? null : $len; } public static function grapheme_substr($s, $start, $len = null) { if (null === $len) { $len = 2147483647; } preg_match_all('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', $s, $s); $slen = \count($s[0]); $start = (int) $start; if (0 > $start) { $start += $slen; } if (0 > $start) { if (\PHP_VERSION_ID < 80000) { return false; } $start = 0; } if ($start >= $slen) { return \PHP_VERSION_ID >= 80000 ? '' : false; } $rem = $slen - $start; if (0 > $len) { $len += $rem; } if (0 === $len) { return ''; } if (0 > $len) { return \PHP_VERSION_ID >= 80000 ? '' : false; } if ($len > $rem) { $len = $rem; } return implode('', \array_slice($s[0], $start, $len)); } public static function grapheme_strpos($s, $needle, $offset = 0) { return self::grapheme_position($s, $needle, $offset, 0); } public static function grapheme_stripos($s, $needle, $offset = 0) { return self::grapheme_position($s, $needle, $offset, 1); } public static function grapheme_strrpos($s, $needle, $offset = 0) { return self::grapheme_position($s, $needle, $offset, 2); } public static function grapheme_strripos($s, $needle, $offset = 0) { return self::grapheme_position($s, $needle, $offset, 3); } public static function grapheme_stristr($s, $needle, $beforeNeedle = false) { return mb_stristr($s, $needle, $beforeNeedle, 'UTF-8'); } public static function grapheme_strstr($s, $needle, $beforeNeedle = false) { return mb_strstr($s, $needle, $beforeNeedle, 'UTF-8'); } private static function grapheme_position($s, $needle, $offset, $mode) { $needle = (string) $needle; if (80000 > \PHP_VERSION_ID && !preg_match('/./us', $needle)) { return false; } $s = (string) $s; if (!preg_match('/./us', $s)) { return false; } if ($offset > 0) { $s = self::grapheme_substr($s, $offset); } elseif ($offset < 0) { if (2 > $mode) { $offset += self::grapheme_strlen($s); $s = self::grapheme_substr($s, $offset); if (0 > $offset) { $offset = 0; } } elseif (0 > $offset += self::grapheme_strlen($needle)) { $s = self::grapheme_substr($s, 0, $offset); $offset = 0; } else { $offset = 0; } } // As UTF-8 is self-synchronizing, and we have ensured the strings are valid UTF-8, // we can use normal binary string functions here. For case-insensitive searches, // case fold the strings first. $caseInsensitive = $mode & 1; $reverse = $mode & 2; if ($caseInsensitive) { // Use the same case folding mode as mbstring does for mb_stripos(). // Stick to SIMPLE case folding to avoid changing the length of the string, which // might result in offsets being shifted. $mode = \defined('MB_CASE_FOLD_SIMPLE') ? \MB_CASE_FOLD_SIMPLE : \MB_CASE_LOWER; $s = mb_convert_case($s, $mode, 'UTF-8'); $needle = mb_convert_case($needle, $mode, 'UTF-8'); if (!\defined('MB_CASE_FOLD_SIMPLE')) { $s = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $s); $needle = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $needle); } } if ($reverse) { $needlePos = strrpos($s, $needle); } else { $needlePos = strpos($s, $needle); } return false !== $needlePos ? self::grapheme_strlen(substr($s, 0, $needlePos)) + $offset : false; } } polyfill-intl-grapheme/README.md 0000644 00000003113 15025017654 0012412 0 ustar 00 Symfony Polyfill / Intl: Grapheme ================================= This component provides a partial, native PHP implementation of the [Grapheme functions](https://php.net/intl.grapheme) from the [Intl](https://php.net/intl) extension. - [`grapheme_extract`](https://php.net/grapheme_extract): Extract a sequence of grapheme clusters from a text buffer, which must be encoded in UTF-8 - [`grapheme_stripos`](https://php.net/grapheme_stripos): Find position (in grapheme units) of first occurrence of a case-insensitive string - [`grapheme_stristr`](https://php.net/grapheme_stristr): Returns part of haystack string from the first occurrence of case-insensitive needle to the end of haystack - [`grapheme_strlen`](https://php.net/grapheme_strlen): Get string length in grapheme units - [`grapheme_strpos`](https://php.net/grapheme_strpos): Find position (in grapheme units) of first occurrence of a string - [`grapheme_strripos`](https://php.net/grapheme_strripos): Find position (in grapheme units) of last occurrence of a case-insensitive string - [`grapheme_strrpos`](https://php.net/grapheme_strrpos): Find position (in grapheme units) of last occurrence of a string - [`grapheme_strstr`](https://php.net/grapheme_strstr): Returns part of haystack string from the first occurrence of needle to the end of haystack - [`grapheme_substr`](https://php.net/grapheme_substr): Return part of a string More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). polyfill-intl-grapheme/LICENSE 0000644 00000002051 15025017654 0012140 0 ustar 00 Copyright (c) 2015-2019 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. polyfill-intl-grapheme/bootstrap80.php 0000644 00000005147 15025017654 0014042 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Intl\Grapheme as p; if (!defined('GRAPHEME_EXTR_COUNT')) { define('GRAPHEME_EXTR_COUNT', 0); } if (!defined('GRAPHEME_EXTR_MAXBYTES')) { define('GRAPHEME_EXTR_MAXBYTES', 1); } if (!defined('GRAPHEME_EXTR_MAXCHARS')) { define('GRAPHEME_EXTR_MAXCHARS', 2); } if (!function_exists('grapheme_extract')) { function grapheme_extract(?string $haystack, ?int $size, ?int $type = GRAPHEME_EXTR_COUNT, ?int $offset = 0, &$next = null): string|false { return p\Grapheme::grapheme_extract((string) $haystack, (int) $size, (int) $type, (int) $offset, $next); } } if (!function_exists('grapheme_stripos')) { function grapheme_stripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_stripos((string) $haystack, (string) $needle, (int) $offset); } } if (!function_exists('grapheme_stristr')) { function grapheme_stristr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_stristr((string) $haystack, (string) $needle, (bool) $beforeNeedle); } } if (!function_exists('grapheme_strlen')) { function grapheme_strlen(?string $string): int|false|null { return p\Grapheme::grapheme_strlen((string) $string); } } if (!function_exists('grapheme_strpos')) { function grapheme_strpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strpos((string) $haystack, (string) $needle, (int) $offset); } } if (!function_exists('grapheme_strripos')) { function grapheme_strripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strripos((string) $haystack, (string) $needle, (int) $offset); } } if (!function_exists('grapheme_strrpos')) { function grapheme_strrpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strrpos((string) $haystack, (string) $needle, (int) $offset); } } if (!function_exists('grapheme_strstr')) { function grapheme_strstr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_strstr((string) $haystack, (string) $needle, (bool) $beforeNeedle); } } if (!function_exists('grapheme_substr')) { function grapheme_substr(?string $string, ?int $offset, ?int $length = null): string|false { return p\Grapheme::grapheme_substr((string) $string, (int) $offset, $length); } } error-handler/Internal/TentativeTypes.php 0000644 00000163377 15025017654 0014613 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\Internal; /** * This class has been generated by extract-tentative-return-types.php. * * @internal */ class TentativeTypes { public const RETURN_TYPES = [ 'CURLFile' => [ 'getFilename' => 'string', 'getMimeType' => 'string', 'getPostFilename' => 'string', 'setMimeType' => 'void', 'setPostFilename' => 'void', ], 'DateTimeInterface' => [ 'format' => 'string', 'getTimezone' => 'DateTimeZone|false', 'getOffset' => 'int', 'getTimestamp' => 'int', 'diff' => 'DateInterval', '__wakeup' => 'void', ], 'DateTime' => [ '__wakeup' => 'void', '__set_state' => 'DateTime', 'createFromImmutable' => 'static', 'createFromFormat' => 'DateTime|false', 'getLastErrors' => 'array|false', 'format' => 'string', 'modify' => 'DateTime|false', 'add' => 'DateTime', 'sub' => 'DateTime', 'getTimezone' => 'DateTimeZone|false', 'setTimezone' => 'DateTime', 'getOffset' => 'int', 'setTime' => 'DateTime', 'setDate' => 'DateTime', 'setISODate' => 'DateTime', 'setTimestamp' => 'DateTime', 'getTimestamp' => 'int', 'diff' => 'DateInterval', ], 'DateTimeImmutable' => [ '__wakeup' => 'void', '__set_state' => 'DateTimeImmutable', 'createFromFormat' => 'DateTimeImmutable|false', 'getLastErrors' => 'array|false', 'format' => 'string', 'getTimezone' => 'DateTimeZone|false', 'getOffset' => 'int', 'getTimestamp' => 'int', 'diff' => 'DateInterval', 'modify' => 'DateTimeImmutable|false', 'add' => 'DateTimeImmutable', 'sub' => 'DateTimeImmutable', 'setTimezone' => 'DateTimeImmutable', 'setTime' => 'DateTimeImmutable', 'setDate' => 'DateTimeImmutable', 'setISODate' => 'DateTimeImmutable', 'setTimestamp' => 'DateTimeImmutable', 'createFromMutable' => 'static', ], 'DateTimeZone' => [ 'getName' => 'string', 'getOffset' => 'int', 'getTransitions' => 'array|false', 'getLocation' => 'array|false', 'listAbbreviations' => 'array', 'listIdentifiers' => 'array', '__wakeup' => 'void', '__set_state' => 'DateTimeZone', ], 'DateInterval' => [ 'createFromDateString' => 'DateInterval|false', 'format' => 'string', '__wakeup' => 'void', '__set_state' => 'DateInterval', ], 'DatePeriod' => [ 'getStartDate' => 'DateTimeInterface', 'getEndDate' => '?DateTimeInterface', 'getDateInterval' => 'DateInterval', 'getRecurrences' => '?int', '__wakeup' => 'void', '__set_state' => 'DatePeriod', ], 'DOMNode' => [ 'C14N' => 'string|false', 'C14NFile' => 'int|false', 'getLineNo' => 'int', 'getNodePath' => '?string', 'hasAttributes' => 'bool', 'hasChildNodes' => 'bool', 'isDefaultNamespace' => 'bool', 'isSameNode' => 'bool', 'isSupported' => 'bool', 'lookupNamespaceURI' => '?string', 'lookupPrefix' => '?string', 'normalize' => 'void', ], 'DOMImplementation' => [ 'getFeature' => 'never', 'hasFeature' => 'bool', ], 'DOMDocumentFragment' => [ 'appendXML' => 'bool', ], 'DOMNodeList' => [ 'count' => 'int', ], 'DOMCharacterData' => [ 'appendData' => 'bool', 'insertData' => 'bool', 'deleteData' => 'bool', 'replaceData' => 'bool', ], 'DOMAttr' => [ 'isId' => 'bool', ], 'DOMElement' => [ 'getAttribute' => 'string', 'getAttributeNS' => 'string', 'getElementsByTagName' => 'DOMNodeList', 'getElementsByTagNameNS' => 'DOMNodeList', 'hasAttribute' => 'bool', 'hasAttributeNS' => 'bool', 'removeAttribute' => 'bool', 'removeAttributeNS' => 'void', 'setAttributeNS' => 'void', 'setIdAttribute' => 'void', 'setIdAttributeNS' => 'void', 'setIdAttributeNode' => 'void', ], 'DOMDocument' => [ 'createComment' => 'DOMComment', 'createDocumentFragment' => 'DOMDocumentFragment', 'createTextNode' => 'DOMText', 'getElementById' => '?DOMElement', 'getElementsByTagName' => 'DOMNodeList', 'getElementsByTagNameNS' => 'DOMNodeList', 'normalizeDocument' => 'void', 'registerNodeClass' => 'bool', 'save' => 'int|false', 'saveHTML' => 'string|false', 'saveHTMLFile' => 'int|false', 'saveXML' => 'string|false', 'schemaValidate' => 'bool', 'schemaValidateSource' => 'bool', 'relaxNGValidate' => 'bool', 'relaxNGValidateSource' => 'bool', 'validate' => 'bool', 'xinclude' => 'int|false', ], 'DOMText' => [ 'isWhitespaceInElementContent' => 'bool', 'isElementContentWhitespace' => 'bool', ], 'DOMNamedNodeMap' => [ 'getNamedItem' => '?DOMNode', 'getNamedItemNS' => '?DOMNode', 'item' => '?DOMNode', 'count' => 'int', ], 'DOMXPath' => [ 'evaluate' => 'mixed', 'query' => 'mixed', 'registerNamespace' => 'bool', 'registerPhpFunctions' => 'void', ], 'finfo' => [ 'file' => 'string|false', 'buffer' => 'string|false', ], 'IntlPartsIterator' => [ 'getBreakIterator' => 'IntlBreakIterator', 'getRuleStatus' => 'int', ], 'IntlBreakIterator' => [ 'createCharacterInstance' => '?IntlBreakIterator', 'createCodePointInstance' => 'IntlCodePointBreakIterator', 'createLineInstance' => '?IntlBreakIterator', 'createSentenceInstance' => '?IntlBreakIterator', 'createTitleInstance' => '?IntlBreakIterator', 'createWordInstance' => '?IntlBreakIterator', 'current' => 'int', 'first' => 'int', 'following' => 'int', 'getErrorCode' => 'int', 'getErrorMessage' => 'string', 'getLocale' => 'string|false', 'getPartsIterator' => 'IntlPartsIterator', 'getText' => '?string', 'isBoundary' => 'bool', 'last' => 'int', 'next' => 'int', 'preceding' => 'int', 'previous' => 'int', 'setText' => '?bool', ], 'IntlRuleBasedBreakIterator' => [ 'getBinaryRules' => 'string|false', 'getRules' => 'string|false', 'getRuleStatus' => 'int', 'getRuleStatusVec' => 'array|false', ], 'IntlCodePointBreakIterator' => [ 'getLastCodePoint' => 'int', ], 'IntlCalendar' => [ 'createInstance' => '?IntlCalendar', 'equals' => 'bool', 'fieldDifference' => 'int|false', 'add' => 'bool', 'after' => 'bool', 'before' => 'bool', 'fromDateTime' => '?IntlCalendar', 'get' => 'int|false', 'getActualMaximum' => 'int|false', 'getActualMinimum' => 'int|false', 'getAvailableLocales' => 'array', 'getDayOfWeekType' => 'int|false', 'getErrorCode' => 'int|false', 'getErrorMessage' => 'string|false', 'getFirstDayOfWeek' => 'int|false', 'getGreatestMinimum' => 'int|false', 'getKeywordValuesForLocale' => 'IntlIterator|false', 'getLeastMaximum' => 'int|false', 'getLocale' => 'string|false', 'getMaximum' => 'int|false', 'getMinimalDaysInFirstWeek' => 'int|false', 'getMinimum' => 'int|false', 'getNow' => 'float', 'getRepeatedWallTimeOption' => 'int', 'getSkippedWallTimeOption' => 'int', 'getTime' => 'float|false', 'getTimeZone' => 'IntlTimeZone|false', 'getType' => 'string', 'getWeekendTransition' => 'int|false', 'inDaylightTime' => 'bool', 'isEquivalentTo' => 'bool', 'isLenient' => 'bool', 'isWeekend' => 'bool', 'roll' => 'bool', 'isSet' => 'bool', 'setTime' => 'bool', 'setTimeZone' => 'bool', 'toDateTime' => 'DateTime|false', ], 'IntlGregorianCalendar' => [ 'setGregorianChange' => 'bool', 'getGregorianChange' => 'float', 'isLeapYear' => 'bool', ], 'Collator' => [ 'create' => '?Collator', 'compare' => 'int|false', 'sort' => 'bool', 'sortWithSortKeys' => 'bool', 'asort' => 'bool', 'getAttribute' => 'int|false', 'setAttribute' => 'bool', 'getStrength' => 'int', 'getLocale' => 'string|false', 'getErrorCode' => 'int|false', 'getErrorMessage' => 'string|false', 'getSortKey' => 'string|false', ], 'IntlIterator' => [ 'current' => 'mixed', 'key' => 'mixed', 'next' => 'void', 'rewind' => 'void', 'valid' => 'bool', ], 'UConverter' => [ 'convert' => 'string|false', 'fromUCallback' => 'string|int|array|null', 'getAliases' => 'array|false|null', 'getAvailable' => 'array', 'getDestinationEncoding' => 'string|false|null', 'getDestinationType' => 'int|false|null', 'getErrorCode' => 'int', 'getErrorMessage' => '?string', 'getSourceEncoding' => 'string|false|null', 'getSourceType' => 'int|false|null', 'getStandards' => '?array', 'getSubstChars' => 'string|false|null', 'reasonText' => 'string', 'setDestinationEncoding' => 'bool', 'setSourceEncoding' => 'bool', 'setSubstChars' => 'bool', 'toUCallback' => 'string|int|array|null', 'transcode' => 'string|false', ], 'IntlDateFormatter' => [ 'create' => '?IntlDateFormatter', 'getDateType' => 'int|false', 'getTimeType' => 'int|false', 'getCalendar' => 'int|false', 'setCalendar' => 'bool', 'getTimeZoneId' => 'string|false', 'getCalendarObject' => 'IntlCalendar|false|null', 'getTimeZone' => 'IntlTimeZone|false', 'setTimeZone' => '?bool', 'setPattern' => 'bool', 'getPattern' => 'string|false', 'getLocale' => 'string|false', 'setLenient' => 'void', 'isLenient' => 'bool', 'format' => 'string|false', 'formatObject' => 'string|false', 'parse' => 'int|float|false', 'localtime' => 'array|false', 'getErrorCode' => 'int', 'getErrorMessage' => 'string', ], 'NumberFormatter' => [ 'create' => '?NumberFormatter', 'format' => 'string|false', 'parse' => 'int|float|false', 'formatCurrency' => 'string|false', 'parseCurrency' => 'float|false', 'setAttribute' => 'bool', 'getAttribute' => 'int|float|false', 'setTextAttribute' => 'bool', 'getTextAttribute' => 'string|false', 'setSymbol' => 'bool', 'getSymbol' => 'string|false', 'setPattern' => 'bool', 'getPattern' => 'string|false', 'getLocale' => 'string|false', 'getErrorCode' => 'int', 'getErrorMessage' => 'string', ], 'Locale' => [ 'getDefault' => 'string', 'getPrimaryLanguage' => '?string', 'getScript' => '?string', 'getRegion' => '?string', 'getKeywords' => 'array|false|null', 'getDisplayScript' => 'string|false', 'getDisplayRegion' => 'string|false', 'getDisplayName' => 'string|false', 'getDisplayLanguage' => 'string|false', 'getDisplayVariant' => 'string|false', 'composeLocale' => 'string|false', 'parseLocale' => '?array', 'getAllVariants' => '?array', 'filterMatches' => '?bool', 'lookup' => '?string', 'canonicalize' => '?string', 'acceptFromHttp' => 'string|false', ], 'MessageFormatter' => [ 'create' => '?MessageFormatter', 'format' => 'string|false', 'formatMessage' => 'string|false', 'parse' => 'array|false', 'parseMessage' => 'array|false', 'setPattern' => 'bool', 'getPattern' => 'string|false', 'getLocale' => 'string', 'getErrorCode' => 'int', 'getErrorMessage' => 'string', ], 'Normalizer' => [ 'normalize' => 'string|false', 'isNormalized' => 'bool', 'getRawDecomposition' => '?string', ], 'ResourceBundle' => [ 'create' => '?ResourceBundle', 'get' => 'mixed', 'count' => 'int', 'getLocales' => 'array|false', 'getErrorCode' => 'int', 'getErrorMessage' => 'string', ], 'Spoofchecker' => [ 'isSuspicious' => 'bool', 'areConfusable' => 'bool', 'setAllowedLocales' => 'void', 'setChecks' => 'void', 'setRestrictionLevel' => 'void', ], 'IntlTimeZone' => [ 'countEquivalentIDs' => 'int|false', 'createDefault' => 'IntlTimeZone', 'createEnumeration' => 'IntlIterator|false', 'createTimeZone' => '?IntlTimeZone', 'createTimeZoneIDEnumeration' => 'IntlIterator|false', 'fromDateTimeZone' => '?IntlTimeZone', 'getCanonicalID' => 'string|false', 'getDisplayName' => 'string|false', 'getDSTSavings' => 'int', 'getEquivalentID' => 'string|false', 'getErrorCode' => 'int|false', 'getErrorMessage' => 'string|false', 'getGMT' => 'IntlTimeZone', 'getID' => 'string|false', 'getOffset' => 'bool', 'getRawOffset' => 'int', 'getRegion' => 'string|false', 'getTZDataVersion' => 'string|false', 'getUnknown' => 'IntlTimeZone', 'getWindowsID' => 'string|false', 'getIDForWindowsID' => 'string|false', 'hasSameRules' => 'bool', 'toDateTimeZone' => 'DateTimeZone|false', 'useDaylightTime' => 'bool', ], 'Transliterator' => [ 'create' => '?Transliterator', 'createFromRules' => '?Transliterator', 'createInverse' => '?Transliterator', 'listIDs' => 'array|false', 'transliterate' => 'string|false', 'getErrorCode' => 'int|false', 'getErrorMessage' => 'string|false', ], 'IntlChar' => [ 'hasBinaryProperty' => '?bool', 'charAge' => '?array', 'charDigitValue' => '?int', 'charDirection' => '?int', 'charFromName' => '?int', 'charMirror' => 'int|string|null', 'charName' => '?string', 'charType' => '?int', 'chr' => '?string', 'digit' => 'int|false|null', 'enumCharNames' => '?bool', 'enumCharTypes' => 'void', 'foldCase' => 'int|string|null', 'forDigit' => 'int', 'getBidiPairedBracket' => 'int|string|null', 'getBlockCode' => '?int', 'getCombiningClass' => '?int', 'getFC_NFKC_Closure' => 'string|false|null', 'getIntPropertyMaxValue' => 'int', 'getIntPropertyMinValue' => 'int', 'getIntPropertyValue' => '?int', 'getNumericValue' => '?float', 'getPropertyEnum' => 'int', 'getPropertyName' => 'string|false', 'getPropertyValueEnum' => 'int', 'getPropertyValueName' => 'string|false', 'getUnicodeVersion' => 'array', 'isalnum' => '?bool', 'isalpha' => '?bool', 'isbase' => '?bool', 'isblank' => '?bool', 'iscntrl' => '?bool', 'isdefined' => '?bool', 'isdigit' => '?bool', 'isgraph' => '?bool', 'isIDIgnorable' => '?bool', 'isIDPart' => '?bool', 'isIDStart' => '?bool', 'isISOControl' => '?bool', 'isJavaIDPart' => '?bool', 'isJavaIDStart' => '?bool', 'isJavaSpaceChar' => '?bool', 'islower' => '?bool', 'isMirrored' => '?bool', 'isprint' => '?bool', 'ispunct' => '?bool', 'isspace' => '?bool', 'istitle' => '?bool', 'isUAlphabetic' => '?bool', 'isULowercase' => '?bool', 'isupper' => '?bool', 'isUUppercase' => '?bool', 'isUWhiteSpace' => '?bool', 'isWhitespace' => '?bool', 'isxdigit' => '?bool', 'ord' => '?int', 'tolower' => 'int|string|null', 'totitle' => 'int|string|null', 'toupper' => 'int|string|null', ], 'JsonSerializable' => [ 'jsonSerialize' => 'mixed', ], 'mysqli' => [ 'autocommit' => 'bool', 'begin_transaction' => 'bool', 'change_user' => 'bool', 'character_set_name' => 'string', 'commit' => 'bool', 'connect' => 'bool', 'dump_debug_info' => 'bool', 'get_charset' => '?object', 'get_client_info' => 'string', 'get_connection_stats' => 'array', 'get_server_info' => 'string', 'get_warnings' => 'mysqli_warning|false', 'kill' => 'bool', 'multi_query' => 'bool', 'more_results' => 'bool', 'next_result' => 'bool', 'ping' => 'bool', 'poll' => 'int|false', 'prepare' => 'mysqli_stmt|false', 'query' => 'mysqli_result|bool', 'real_connect' => 'bool', 'real_escape_string' => 'string', 'reap_async_query' => 'mysqli_result|bool', 'escape_string' => 'string', 'real_query' => 'bool', 'release_savepoint' => 'bool', 'rollback' => 'bool', 'savepoint' => 'bool', 'select_db' => 'bool', 'set_charset' => 'bool', 'options' => 'bool', 'set_opt' => 'bool', 'stat' => 'string|false', 'stmt_init' => 'mysqli_stmt|false', 'store_result' => 'mysqli_result|false', 'thread_safe' => 'bool', 'use_result' => 'mysqli_result|false', 'refresh' => 'bool', ], 'mysqli_result' => [ 'close' => 'void', 'free' => 'void', 'data_seek' => 'bool', 'fetch_field' => 'object|false', 'fetch_fields' => 'array', 'fetch_field_direct' => 'object|false', 'fetch_all' => 'array', 'fetch_array' => 'array|null|false', 'fetch_assoc' => 'array|null|false', 'fetch_object' => 'object|null|false', 'fetch_row' => 'array|null|false', 'field_seek' => 'bool', 'free_result' => 'void', ], 'mysqli_stmt' => [ 'attr_get' => 'int', 'attr_set' => 'bool', 'bind_param' => 'bool', 'bind_result' => 'bool', 'data_seek' => 'void', 'execute' => 'bool', 'fetch' => '?bool', 'get_warnings' => 'mysqli_warning|false', 'result_metadata' => 'mysqli_result|false', 'more_results' => 'bool', 'next_result' => 'bool', 'num_rows' => 'int|string', 'send_long_data' => 'bool', 'free_result' => 'void', 'reset' => 'bool', 'prepare' => 'bool', 'store_result' => 'bool', 'get_result' => 'mysqli_result|false', ], 'OCILob' => [ 'save' => 'bool', 'import' => 'bool', 'saveFile' => 'bool', 'load' => 'string|false', 'read' => 'string|false', 'eof' => 'bool', 'tell' => 'int|false', 'rewind' => 'bool', 'seek' => 'bool', 'size' => 'int|false', 'write' => 'int|false', 'append' => 'bool', 'truncate' => 'bool', 'erase' => 'int|false', 'flush' => 'bool', 'setBuffering' => 'bool', 'getBuffering' => 'bool', 'writeToFile' => 'bool', 'export' => 'bool', 'writeTemporary' => 'bool', 'close' => 'bool', 'free' => 'bool', ], 'OCICollection' => [ 'free' => 'bool', 'append' => 'bool', 'getElem' => 'string|float|null|false', 'assign' => 'bool', 'assignElem' => 'bool', 'size' => 'int|false', 'max' => 'int|false', 'trim' => 'bool', ], 'PDO' => [ 'beginTransaction' => 'bool', 'commit' => 'bool', 'errorCode' => '?string', 'errorInfo' => 'array', 'exec' => 'int|false', 'getAttribute' => 'mixed', 'getAvailableDrivers' => 'array', 'inTransaction' => 'bool', 'lastInsertId' => 'string|false', 'prepare' => 'PDOStatement|false', 'query' => 'PDOStatement|false', 'quote' => 'string|false', 'rollBack' => 'bool', 'setAttribute' => 'bool', ], 'PDOStatement' => [ 'bindColumn' => 'bool', 'bindParam' => 'bool', 'bindValue' => 'bool', 'closeCursor' => 'bool', 'columnCount' => 'int', 'debugDumpParams' => '?bool', 'errorCode' => '?string', 'errorInfo' => 'array', 'execute' => 'bool', 'fetch' => 'mixed', 'fetchAll' => 'array', 'fetchColumn' => 'mixed', 'fetchObject' => 'object|false', 'getAttribute' => 'mixed', 'getColumnMeta' => 'array|false', 'nextRowset' => 'bool', 'rowCount' => 'int', 'setAttribute' => 'bool', ], 'PDO_PGSql_Ext' => [ 'pgsqlCopyFromArray' => 'bool', 'pgsqlCopyFromFile' => 'bool', 'pgsqlCopyToArray' => 'array|false', 'pgsqlCopyToFile' => 'bool', 'pgsqlLOBCreate' => 'string|false', 'pgsqlLOBUnlink' => 'bool', 'pgsqlGetNotify' => 'array|false', 'pgsqlGetPid' => 'int', ], 'PDO_SQLite_Ext' => [ 'sqliteCreateFunction' => 'bool', 'sqliteCreateAggregate' => 'bool', 'sqliteCreateCollation' => 'bool', ], 'Phar' => [ 'addEmptyDir' => 'void', 'addFile' => 'void', 'addFromString' => 'void', 'buildFromDirectory' => 'array', 'buildFromIterator' => 'array', 'compressFiles' => 'void', 'compress' => '?Phar', 'decompress' => '?Phar', 'convertToExecutable' => '?Phar', 'convertToData' => '?PharData', 'count' => 'int', 'extractTo' => 'bool', 'getAlias' => '?string', 'getPath' => 'string', 'getMetadata' => 'mixed', 'getModified' => 'bool', 'getSignature' => 'array|false', 'getStub' => 'string', 'getVersion' => 'string', 'hasMetadata' => 'bool', 'isBuffering' => 'bool', 'isCompressed' => 'int|false', 'isFileFormat' => 'bool', 'isWritable' => 'bool', 'offsetExists' => 'bool', 'offsetGet' => 'SplFileInfo', 'offsetSet' => 'void', 'offsetUnset' => 'void', 'setAlias' => 'bool', 'setDefaultStub' => 'bool', 'setMetadata' => 'void', 'setSignatureAlgorithm' => 'void', 'startBuffering' => 'void', 'stopBuffering' => 'void', ], 'PharData' => [ 'addEmptyDir' => 'void', 'addFile' => 'void', 'addFromString' => 'void', 'buildFromDirectory' => 'array', 'buildFromIterator' => 'array', 'compressFiles' => 'void', 'compress' => '?PharData', 'decompress' => '?PharData', 'convertToExecutable' => '?Phar', 'convertToData' => '?PharData', 'count' => 'int', 'extractTo' => 'bool', 'getAlias' => '?string', 'getPath' => 'string', 'getMetadata' => 'mixed', 'getModified' => 'bool', 'getSignature' => 'array|false', 'getStub' => 'string', 'getVersion' => 'string', 'hasMetadata' => 'bool', 'isBuffering' => 'bool', 'isCompressed' => 'int|false', 'isFileFormat' => 'bool', 'isWritable' => 'bool', 'offsetExists' => 'bool', 'offsetGet' => 'SplFileInfo', 'offsetSet' => 'void', 'offsetUnset' => 'void', 'setAlias' => 'bool', 'setDefaultStub' => 'bool', 'setMetadata' => 'void', 'setSignatureAlgorithm' => 'void', 'startBuffering' => 'void', 'stopBuffering' => 'void', ], 'PharFileInfo' => [ 'chmod' => 'void', 'getCompressedSize' => 'int', 'getCRC32' => 'int', 'getContent' => 'string', 'getMetadata' => 'mixed', 'getPharFlags' => 'int', 'hasMetadata' => 'bool', 'isCompressed' => 'bool', 'isCRCChecked' => 'bool', 'setMetadata' => 'void', ], 'Reflection' => [ 'getModifierNames' => 'array', ], 'ReflectionFunctionAbstract' => [ 'inNamespace' => 'bool', 'isClosure' => 'bool', 'isDeprecated' => 'bool', 'isInternal' => 'bool', 'isUserDefined' => 'bool', 'isGenerator' => 'bool', 'isVariadic' => 'bool', 'isStatic' => 'bool', 'getClosureThis' => '?object', 'getClosureCalledClass' => '?ReflectionClass', 'getClosureScopeClass' => '?ReflectionClass', 'getDocComment' => 'string|false', 'getEndLine' => 'int|false', 'getExtension' => '?ReflectionExtension', 'getExtensionName' => 'string|false', 'getFileName' => 'string|false', 'getName' => 'string', 'getNamespaceName' => 'string', 'getNumberOfParameters' => 'int', 'getNumberOfRequiredParameters' => 'int', 'getParameters' => 'array', 'getShortName' => 'string', 'getStartLine' => 'int|false', 'getStaticVariables' => 'array', 'returnsReference' => 'bool', 'hasReturnType' => 'bool', 'getReturnType' => '?ReflectionType', ], 'ReflectionFunction' => [ 'isDisabled' => 'bool', 'invoke' => 'mixed', 'invokeArgs' => 'mixed', 'getClosure' => 'Closure', 'getExecutingLine' => 'int', 'getExecutingFile' => 'string', 'getTrace' => 'array', 'getFunction' => 'ReflectionFunctionAbstract', 'getThis' => '?object', 'getExecutingGenerator' => 'Generator', ], 'ReflectionMethod' => [ 'isPublic' => 'bool', 'isPrivate' => 'bool', 'isProtected' => 'bool', 'isAbstract' => 'bool', 'isFinal' => 'bool', 'isConstructor' => 'bool', 'isDestructor' => 'bool', 'getClosure' => 'Closure', 'getModifiers' => 'int', 'invoke' => 'mixed', 'invokeArgs' => 'mixed', 'getDeclaringClass' => 'ReflectionClass', 'getPrototype' => 'ReflectionMethod', 'setAccessible' => 'void', ], 'ReflectionClass' => [ 'getName' => 'string', 'isInternal' => 'bool', 'isUserDefined' => 'bool', 'isAnonymous' => 'bool', 'isInstantiable' => 'bool', 'isCloneable' => 'bool', 'getFileName' => 'string|false', 'getStartLine' => 'int|false', 'getEndLine' => 'int|false', 'getDocComment' => 'string|false', 'getConstructor' => '?ReflectionMethod', 'hasMethod' => 'bool', 'getMethod' => 'ReflectionMethod', 'getMethods' => 'array', 'hasProperty' => 'bool', 'getProperty' => 'ReflectionProperty', 'getProperties' => 'array', 'hasConstant' => 'bool', 'getConstants' => 'array', 'getReflectionConstants' => 'array', 'getConstant' => 'mixed', 'getReflectionConstant' => 'ReflectionClassConstant|false', 'getInterfaces' => 'array', 'getInterfaceNames' => 'array', 'isInterface' => 'bool', 'getTraits' => 'array', 'getTraitNames' => 'array', 'getTraitAliases' => 'array', 'isTrait' => 'bool', 'isAbstract' => 'bool', 'isFinal' => 'bool', 'getModifiers' => 'int', 'isInstance' => 'bool', 'newInstance' => 'object', 'newInstanceWithoutConstructor' => 'object', 'newInstanceArgs' => '?object', 'getParentClass' => 'ReflectionClass|false', 'isSubclassOf' => 'bool', 'getStaticProperties' => '?array', 'getStaticPropertyValue' => 'mixed', 'setStaticPropertyValue' => 'void', 'getDefaultProperties' => 'array', 'isIterable' => 'bool', 'isIterateable' => 'bool', 'implementsInterface' => 'bool', 'getExtension' => '?ReflectionExtension', 'getExtensionName' => 'string|false', 'inNamespace' => 'bool', 'getNamespaceName' => 'string', 'getShortName' => 'string', ], 'ReflectionProperty' => [ 'getName' => 'string', 'getValue' => 'mixed', 'setValue' => 'void', 'isInitialized' => 'bool', 'isPublic' => 'bool', 'isPrivate' => 'bool', 'isProtected' => 'bool', 'isStatic' => 'bool', 'isDefault' => 'bool', 'getModifiers' => 'int', 'getDeclaringClass' => 'ReflectionClass', 'getDocComment' => 'string|false', 'setAccessible' => 'void', 'getType' => '?ReflectionType', 'hasType' => 'bool', 'getDefaultValue' => 'mixed', ], 'ReflectionClassConstant' => [ 'getName' => 'string', 'getValue' => 'mixed', 'isPublic' => 'bool', 'isPrivate' => 'bool', 'isProtected' => 'bool', 'getModifiers' => 'int', 'getDeclaringClass' => 'ReflectionClass', 'getDocComment' => 'string|false', ], 'ReflectionParameter' => [ 'getName' => 'string', 'isPassedByReference' => 'bool', 'canBePassedByValue' => 'bool', 'getDeclaringFunction' => 'ReflectionFunctionAbstract', 'getDeclaringClass' => '?ReflectionClass', 'getClass' => '?ReflectionClass', 'hasType' => 'bool', 'getType' => '?ReflectionType', 'isArray' => 'bool', 'isCallable' => 'bool', 'allowsNull' => 'bool', 'getPosition' => 'int', 'isOptional' => 'bool', 'isDefaultValueAvailable' => 'bool', 'getDefaultValue' => 'mixed', 'isDefaultValueConstant' => 'bool', 'getDefaultValueConstantName' => '?string', 'isVariadic' => 'bool', ], 'ReflectionType' => [ 'allowsNull' => 'bool', ], 'ReflectionNamedType' => [ 'getName' => 'string', 'isBuiltin' => 'bool', ], 'ReflectionExtension' => [ 'getName' => 'string', 'getVersion' => '?string', 'getFunctions' => 'array', 'getConstants' => 'array', 'getINIEntries' => 'array', 'getClasses' => 'array', 'getClassNames' => 'array', 'getDependencies' => 'array', 'info' => 'void', 'isPersistent' => 'bool', 'isTemporary' => 'bool', ], 'ReflectionZendExtension' => [ 'getName' => 'string', 'getVersion' => 'string', 'getAuthor' => 'string', 'getURL' => 'string', 'getCopyright' => 'string', ], 'SessionHandlerInterface' => [ 'open' => 'bool', 'close' => 'bool', 'read' => 'string|false', 'write' => 'bool', 'destroy' => 'bool', 'gc' => 'int|false', ], 'SessionIdInterface' => [ 'create_sid' => 'string', ], 'SessionUpdateTimestampHandlerInterface' => [ 'validateId' => 'bool', 'updateTimestamp' => 'bool', ], 'SessionHandler' => [ 'open' => 'bool', 'close' => 'bool', 'read' => 'string|false', 'write' => 'bool', 'destroy' => 'bool', 'gc' => 'int|false', 'create_sid' => 'string', ], 'SimpleXMLElement' => [ 'xpath' => 'array|null|false', 'registerXPathNamespace' => 'bool', 'asXML' => 'string|bool', 'saveXML' => 'string|bool', 'getNamespaces' => 'array', 'getDocNamespaces' => 'array|false', 'children' => '?SimpleXMLElement', 'attributes' => '?SimpleXMLElement', 'addChild' => '?SimpleXMLElement', 'addAttribute' => 'void', 'getName' => 'string', 'count' => 'int', 'rewind' => 'void', 'valid' => 'bool', 'current' => 'SimpleXMLElement', 'key' => 'string', 'next' => 'void', 'hasChildren' => 'bool', 'getChildren' => '?SimpleXMLElement', ], 'SNMP' => [ 'close' => 'bool', 'setSecurity' => 'bool', 'get' => 'mixed', 'getnext' => 'mixed', 'walk' => 'array|false', 'set' => 'bool', 'getErrno' => 'int', 'getError' => 'string', ], 'SoapServer' => [ 'fault' => 'void', 'addSoapHeader' => 'void', 'setPersistence' => 'void', 'setClass' => 'void', 'setObject' => 'void', 'getFunctions' => 'array', 'addFunction' => 'void', 'handle' => 'void', ], 'SoapClient' => [ '__call' => 'mixed', '__soapCall' => 'mixed', '__getFunctions' => '?array', '__getTypes' => '?array', '__getLastRequest' => '?string', '__getLastResponse' => '?string', '__getLastRequestHeaders' => '?string', '__getLastResponseHeaders' => '?string', '__doRequest' => '?string', '__setCookie' => 'void', '__getCookies' => 'array', '__setSoapHeaders' => 'bool', '__setLocation' => '?string', ], 'ArrayObject' => [ 'offsetExists' => 'bool', 'offsetGet' => 'mixed', 'offsetSet' => 'void', 'offsetUnset' => 'void', 'append' => 'void', 'getArrayCopy' => 'array', 'count' => 'int', 'getFlags' => 'int', 'setFlags' => 'void', 'asort' => 'bool', 'ksort' => 'bool', 'uasort' => 'bool', 'uksort' => 'bool', 'natsort' => 'bool', 'natcasesort' => 'bool', 'unserialize' => 'void', 'serialize' => 'string', '__serialize' => 'array', '__unserialize' => 'void', 'getIterator' => 'Iterator', 'exchangeArray' => 'array', 'setIteratorClass' => 'void', 'getIteratorClass' => 'string', '__debugInfo' => 'array', ], 'ArrayIterator' => [ 'offsetExists' => 'bool', 'offsetGet' => 'mixed', 'offsetSet' => 'void', 'offsetUnset' => 'void', 'append' => 'void', 'getArrayCopy' => 'array', 'count' => 'int', 'getFlags' => 'int', 'setFlags' => 'void', 'asort' => 'bool', 'ksort' => 'bool', 'uasort' => 'bool', 'uksort' => 'bool', 'natsort' => 'bool', 'natcasesort' => 'bool', 'unserialize' => 'void', 'serialize' => 'string', '__serialize' => 'array', '__unserialize' => 'void', 'rewind' => 'void', 'current' => 'mixed', 'key' => 'string|int|null', 'next' => 'void', 'valid' => 'bool', 'seek' => 'void', '__debugInfo' => 'array', ], 'RecursiveArrayIterator' => [ 'hasChildren' => 'bool', 'getChildren' => '?RecursiveArrayIterator', ], 'SplFileInfo' => [ 'getPath' => 'string', 'getFilename' => 'string', 'getExtension' => 'string', 'getBasename' => 'string', 'getPathname' => 'string', 'getPerms' => 'int|false', 'getInode' => 'int|false', 'getSize' => 'int|false', 'getOwner' => 'int|false', 'getGroup' => 'int|false', 'getATime' => 'int|false', 'getMTime' => 'int|false', 'getCTime' => 'int|false', 'getType' => 'string|false', 'isWritable' => 'bool', 'isReadable' => 'bool', 'isExecutable' => 'bool', 'isFile' => 'bool', 'isDir' => 'bool', 'isLink' => 'bool', 'getLinkTarget' => 'string|false', 'getRealPath' => 'string|false', 'getFileInfo' => 'SplFileInfo', 'getPathInfo' => '?SplFileInfo', 'openFile' => 'SplFileObject', 'setFileClass' => 'void', 'setInfoClass' => 'void', '__debugInfo' => 'array', '_bad_state_ex' => 'void', ], 'DirectoryIterator' => [ 'getFilename' => 'string', 'getExtension' => 'string', 'getBasename' => 'string', 'isDot' => 'bool', 'rewind' => 'void', 'valid' => 'bool', 'key' => 'mixed', 'current' => 'mixed', 'next' => 'void', 'seek' => 'void', ], 'FilesystemIterator' => [ 'rewind' => 'void', 'key' => 'string', 'current' => 'string|SplFileInfo|FilesystemIterator', 'getFlags' => 'int', 'setFlags' => 'void', ], 'RecursiveDirectoryIterator' => [ 'hasChildren' => 'bool', 'getChildren' => 'RecursiveDirectoryIterator', 'getSubPath' => 'string', 'getSubPathname' => 'string', ], 'GlobIterator' => [ 'count' => 'int', ], 'SplFileObject' => [ 'rewind' => 'void', 'eof' => 'bool', 'valid' => 'bool', 'fgets' => 'string', 'fread' => 'string|false', 'fgetcsv' => 'array|false', 'fputcsv' => 'int|false', 'setCsvControl' => 'void', 'getCsvControl' => 'array', 'flock' => 'bool', 'fflush' => 'bool', 'ftell' => 'int|false', 'fseek' => 'int', 'fgetc' => 'string|false', 'fpassthru' => 'int', 'fscanf' => 'array|int|null', 'fwrite' => 'int|false', 'fstat' => 'array', 'ftruncate' => 'bool', 'current' => 'string|array|false', 'key' => 'int', 'next' => 'void', 'setFlags' => 'void', 'getFlags' => 'int', 'setMaxLineLen' => 'void', 'getMaxLineLen' => 'int', 'hasChildren' => 'false', 'getChildren' => 'null', 'seek' => 'void', 'getCurrentLine' => 'string', ], 'SplDoublyLinkedList' => [ 'add' => 'void', 'pop' => 'mixed', 'shift' => 'mixed', 'push' => 'void', 'unshift' => 'void', 'top' => 'mixed', 'bottom' => 'mixed', '__debugInfo' => 'array', 'count' => 'int', 'isEmpty' => 'bool', 'setIteratorMode' => 'int', 'getIteratorMode' => 'int', 'offsetExists' => 'bool', 'offsetGet' => 'mixed', 'offsetSet' => 'void', 'offsetUnset' => 'void', 'rewind' => 'void', 'current' => 'mixed', 'key' => 'int', 'prev' => 'void', 'next' => 'void', 'valid' => 'bool', 'unserialize' => 'void', 'serialize' => 'string', '__serialize' => 'array', '__unserialize' => 'void', ], 'SplQueue' => [ 'enqueue' => 'void', 'dequeue' => 'mixed', ], 'SplFixedArray' => [ '__wakeup' => 'void', 'count' => 'int', 'toArray' => 'array', 'fromArray' => 'SplFixedArray', 'getSize' => 'int', 'offsetExists' => 'bool', 'offsetGet' => 'mixed', 'offsetSet' => 'void', 'offsetUnset' => 'void', ], 'SplPriorityQueue' => [ 'compare' => 'int', 'setExtractFlags' => 'int', 'top' => 'mixed', 'extract' => 'mixed', 'count' => 'int', 'isEmpty' => 'bool', 'rewind' => 'void', 'current' => 'mixed', 'key' => 'int', 'next' => 'void', 'valid' => 'bool', 'isCorrupted' => 'bool', 'getExtractFlags' => 'int', '__debugInfo' => 'array', ], 'SplHeap' => [ 'extract' => 'mixed', 'insert' => 'bool', 'top' => 'mixed', 'count' => 'int', 'isEmpty' => 'bool', 'rewind' => 'void', 'current' => 'mixed', 'key' => 'int', 'next' => 'void', 'valid' => 'bool', 'recoverFromCorruption' => 'bool', 'compare' => 'int', 'isCorrupted' => 'bool', '__debugInfo' => 'array', ], 'SplMinHeap' => [ 'compare' => 'int', ], 'SplMaxHeap' => [ 'compare' => 'int', ], 'EmptyIterator' => [ 'current' => 'never', 'next' => 'void', 'key' => 'never', 'valid' => 'false', 'rewind' => 'void', ], 'CallbackFilterIterator' => [ 'accept' => 'bool', ], 'RecursiveCallbackFilterIterator' => [ 'hasChildren' => 'bool', 'getChildren' => 'RecursiveCallbackFilterIterator', ], 'RecursiveIterator' => [ 'hasChildren' => 'bool', 'getChildren' => '?RecursiveIterator', ], 'RecursiveIteratorIterator' => [ 'rewind' => 'void', 'valid' => 'bool', 'key' => 'mixed', 'current' => 'mixed', 'next' => 'void', 'getDepth' => 'int', 'getSubIterator' => '?RecursiveIterator', 'getInnerIterator' => 'RecursiveIterator', 'beginIteration' => 'void', 'endIteration' => 'void', 'callHasChildren' => 'bool', 'callGetChildren' => '?RecursiveIterator', 'beginChildren' => 'void', 'endChildren' => 'void', 'nextElement' => 'void', 'setMaxDepth' => 'void', 'getMaxDepth' => 'int|false', ], 'OuterIterator' => [ 'getInnerIterator' => '?Iterator', ], 'IteratorIterator' => [ 'getInnerIterator' => '?Iterator', 'rewind' => 'void', 'valid' => 'bool', 'key' => 'mixed', 'current' => 'mixed', 'next' => 'void', ], 'FilterIterator' => [ 'accept' => 'bool', 'rewind' => 'void', 'next' => 'void', ], 'RecursiveFilterIterator' => [ 'hasChildren' => 'bool', 'getChildren' => '?RecursiveFilterIterator', ], 'ParentIterator' => [ 'accept' => 'bool', ], 'SeekableIterator' => [ 'seek' => 'void', ], 'LimitIterator' => [ 'rewind' => 'void', 'valid' => 'bool', 'next' => 'void', 'seek' => 'int', 'getPosition' => 'int', ], 'CachingIterator' => [ 'rewind' => 'void', 'valid' => 'bool', 'next' => 'void', 'hasNext' => 'bool', 'getFlags' => 'int', 'setFlags' => 'void', 'offsetGet' => 'mixed', 'offsetSet' => 'void', 'offsetUnset' => 'void', 'offsetExists' => 'bool', 'getCache' => 'array', 'count' => 'int', ], 'RecursiveCachingIterator' => [ 'hasChildren' => 'bool', 'getChildren' => '?RecursiveCachingIterator', ], 'NoRewindIterator' => [ 'rewind' => 'void', 'valid' => 'bool', 'key' => 'mixed', 'current' => 'mixed', 'next' => 'void', ], 'AppendIterator' => [ 'append' => 'void', 'rewind' => 'void', 'valid' => 'bool', 'current' => 'mixed', 'next' => 'void', 'getIteratorIndex' => '?int', 'getArrayIterator' => 'ArrayIterator', ], 'InfiniteIterator' => [ 'next' => 'void', ], 'RegexIterator' => [ 'accept' => 'bool', 'getMode' => 'int', 'setMode' => 'void', 'getFlags' => 'int', 'setFlags' => 'void', 'getRegex' => 'string', 'getPregFlags' => 'int', 'setPregFlags' => 'void', ], 'RecursiveRegexIterator' => [ 'accept' => 'bool', 'hasChildren' => 'bool', 'getChildren' => 'RecursiveRegexIterator', ], 'RecursiveTreeIterator' => [ 'key' => 'mixed', 'current' => 'mixed', 'getPrefix' => 'string', 'setPostfix' => 'void', 'setPrefixPart' => 'void', 'getEntry' => 'string', 'getPostfix' => 'string', ], 'SplObserver' => [ 'update' => 'void', ], 'SplSubject' => [ 'attach' => 'void', 'detach' => 'void', 'notify' => 'void', ], 'SplObjectStorage' => [ 'attach' => 'void', 'detach' => 'void', 'contains' => 'bool', 'addAll' => 'int', 'removeAll' => 'int', 'removeAllExcept' => 'int', 'getInfo' => 'mixed', 'setInfo' => 'void', 'count' => 'int', 'rewind' => 'void', 'valid' => 'bool', 'key' => 'int', 'current' => 'object', 'next' => 'void', 'unserialize' => 'void', 'serialize' => 'string', 'offsetExists' => 'bool', 'offsetGet' => 'mixed', 'offsetSet' => 'void', 'offsetUnset' => 'void', 'getHash' => 'string', '__serialize' => 'array', '__unserialize' => 'void', '__debugInfo' => 'array', ], 'MultipleIterator' => [ 'getFlags' => 'int', 'setFlags' => 'void', 'attachIterator' => 'void', 'detachIterator' => 'void', 'containsIterator' => 'bool', 'countIterators' => 'int', 'rewind' => 'void', 'valid' => 'bool', 'key' => 'array', 'current' => 'array', 'next' => 'void', '__debugInfo' => 'array', ], 'SQLite3' => [ 'open' => 'void', 'version' => 'array', 'lastInsertRowID' => 'int', 'lastErrorCode' => 'int', 'lastExtendedErrorCode' => 'int', 'lastErrorMsg' => 'string', 'changes' => 'int', 'busyTimeout' => 'bool', 'loadExtension' => 'bool', 'backup' => 'bool', 'escapeString' => 'string', 'prepare' => 'SQLite3Stmt|false', 'exec' => 'bool', 'query' => 'SQLite3Result|false', 'querySingle' => 'mixed', 'createFunction' => 'bool', 'createAggregate' => 'bool', 'createCollation' => 'bool', 'enableExceptions' => 'bool', 'enableExtendedResultCodes' => 'bool', 'setAuthorizer' => 'bool', ], 'SQLite3Stmt' => [ 'bindParam' => 'bool', 'bindValue' => 'bool', 'clear' => 'bool', 'close' => 'bool', 'execute' => 'SQLite3Result|false', 'getSQL' => 'string|false', 'paramCount' => 'int', 'readOnly' => 'bool', 'reset' => 'bool', ], 'SQLite3Result' => [ 'numColumns' => 'int', 'columnName' => 'string|false', 'columnType' => 'int|false', 'fetchArray' => 'array|false', 'reset' => 'bool', ], 'Directory' => [ 'close' => 'void', 'rewind' => 'void', 'read' => 'string|false', ], 'php_user_filter' => [ 'filter' => 'int', 'onCreate' => 'bool', 'onClose' => 'void', ], 'tidy' => [ 'getOpt' => 'string|int|bool', 'cleanRepair' => 'bool', 'parseFile' => 'bool', 'parseString' => 'bool', 'repairString' => 'string|false', 'repairFile' => 'string|false', 'diagnose' => 'bool', 'getRelease' => 'string', 'getConfig' => 'array', 'getStatus' => 'int', 'getHtmlVer' => 'int', 'getOptDoc' => 'string|false', 'isXhtml' => 'bool', 'isXml' => 'bool', 'root' => '?tidyNode', 'head' => '?tidyNode', 'html' => '?tidyNode', 'body' => '?tidyNode', ], 'XMLReader' => [ 'getAttribute' => '?string', 'getAttributeNo' => '?string', 'getAttributeNs' => '?string', 'getParserProperty' => 'bool', 'isValid' => 'bool', 'lookupNamespace' => '?string', 'moveToAttribute' => 'bool', 'moveToAttributeNo' => 'bool', 'moveToAttributeNs' => 'bool', 'moveToElement' => 'bool', 'moveToFirstAttribute' => 'bool', 'moveToNextAttribute' => 'bool', 'read' => 'bool', 'next' => 'bool', 'readInnerXml' => 'string', 'readOuterXml' => 'string', 'readString' => 'string', 'setSchema' => 'bool', 'setParserProperty' => 'bool', 'setRelaxNGSchema' => 'bool', 'setRelaxNGSchemaSource' => 'bool', 'expand' => 'DOMNode|false', ], 'XMLWriter' => [ 'openUri' => 'bool', 'openMemory' => 'bool', 'setIndent' => 'bool', 'setIndentString' => 'bool', 'startComment' => 'bool', 'endComment' => 'bool', 'startAttribute' => 'bool', 'endAttribute' => 'bool', 'writeAttribute' => 'bool', 'startAttributeNs' => 'bool', 'writeAttributeNs' => 'bool', 'startElement' => 'bool', 'endElement' => 'bool', 'fullEndElement' => 'bool', 'startElementNs' => 'bool', 'writeElement' => 'bool', 'writeElementNs' => 'bool', 'startPi' => 'bool', 'endPi' => 'bool', 'writePi' => 'bool', 'startCdata' => 'bool', 'endCdata' => 'bool', 'writeCdata' => 'bool', 'text' => 'bool', 'writeRaw' => 'bool', 'startDocument' => 'bool', 'endDocument' => 'bool', 'writeComment' => 'bool', 'startDtd' => 'bool', 'endDtd' => 'bool', 'writeDtd' => 'bool', 'startDtdElement' => 'bool', 'endDtdElement' => 'bool', 'writeDtdElement' => 'bool', 'startDtdAttlist' => 'bool', 'endDtdAttlist' => 'bool', 'writeDtdAttlist' => 'bool', 'startDtdEntity' => 'bool', 'endDtdEntity' => 'bool', 'writeDtdEntity' => 'bool', 'outputMemory' => 'string', 'flush' => 'string|int', ], 'XSLTProcessor' => [ 'importStylesheet' => 'bool', 'transformToDoc' => 'DOMDocument|false', 'transformToUri' => 'int', 'transformToXml' => 'string|null|false', 'setParameter' => 'bool', 'getParameter' => 'string|false', 'removeParameter' => 'bool', 'hasExsltSupport' => 'bool', 'registerPHPFunctions' => 'void', 'setSecurityPrefs' => 'int', 'getSecurityPrefs' => 'int', ], 'ZipArchive' => [ 'open' => 'bool|int', 'setPassword' => 'bool', 'close' => 'bool', 'count' => 'int', 'getStatusString' => 'string', 'addEmptyDir' => 'bool', 'addFromString' => 'bool', 'addFile' => 'bool', 'replaceFile' => 'bool', 'addGlob' => 'array|false', 'addPattern' => 'array|false', 'renameIndex' => 'bool', 'renameName' => 'bool', 'setArchiveComment' => 'bool', 'getArchiveComment' => 'string|false', 'setCommentIndex' => 'bool', 'setCommentName' => 'bool', 'setMtimeIndex' => 'bool', 'setMtimeName' => 'bool', 'getCommentIndex' => 'string|false', 'getCommentName' => 'string|false', 'deleteIndex' => 'bool', 'deleteName' => 'bool', 'statName' => 'array|false', 'statIndex' => 'array|false', 'locateName' => 'int|false', 'getNameIndex' => 'string|false', 'unchangeArchive' => 'bool', 'unchangeAll' => 'bool', 'unchangeIndex' => 'bool', 'unchangeName' => 'bool', 'extractTo' => 'bool', 'getFromName' => 'string|false', 'getFromIndex' => 'string|false', 'setExternalAttributesName' => 'bool', 'setExternalAttributesIndex' => 'bool', 'getExternalAttributesName' => 'bool', 'getExternalAttributesIndex' => 'bool', 'setCompressionName' => 'bool', 'setCompressionIndex' => 'bool', 'setEncryptionName' => 'bool', 'setEncryptionIndex' => 'bool', 'registerProgressCallback' => 'bool', 'registerCancelCallback' => 'bool', ], 'Exception' => [ '__wakeup' => 'void', ], 'Error' => [ '__wakeup' => 'void', ], 'IteratorAggregate' => [ 'getIterator' => 'Traversable', ], 'Iterator' => [ 'current' => 'mixed', 'next' => 'void', 'key' => 'mixed', 'valid' => 'bool', 'rewind' => 'void', ], 'ArrayAccess' => [ 'offsetExists' => 'bool', 'offsetGet' => 'mixed', 'offsetSet' => 'void', 'offsetUnset' => 'void', ], 'Countable' => [ 'count' => 'int', ], ]; } error-handler/ErrorEnhancer/ErrorEnhancerInterface.php 0000644 00000000772 15025017654 0017147 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\ErrorEnhancer; interface ErrorEnhancerInterface { /** * Returns an \Throwable instance if the class is able to improve the error, null otherwise. */ public function enhance(\Throwable $error): ?\Throwable; } error-handler/ErrorEnhancer/UndefinedMethodErrorEnhancer.php 0000644 00000004205 15025017654 0020304 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\ErrorEnhancer; use Symfony\Component\ErrorHandler\Error\FatalError; use Symfony\Component\ErrorHandler\Error\UndefinedMethodError; /** * @author Grégoire Pineau <lyrixx@lyrixx.info> */ class UndefinedMethodErrorEnhancer implements ErrorEnhancerInterface { /** * {@inheritdoc} */ public function enhance(\Throwable $error): ?\Throwable { if ($error instanceof FatalError) { return null; } $message = $error->getMessage(); preg_match('/^Call to undefined method (.*)::(.*)\(\)$/', $message, $matches); if (!$matches) { return null; } $className = $matches[1]; $methodName = $matches[2]; $message = sprintf('Attempted to call an undefined method named "%s" of class "%s".', $methodName, $className); if ('' === $methodName || !class_exists($className) || null === $methods = get_class_methods($className)) { // failed to get the class or its methods on which an unknown method was called (for example on an anonymous class) return new UndefinedMethodError($message, $error); } $candidates = []; foreach ($methods as $definedMethodName) { $lev = levenshtein($methodName, $definedMethodName); if ($lev <= \strlen($methodName) / 3 || str_contains($definedMethodName, $methodName)) { $candidates[] = $definedMethodName; } } if ($candidates) { sort($candidates); $last = array_pop($candidates).'"?'; if ($candidates) { $candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last; } else { $candidates = '"'.$last; } $message .= "\nDid you mean to call ".$candidates; } return new UndefinedMethodError($message, $error); } } error-handler/ErrorEnhancer/UndefinedFunctionErrorEnhancer.php 0000644 00000006011 15025017654 0020646 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\ErrorEnhancer; use Symfony\Component\ErrorHandler\Error\FatalError; use Symfony\Component\ErrorHandler\Error\UndefinedFunctionError; /** * @author Fabien Potencier <fabien@symfony.com> */ class UndefinedFunctionErrorEnhancer implements ErrorEnhancerInterface { /** * {@inheritdoc} */ public function enhance(\Throwable $error): ?\Throwable { if ($error instanceof FatalError) { return null; } $message = $error->getMessage(); $messageLen = \strlen($message); $notFoundSuffix = '()'; $notFoundSuffixLen = \strlen($notFoundSuffix); if ($notFoundSuffixLen > $messageLen) { return null; } if (0 !== substr_compare($message, $notFoundSuffix, -$notFoundSuffixLen)) { return null; } $prefix = 'Call to undefined function '; $prefixLen = \strlen($prefix); if (!str_starts_with($message, $prefix)) { return null; } $fullyQualifiedFunctionName = substr($message, $prefixLen, -$notFoundSuffixLen); if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedFunctionName, '\\')) { $functionName = substr($fullyQualifiedFunctionName, $namespaceSeparatorIndex + 1); $namespacePrefix = substr($fullyQualifiedFunctionName, 0, $namespaceSeparatorIndex); $message = sprintf('Attempted to call function "%s" from namespace "%s".', $functionName, $namespacePrefix); } else { $functionName = $fullyQualifiedFunctionName; $message = sprintf('Attempted to call function "%s" from the global namespace.', $functionName); } $candidates = []; foreach (get_defined_functions() as $type => $definedFunctionNames) { foreach ($definedFunctionNames as $definedFunctionName) { if (false !== $namespaceSeparatorIndex = strrpos($definedFunctionName, '\\')) { $definedFunctionNameBasename = substr($definedFunctionName, $namespaceSeparatorIndex + 1); } else { $definedFunctionNameBasename = $definedFunctionName; } if ($definedFunctionNameBasename === $functionName) { $candidates[] = '\\'.$definedFunctionName; } } } if ($candidates) { sort($candidates); $last = array_pop($candidates).'"?'; if ($candidates) { $candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last; } else { $candidates = '"'.$last; } $message .= "\nDid you mean to call ".$candidates; } return new UndefinedFunctionError($message, $error); } } error-handler/ErrorEnhancer/ClassNotFoundErrorEnhancer.php 0000644 00000014565 15025017654 0017776 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\ErrorEnhancer; use Composer\Autoload\ClassLoader; use Symfony\Component\ErrorHandler\DebugClassLoader; use Symfony\Component\ErrorHandler\Error\ClassNotFoundError; use Symfony\Component\ErrorHandler\Error\FatalError; /** * @author Fabien Potencier <fabien@symfony.com> */ class ClassNotFoundErrorEnhancer implements ErrorEnhancerInterface { /** * {@inheritdoc} */ public function enhance(\Throwable $error): ?\Throwable { // Some specific versions of PHP produce a fatal error when extending a not found class. $message = !$error instanceof FatalError ? $error->getMessage() : $error->getError()['message']; if (!preg_match('/^(Class|Interface|Trait) [\'"]([^\'"]+)[\'"] not found$/', $message, $matches)) { return null; } $typeName = strtolower($matches[1]); $fullyQualifiedClassName = $matches[2]; if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) { $className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1); $namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex); $message = sprintf('Attempted to load %s "%s" from namespace "%s".', $typeName, $className, $namespacePrefix); $tail = ' for another namespace?'; } else { $className = $fullyQualifiedClassName; $message = sprintf('Attempted to load %s "%s" from the global namespace.', $typeName, $className); $tail = '?'; } if ($candidates = $this->getClassCandidates($className)) { $tail = array_pop($candidates).'"?'; if ($candidates) { $tail = ' for e.g. "'.implode('", "', $candidates).'" or "'.$tail; } else { $tail = ' for "'.$tail; } } $message .= "\nDid you forget a \"use\" statement".$tail; return new ClassNotFoundError($message, $error); } /** * Tries to guess the full namespace for a given class name. * * By default, it looks for PSR-0 and PSR-4 classes registered via a Symfony or a Composer * autoloader (that should cover all common cases). * * @param string $class A class name (without its namespace) * * Returns an array of possible fully qualified class names */ private function getClassCandidates(string $class): array { if (!\is_array($functions = spl_autoload_functions())) { return []; } // find Symfony and Composer autoloaders $classes = []; foreach ($functions as $function) { if (!\is_array($function)) { continue; } // get class loaders wrapped by DebugClassLoader if ($function[0] instanceof DebugClassLoader) { $function = $function[0]->getClassLoader(); if (!\is_array($function)) { continue; } } if ($function[0] instanceof ClassLoader) { foreach ($function[0]->getPrefixes() as $prefix => $paths) { foreach ($paths as $path) { $classes[] = $this->findClassInPath($path, $class, $prefix); } } foreach ($function[0]->getPrefixesPsr4() as $prefix => $paths) { foreach ($paths as $path) { $classes[] = $this->findClassInPath($path, $class, $prefix); } } } } return array_unique(array_merge([], ...$classes)); } private function findClassInPath(string $path, string $class, string $prefix): array { if (!$path = realpath($path.'/'.strtr($prefix, '\\_', '//')) ?: realpath($path.'/'.\dirname(strtr($prefix, '\\_', '//'))) ?: realpath($path)) { return []; } $classes = []; $filename = $class.'.php'; foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { if ($filename == $file->getFileName() && $class = $this->convertFileToClass($path, $file->getPathName(), $prefix)) { $classes[] = $class; } } return $classes; } private function convertFileToClass(string $path, string $file, string $prefix): ?string { $candidates = [ // namespaced class $namespacedClass = str_replace([$path.\DIRECTORY_SEPARATOR, '.php', '/'], ['', '', '\\'], $file), // namespaced class (with target dir) $prefix.$namespacedClass, // namespaced class (with target dir and separator) $prefix.'\\'.$namespacedClass, // PEAR class str_replace('\\', '_', $namespacedClass), // PEAR class (with target dir) str_replace('\\', '_', $prefix.$namespacedClass), // PEAR class (with target dir and separator) str_replace('\\', '_', $prefix.'\\'.$namespacedClass), ]; if ($prefix) { $candidates = array_filter($candidates, function ($candidate) use ($prefix) { return str_starts_with($candidate, $prefix); }); } // We cannot use the autoloader here as most of them use require; but if the class // is not found, the new autoloader call will require the file again leading to a // "cannot redeclare class" error. foreach ($candidates as $candidate) { if ($this->classExists($candidate)) { return $candidate; } } try { require_once $file; } catch (\Throwable $e) { return null; } foreach ($candidates as $candidate) { if ($this->classExists($candidate)) { return $candidate; } } return null; } private function classExists(string $class): bool { return class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false); } } error-handler/composer.json 0000644 00000001771 15025017654 0012045 0 ustar 00 { "name": "symfony/error-handler", "type": "library", "description": "Provides tools to manage errors and ease debugging PHP code", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.0.2", "psr/log": "^1|^2|^3", "symfony/var-dumper": "^5.4|^6.0" }, "require-dev": { "symfony/http-kernel": "^5.4|^6.0", "symfony/serializer": "^5.4|^6.0", "symfony/deprecation-contracts": "^2.1|^3" }, "autoload": { "psr-4": { "Symfony\\Component\\ErrorHandler\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "bin": [ "Resources/bin/patch-type-declarations" ], "minimum-stability": "dev" } error-handler/BufferingLogger.php 0000644 00000003740 15025017654 0013101 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler; use Psr\Log\AbstractLogger; /** * A buffering logger that stacks logs for later. * * @author Nicolas Grekas <p@tchwork.com> */ class BufferingLogger extends AbstractLogger { private array $logs = []; public function log($level, $message, array $context = []): void { $this->logs[] = [$level, $message, $context]; } public function cleanLogs(): array { $logs = $this->logs; $this->logs = []; return $logs; } public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } public function __wakeup() { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } public function __destruct() { foreach ($this->logs as [$level, $message, $context]) { if (str_contains($message, '{')) { foreach ($context as $key => $val) { if (null === $val || \is_scalar($val) || (\is_object($val) && \is_callable([$val, '__toString']))) { $message = str_replace("{{$key}}", $val, $message); } elseif ($val instanceof \DateTimeInterface) { $message = str_replace("{{$key}}", $val->format(\DateTime::RFC3339), $message); } elseif (\is_object($val)) { $message = str_replace("{{$key}}", '[object '.get_debug_type($val).']', $message); } else { $message = str_replace("{{$key}}", '['.\gettype($val).']', $message); } } } error_log(sprintf('%s [%s] %s', date(\DateTime::RFC3339), $level, $message)); } } } error-handler/CHANGELOG.md 0000644 00000001174 15025017654 0011131 0 ustar 00 CHANGELOG ========= 5.4 --- * Make `DebugClassLoader` trigger deprecation notices on missing return types * Add `SYMFONY_PATCH_TYPE_DECLARATIONS='force=2'` mode to `DebugClassLoader` to turn annotations into native return types 5.2.0 ----- * added the ability to set `HtmlErrorRenderer::$template` to a custom template to render when not in debug mode. 5.1.0 ----- * The `HtmlErrorRenderer` and `SerializerErrorRenderer` add `X-Debug-Exception` and `X-Debug-Exception-File` headers in debug mode. 4.4.0 ----- * added the component * added `ErrorHandler::call()` method utility to turn any PHP error into `\ErrorException` error-handler/Exception/FlattenException.php 0000644 00000024277 15025017654 0015254 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\Exception; use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; /** * FlattenException wraps a PHP Error or Exception to be able to serialize it. * * Basically, this class removes all objects from the trace. * * @author Fabien Potencier <fabien@symfony.com> */ class FlattenException { private string $message; private string|int $code; private ?self $previous = null; private array $trace; private string $traceAsString; private string $class; private int $statusCode; private string $statusText; private array $headers; private string $file; private int $line; private ?string $asString = null; public static function create(\Exception $exception, int $statusCode = null, array $headers = []): static { return static::createFromThrowable($exception, $statusCode, $headers); } public static function createFromThrowable(\Throwable $exception, int $statusCode = null, array $headers = []): static { $e = new static(); $e->setMessage($exception->getMessage()); $e->setCode($exception->getCode()); if ($exception instanceof HttpExceptionInterface) { $statusCode = $exception->getStatusCode(); $headers = array_merge($headers, $exception->getHeaders()); } elseif ($exception instanceof RequestExceptionInterface) { $statusCode = 400; } if (null === $statusCode) { $statusCode = 500; } if (class_exists(Response::class) && isset(Response::$statusTexts[$statusCode])) { $statusText = Response::$statusTexts[$statusCode]; } else { $statusText = 'Whoops, looks like something went wrong.'; } $e->setStatusText($statusText); $e->setStatusCode($statusCode); $e->setHeaders($headers); $e->setTraceFromThrowable($exception); $e->setClass(get_debug_type($exception)); $e->setFile($exception->getFile()); $e->setLine($exception->getLine()); $previous = $exception->getPrevious(); if ($previous instanceof \Throwable) { $e->setPrevious(static::createFromThrowable($previous)); } return $e; } public function toArray(): array { $exceptions = []; foreach (array_merge([$this], $this->getAllPrevious()) as $exception) { $exceptions[] = [ 'message' => $exception->getMessage(), 'class' => $exception->getClass(), 'trace' => $exception->getTrace(), ]; } return $exceptions; } public function getStatusCode(): int { return $this->statusCode; } /** * @return $this */ public function setStatusCode(int $code): static { $this->statusCode = $code; return $this; } public function getHeaders(): array { return $this->headers; } /** * @return $this */ public function setHeaders(array $headers): static { $this->headers = $headers; return $this; } public function getClass(): string { return $this->class; } /** * @return $this */ public function setClass(string $class): static { $this->class = str_contains($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class; return $this; } public function getFile(): string { return $this->file; } /** * @return $this */ public function setFile(string $file): static { $this->file = $file; return $this; } public function getLine(): int { return $this->line; } /** * @return $this */ public function setLine(int $line): static { $this->line = $line; return $this; } public function getStatusText(): string { return $this->statusText; } /** * @return $this */ public function setStatusText(string $statusText): static { $this->statusText = $statusText; return $this; } public function getMessage(): string { return $this->message; } /** * @return $this */ public function setMessage(string $message): static { if (str_contains($message, "@anonymous\0")) { $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) { return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0]; }, $message); } $this->message = $message; return $this; } /** * @return int|string int most of the time (might be a string with PDOException) */ public function getCode(): int|string { return $this->code; } /** * @return $this */ public function setCode(int|string $code): static { $this->code = $code; return $this; } public function getPrevious(): ?self { return $this->previous; } /** * @return $this */ public function setPrevious(?self $previous): static { $this->previous = $previous; return $this; } /** * @return self[] */ public function getAllPrevious(): array { $exceptions = []; $e = $this; while ($e = $e->getPrevious()) { $exceptions[] = $e; } return $exceptions; } public function getTrace(): array { return $this->trace; } /** * @return $this */ public function setTraceFromThrowable(\Throwable $throwable): static { $this->traceAsString = $throwable->getTraceAsString(); return $this->setTrace($throwable->getTrace(), $throwable->getFile(), $throwable->getLine()); } /** * @return $this */ public function setTrace(array $trace, ?string $file, ?int $line): static { $this->trace = []; $this->trace[] = [ 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => $file, 'line' => $line, 'args' => [], ]; foreach ($trace as $entry) { $class = ''; $namespace = ''; if (isset($entry['class'])) { $parts = explode('\\', $entry['class']); $class = array_pop($parts); $namespace = implode('\\', $parts); } $this->trace[] = [ 'namespace' => $namespace, 'short_class' => $class, 'class' => $entry['class'] ?? '', 'type' => $entry['type'] ?? '', 'function' => $entry['function'] ?? null, 'file' => $entry['file'] ?? null, 'line' => $entry['line'] ?? null, 'args' => isset($entry['args']) ? $this->flattenArgs($entry['args']) : [], ]; } return $this; } private function flattenArgs(array $args, int $level = 0, int &$count = 0): array { $result = []; foreach ($args as $key => $value) { if (++$count > 1e4) { return ['array', '*SKIPPED over 10000 entries*']; } if ($value instanceof \__PHP_Incomplete_Class) { $result[$key] = ['incomplete-object', $this->getClassNameFromIncomplete($value)]; } elseif (\is_object($value)) { $result[$key] = ['object', get_debug_type($value)]; } elseif (\is_array($value)) { if ($level > 10) { $result[$key] = ['array', '*DEEP NESTED ARRAY*']; } else { $result[$key] = ['array', $this->flattenArgs($value, $level + 1, $count)]; } } elseif (null === $value) { $result[$key] = ['null', null]; } elseif (\is_bool($value)) { $result[$key] = ['boolean', $value]; } elseif (\is_int($value)) { $result[$key] = ['integer', $value]; } elseif (\is_float($value)) { $result[$key] = ['float', $value]; } elseif (\is_resource($value)) { $result[$key] = ['resource', get_resource_type($value)]; } else { $result[$key] = ['string', (string) $value]; } } return $result; } private function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value): string { $array = new \ArrayObject($value); return $array['__PHP_Incomplete_Class_Name']; } public function getTraceAsString(): string { return $this->traceAsString; } /** * @return $this */ public function setAsString(?string $asString): static { $this->asString = $asString; return $this; } public function getAsString(): string { if (null !== $this->asString) { return $this->asString; } $message = ''; $next = false; foreach (array_reverse(array_merge([$this], $this->getAllPrevious())) as $exception) { if ($next) { $message .= 'Next '; } else { $next = true; } $message .= $exception->getClass(); if ('' != $exception->getMessage()) { $message .= ': '.$exception->getMessage(); } $message .= ' in '.$exception->getFile().':'.$exception->getLine(). "\nStack trace:\n".$exception->getTraceAsString()."\n\n"; } return rtrim($message); } } error-handler/Exception/SilencedErrorContext.php 0000644 00000002655 15025017654 0016101 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\Exception; /** * Data Object that represents a Silenced Error. * * @author Grégoire Pineau <lyrixx@lyrixx.info> */ class SilencedErrorContext implements \JsonSerializable { public $count = 1; private int $severity; private string $file; private int $line; private array $trace; public function __construct(int $severity, string $file, int $line, array $trace = [], int $count = 1) { $this->severity = $severity; $this->file = $file; $this->line = $line; $this->trace = $trace; $this->count = $count; } public function getSeverity(): int { return $this->severity; } public function getFile(): string { return $this->file; } public function getLine(): int { return $this->line; } public function getTrace(): array { return $this->trace; } public function jsonSerialize(): array { return [ 'severity' => $this->severity, 'file' => $this->file, 'line' => $this->line, 'trace' => $this->trace, 'count' => $this->count, ]; } } error-handler/README.md 0000644 00000002475 15025017654 0010604 0 ustar 00 ErrorHandler Component ====================== The ErrorHandler component provides tools to manage errors and ease debugging PHP code. Getting Started --------------- ``` $ composer require symfony/error-handler ``` ```php use Symfony\Component\ErrorHandler\Debug; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\ErrorHandler\DebugClassLoader; Debug::enable(); // or enable only one feature //ErrorHandler::register(); //DebugClassLoader::enable(); // If you want a custom generic template when debug is not enabled // HtmlErrorRenderer::setTemplate('/path/to/custom/error.html.php'); $data = ErrorHandler::call(static function () use ($filename, $datetimeFormat) { // if any code executed inside this anonymous function fails, a PHP exception // will be thrown, even if the code uses the '@' PHP silence operator $data = json_decode(file_get_contents($filename), true); $data['read_at'] = date($datetimeFormat); file_put_contents($filename, json_encode($data)); return $data; }); ``` Resources --------- * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) error-handler/ErrorRenderer/ErrorRendererInterface.php 0000644 00000001206 15025017654 0017206 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\ErrorRenderer; use Symfony\Component\ErrorHandler\Exception\FlattenException; /** * Formats an exception to be used as response content. * * @author Yonel Ceruto <yonelceruto@gmail.com> */ interface ErrorRendererInterface { /** * Renders a Throwable as a FlattenException. */ public function render(\Throwable $exception): FlattenException; } error-handler/ErrorRenderer/HtmlErrorRenderer.php 0000644 00000055543 15025017654 0016227 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\ErrorRenderer; use Psr\Log\LoggerInterface; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; /** * @author Yonel Ceruto <yonelceruto@gmail.com> */ class HtmlErrorRenderer implements ErrorRendererInterface { private const GHOST_ADDONS = [ '02-14' => self::GHOST_HEART, '02-29' => self::GHOST_PLUS, '10-18' => self::GHOST_GIFT, ]; private const GHOST_GIFT = 'M124.00534057617188,5.3606138080358505 C124.40059661865234,4.644828304648399 125.1237564086914,3.712414965033531 123.88127899169922,3.487462028861046 C123.53517150878906,3.3097832053899765 123.18894958496094,2.9953975528478622 122.8432846069336,3.345616325736046 C122.07421112060547,3.649444565176964 121.40750122070312,4.074306473135948 122.2164306640625,4.869479164481163 C122.57514953613281,5.3830065578222275 122.90142822265625,6.503447040915489 123.3077621459961,6.626829609274864 C123.55027770996094,6.210384353995323 123.7774658203125,5.785196766257286 124.00534057617188,5.3606138080358505 zM122.30630493164062,7.336987480521202 C121.60028076171875,6.076864704489708 121.03211975097656,4.72498320043087 120.16796875,3.562500938773155 C119.11695098876953,2.44033907353878 117.04605865478516,2.940566048026085 116.57544708251953,4.387995228171349 C115.95028686523438,5.819030746817589 117.2991714477539,7.527640804648399 118.826171875,7.348545059561729 C119.98493194580078,7.367936596274376 121.15027618408203,7.420116886496544 122.30630493164062,7.336987480521202 zM128.1732177734375,7.379541382193565 C129.67486572265625,7.17823551595211 130.53842163085938,5.287807449698448 129.68344116210938,4.032590612769127 C128.92578125,2.693056806921959 126.74605560302734,2.6463639587163925 125.98509216308594,4.007616028189659 C125.32617950439453,5.108129009604454 124.75428009033203,6.258124336600304 124.14962768554688,7.388818249106407 C125.48638916015625,7.465229496359825 126.8357162475586,7.447416767477989 128.1732177734375,7.379541382193565 zM130.6601104736328,8.991325363516808 C131.17202758789062,8.540884003043175 133.1543731689453,8.009847149252892 131.65304565429688,7.582054600119591 C131.2811279296875,7.476506695151329 130.84751892089844,6.99234913289547 130.5132598876953,7.124847874045372 C129.78744506835938,8.02728746831417 128.67140197753906,8.55669592320919 127.50616455078125,8.501235947012901 C127.27806091308594,8.576229080557823 126.11459350585938,8.38720129430294 126.428955078125,8.601900085806847 C127.25099182128906,9.070617660880089 128.0523223876953,9.579657539725304 128.902587890625,9.995706543326378 C129.49813842773438,9.678531631827354 130.0761260986328,9.329126343131065 130.6601104736328,8.991325363516808 zM118.96446990966797,9.246344551444054 C119.4022445678711,8.991325363516808 119.84001922607422,8.736305221915245 120.27779388427734,8.481284126639366 C118.93965911865234,8.414779648184776 117.40827941894531,8.607666000723839 116.39698791503906,7.531384453177452 C116.11186981201172,7.212117180228233 115.83845520019531,6.846597656607628 115.44329071044922,7.248530372977257 C114.96995544433594,7.574637398123741 113.5140609741211,7.908811077475548 114.63501739501953,8.306883797049522 C115.61112976074219,8.883499130606651 116.58037567138672,9.474181160330772 117.58061218261719,10.008124336600304 C118.05723571777344,9.784612640738487 118.50651550292969,9.5052699893713 118.96446990966797,9.246344551444054 zM125.38018035888672,12.091858848929405 C125.9474868774414,11.636047348380089 127.32159423828125,11.201767906546593 127.36749267578125,10.712632164359093 C126.08487701416016,9.974547371268272 124.83960723876953,9.152772888541222 123.49772644042969,8.528907760977745 C123.03594207763672,8.353693947196007 122.66152954101562,8.623294815421104 122.28982543945312,8.857431396842003 C121.19065856933594,9.51122473180294 120.06505584716797,10.12446115911007 119.00167083740234,10.835315689444542 C120.39238739013672,11.69529627263546 121.79983520507812,12.529837593436241 123.22095489501953,13.338589653372765 C123.94580841064453,12.932025894522667 124.66128540039062,12.508862480521202 125.38018035888672,12.091858848929405 zM131.07164001464844,13.514615997672081 C131.66018676757812,13.143282875418663 132.2487335205078,12.771927818655968 132.8372802734375,12.400571808218956 C132.8324737548828,11.156818374991417 132.8523406982422,9.912529930472374 132.81829833984375,8.669195160269737 C131.63046264648438,9.332009300589561 130.45948791503906,10.027913078665733 129.30828857421875,10.752535805106163 C129.182373046875,12.035354599356651 129.24623107910156,13.33940313756466 129.27359008789062,14.628684982657433 C129.88104248046875,14.27079389989376 130.4737548828125,13.888019546866417 131.07164001464844,13.514640793204308 zM117.26847839355469,12.731024727225304 C117.32825469970703,11.67083452641964 117.45709991455078,10.46224020421505 116.17853546142578,10.148179039359093 C115.37110900878906,9.77159021794796 114.25194549560547,8.806716904044151 113.62991333007812,8.81639002263546 C113.61052703857422,10.0110072940588 113.62078857421875,11.20585821568966 113.61869049072266,12.400571808218956 C114.81139373779297,13.144886955618858 115.98292541503906,13.925040230154991 117.20137023925781,14.626662239432335 C117.31951141357422,14.010867103934288 117.24227905273438,13.35805033147335 117.26847839355469,12.731024727225304 zM125.80937957763672,16.836034759879112 C126.51483917236328,16.390663132071495 127.22030639648438,15.945291504263878 127.92576599121094,15.49991987645626 C127.92250061035156,14.215868934988976 127.97560119628906,12.929980263113976 127.91757202148438,11.647302612662315 C127.14225769042969,11.869626984000206 126.25550079345703,12.556857094168663 125.43866729736328,12.983742699027061 C124.82704162597656,13.342005714774132 124.21542358398438,13.700271591544151 123.60379028320312,14.05853746831417 C123.61585235595703,15.429577812552452 123.57081604003906,16.803131088614464 123.64839172363281,18.172149643301964 C124.37957000732422,17.744937881827354 125.09130859375,17.284801468253136 125.80937957763672,16.836034759879112 zM122.8521499633789,16.115344032645226 C122.8521499633789,15.429741844534874 122.8521499633789,14.744139656424522 122.8521499633789,14.05853746831417 C121.43595123291016,13.230924591422081 120.02428436279297,12.395455345511436 118.60256958007812,11.577354416251183 C118.52394104003906,12.888403877615929 118.56887817382812,14.204405769705772 118.55702209472656,15.517732605338097 C119.97289276123047,16.4041957706213 121.37410736083984,17.314891800284386 122.80789947509766,18.172149643301964 C122.86368560791016,17.488990768790245 122.84332275390625,16.800363525748253 122.8521499633789,16.115344032645226 zM131.10684204101562,18.871450409293175 C131.68399047851562,18.48711584508419 132.2611541748047,18.10278509557247 132.8383026123047,17.718475326895714 C132.81423950195312,16.499977096915245 132.89776611328125,15.264989838004112 132.77627563476562,14.05993078649044 C131.5760040283203,14.744719490408897 130.41763305664062,15.524359688162804 129.23875427246094,16.255397781729698 C129.26707458496094,17.516149505972862 129.18060302734375,18.791316971182823 129.3108367919922,20.041303619742393 C129.91973876953125,19.667551025748253 130.51010131835938,19.264152511954308 131.10684204101562,18.871450409293175 zM117.2557373046875,18.188333496451378 C117.25104522705078,17.549470886588097 117.24633026123047,16.91058538854122 117.24163055419922,16.271720871329308 C116.04924774169922,15.525708183646202 114.87187957763672,14.75476549565792 113.66158294677734,14.038097366690636 C113.5858383178711,15.262084946036339 113.62901306152344,16.49083898961544 113.61761474609375,17.717010483145714 C114.82051086425781,18.513254150748253 116.00987243652344,19.330610260367393 117.22888946533203,20.101993545889854 C117.27559661865234,19.466014847159386 117.25241088867188,18.825733169913292 117.2557373046875,18.188333496451378 zM125.8398666381836,22.38675306737423 C126.54049682617188,21.921453461050987 127.24110412597656,21.456151947379112 127.94172668457031,20.99083136022091 C127.94009399414062,19.693386062979698 127.96646118164062,18.395381912589073 127.93160247802734,17.098379120230675 C126.50540924072266,17.97775076329708 125.08877563476562,18.873308166861534 123.68258666992188,19.78428266942501 C123.52366638183594,21.03710363805294 123.626708984375,22.32878302037716 123.62647247314453,23.595300659537315 C124.06291198730469,23.86113165318966 125.1788101196289,22.68297766149044 125.8398666381836,22.38675306737423 zM122.8521499633789,21.83134649693966 C122.76741790771484,20.936696991324425 123.21651458740234,19.67745779454708 122.0794677734375,19.330633148550987 C120.93280029296875,18.604360565543175 119.7907485961914,17.870157226920128 118.62899780273438,17.16818617284298 C118.45966339111328,18.396427139639854 118.63676452636719,19.675991043448448 118.50668334960938,20.919256195425987 C119.89984130859375,21.92635916173458 121.32942199707031,22.88914106786251 122.78502655029297,23.803510650992393 C122.90177917480469,23.1627406924963 122.82917022705078,22.48402212560177 122.8521499633789,21.83134649693966 zM117.9798355102539,21.59483526647091 C116.28416442871094,20.46288488805294 114.58848571777344,19.330957397818565 112.892822265625,18.199007019400597 C112.89473724365234,14.705654129385948 112.84647369384766,11.211485847830772 112.90847778320312,7.718807205557823 C113.7575912475586,7.194885239005089 114.66117858886719,6.765397056937218 115.5350341796875,6.284702762961388 C114.97061157226562,4.668964847922325 115.78496551513672,2.7054970115423203 117.42159271240234,2.1007001250982285 C118.79354095458984,1.537783369421959 120.44731903076172,2.0457767099142075 121.32200622558594,3.23083733022213 C121.95732116699219,2.9050118774175644 122.59264373779297,2.5791852325201035 123.22796630859375,2.253336176276207 C123.86669921875,2.5821153968572617 124.50543975830078,2.9108948558568954 125.1441650390625,3.23967407643795 C126.05941009521484,2.154020771384239 127.62747192382812,1.5344576686620712 128.986328125,2.1429056972265244 C130.61741638183594,2.716217741370201 131.50650024414062,4.675290569663048 130.9215545654297,6.2884936183691025 C131.8018341064453,6.78548763692379 132.7589111328125,7.1738648265600204 133.5660400390625,7.780336365103722 C133.60182189941406,11.252970680594444 133.56637573242188,14.726140961050987 133.5631103515625,18.199007019400597 C130.18914794921875,20.431867584586143 126.86984252929688,22.74994657933712 123.44108581542969,24.897907242178917 C122.44406127929688,24.897628769278526 121.5834732055664,23.815067276358604 120.65831756591797,23.37616156041622 C119.76387023925781,22.784828171133995 118.87168884277344,22.19007681310177 117.9798355102539,21.59483526647091 z'; private const GHOST_HEART = 'M125.91386369681868,8.305165958366445 C128.95033202169043,-0.40540639102854037 140.8469835342744,8.305165958366445 125.91386369681868,19.504526138305664 C110.98208663272044,8.305165958366445 122.87795231771452,-0.40540639102854037 125.91386369681868,8.305165958366445 z'; private const GHOST_PLUS = 'M111.36824226379395,8.969108581542969 L118.69175148010254,8.969108581542969 L118.69175148010254,1.6455793380737305 L126.20429420471191,1.6455793380737305 L126.20429420471191,8.969108581542969 L133.52781105041504,8.969108581542969 L133.52781105041504,16.481630325317383 L126.20429420471191,16.481630325317383 L126.20429420471191,23.805158615112305 L118.69175148010254,23.805158615112305 L118.69175148010254,16.481630325317383 L111.36824226379395,16.481630325317383 z'; private bool|\Closure $debug; private string $charset; private string|array|FileLinkFormatter|false $fileLinkFormat; private ?string $projectDir; private string|\Closure $outputBuffer; private $logger; private static string $template = 'views/error.html.php'; /** * @param bool|callable $debug The debugging mode as a boolean or a callable that should return it * @param string|callable $outputBuffer The output buffer as a string or a callable that should return it */ public function __construct(bool|callable $debug = false, string $charset = null, string|FileLinkFormatter $fileLinkFormat = null, string $projectDir = null, string|callable $outputBuffer = '', LoggerInterface $logger = null) { $this->debug = \is_bool($debug) || $debug instanceof \Closure ? $debug : \Closure::fromCallable($debug); $this->charset = $charset ?: (\ini_get('default_charset') ?: 'UTF-8'); $this->fileLinkFormat = $fileLinkFormat ?: (\ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format')); $this->projectDir = $projectDir; $this->outputBuffer = \is_string($outputBuffer) || $outputBuffer instanceof \Closure ? $outputBuffer : \Closure::fromCallable($outputBuffer); $this->logger = $logger; } /** * {@inheritdoc} */ public function render(\Throwable $exception): FlattenException { $headers = ['Content-Type' => 'text/html; charset='.$this->charset]; if (\is_bool($this->debug) ? $this->debug : ($this->debug)($exception)) { $headers['X-Debug-Exception'] = rawurlencode($exception->getMessage()); $headers['X-Debug-Exception-File'] = rawurlencode($exception->getFile()).':'.$exception->getLine(); } $exception = FlattenException::createFromThrowable($exception, null, $headers); return $exception->setAsString($this->renderException($exception)); } /** * Gets the HTML content associated with the given exception. */ public function getBody(FlattenException $exception): string { return $this->renderException($exception, 'views/exception.html.php'); } /** * Gets the stylesheet associated with the given exception. */ public function getStylesheet(): string { if (!$this->debug) { return $this->include('assets/css/error.css'); } return $this->include('assets/css/exception.css'); } public static function isDebug(RequestStack $requestStack, bool $debug): \Closure { return static function () use ($requestStack, $debug): bool { if (!$request = $requestStack->getCurrentRequest()) { return $debug; } return $debug && $request->attributes->getBoolean('showException', true); }; } public static function getAndCleanOutputBuffer(RequestStack $requestStack): \Closure { return static function () use ($requestStack): string { if (!$request = $requestStack->getCurrentRequest()) { return ''; } $startObLevel = $request->headers->get('X-Php-Ob-Level', -1); if (ob_get_level() <= $startObLevel) { return ''; } Response::closeOutputBuffers($startObLevel + 1, true); return ob_get_clean(); }; } private function renderException(FlattenException $exception, string $debugTemplate = 'views/exception_full.html.php'): string { $debug = \is_bool($this->debug) ? $this->debug : ($this->debug)($exception); $statusText = $this->escape($exception->getStatusText()); $statusCode = $this->escape($exception->getStatusCode()); if (!$debug) { return $this->include(self::$template, [ 'statusText' => $statusText, 'statusCode' => $statusCode, ]); } $exceptionMessage = $this->escape($exception->getMessage()); return $this->include($debugTemplate, [ 'exception' => $exception, 'exceptionMessage' => $exceptionMessage, 'statusText' => $statusText, 'statusCode' => $statusCode, 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null, 'currentContent' => \is_string($this->outputBuffer) ? $this->outputBuffer : ($this->outputBuffer)(), ]); } private function formatArgs(array $args): string { $result = []; foreach ($args as $key => $item) { if ('object' === $item[0]) { $formattedValue = sprintf('<em>object</em>(%s)', $this->abbrClass($item[1])); } elseif ('array' === $item[0]) { $formattedValue = sprintf('<em>array</em>(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); } elseif ('null' === $item[0]) { $formattedValue = '<em>null</em>'; } elseif ('boolean' === $item[0]) { $formattedValue = '<em>'.strtolower(var_export($item[1], true)).'</em>'; } elseif ('resource' === $item[0]) { $formattedValue = '<em>resource</em>'; } else { $formattedValue = str_replace("\n", '', $this->escape(var_export($item[1], true))); } $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->escape($key), $formattedValue); } return implode(', ', $result); } private function formatArgsAsText(array $args) { return strip_tags($this->formatArgs($args)); } private function escape(string $string): string { return htmlspecialchars($string, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset); } private function abbrClass(string $class): string { $parts = explode('\\', $class); $short = array_pop($parts); return sprintf('<abbr title="%s">%s</abbr>', $class, $short); } private function getFileRelative(string $file): ?string { $file = str_replace('\\', '/', $file); if (null !== $this->projectDir && str_starts_with($file, $this->projectDir)) { return ltrim(substr($file, \strlen($this->projectDir)), '/'); } return null; } private function getFileLink(string $file, int $line): string|false { if ($fmt = $this->fileLinkFormat) { return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line); } return false; } /** * Formats a file path. * * @param string $file An absolute file path * @param int $line The line number * @param string $text Use this text for the link rather than the file path */ private function formatFile(string $file, int $line, string $text = null): string { $file = trim($file); if (null === $text) { $text = $file; if (null !== $rel = $this->getFileRelative($text)) { $rel = explode('/', $rel, 2); $text = sprintf('<abbr title="%s%2$s">%s</abbr>%s', $this->projectDir, $rel[0], '/'.($rel[1] ?? '')); } } if (0 < $line) { $text .= ' at line '.$line; } if (false !== $link = $this->getFileLink($file, $line)) { return sprintf('<a href="%s" title="Click to open this file" class="file_link">%s</a>', $this->escape($link), $text); } return $text; } /** * Returns an excerpt of a code file around the given line number. * * @param string $file A file path * @param int $line The selected line number * @param int $srcContext The number of displayed lines around or -1 for the whole file */ private function fileExcerpt(string $file, int $line, int $srcContext = 3): string { if (is_file($file) && is_readable($file)) { // highlight_file could throw warnings // see https://bugs.php.net/25725 $code = @highlight_file($file, true); // remove main code/span tags $code = preg_replace('#^<code.*?>\s*<span.*?>(.*)</span>\s*</code>#s', '\\1', $code); // split multiline spans $code = preg_replace_callback('#<span ([^>]++)>((?:[^<]*+<br \/>)++[^<]*+)</span>#', function ($m) { return "<span $m[1]>".str_replace('<br />', "</span><br /><span $m[1]>", $m[2]).'</span>'; }, $code); $content = explode('<br />', $code); $lines = []; if (0 > $srcContext) { $srcContext = \count($content); } for ($i = max($line - $srcContext, 1), $max = min($line + $srcContext, \count($content)); $i <= $max; ++$i) { $lines[] = '<li'.($i == $line ? ' class="selected"' : '').'><code>'.$this->fixCodeMarkup($content[$i - 1]).'</code></li>'; } return '<ol start="'.max($line - $srcContext, 1).'">'.implode("\n", $lines).'</ol>'; } return ''; } private function fixCodeMarkup(string $line) { // </span> ending tag from previous line $opening = strpos($line, '<span'); $closing = strpos($line, '</span>'); if (false !== $closing && (false === $opening || $closing < $opening)) { $line = substr_replace($line, '', $closing, 7); } // missing </span> tag at the end of line $opening = strrpos($line, '<span'); $closing = strrpos($line, '</span>'); if (false !== $opening && (false === $closing || $closing < $opening)) { $line .= '</span>'; } return trim($line); } private function formatFileFromText(string $text) { return preg_replace_callback('/in ("|")?(.+?)\1(?: +(?:on|at))? +line (\d+)/s', function ($match) { return 'in '.$this->formatFile($match[2], $match[3]); }, $text); } private function formatLogMessage(string $message, array $context) { if ($context && str_contains($message, '{')) { $replacements = []; foreach ($context as $key => $val) { if (\is_scalar($val)) { $replacements['{'.$key.'}'] = $val; } } if ($replacements) { $message = strtr($message, $replacements); } } return $this->escape($message); } private function addElementToGhost(): string { if (!isset(self::GHOST_ADDONS[date('m-d')])) { return ''; } return '<path d="'.self::GHOST_ADDONS[date('m-d')].'" fill="#fff" fill-opacity="0.6"></path>'; } private function include(string $name, array $context = []): string { extract($context, \EXTR_SKIP); ob_start(); include is_file(\dirname(__DIR__).'/Resources/'.$name) ? \dirname(__DIR__).'/Resources/'.$name : $name; return trim(ob_get_clean()); } /** * Allows overriding the default non-debug template. * * @param string $template path to the custom template file to render */ public static function setTemplate(string $template): void { self::$template = $template; } } error-handler/ErrorRenderer/SerializerErrorRenderer.php 0000644 00000006521 15025017654 0017424 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\ErrorRenderer; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Serializer\Exception\NotEncodableValueException; use Symfony\Component\Serializer\SerializerInterface; /** * Formats an exception using Serializer for rendering. * * @author Nicolas Grekas <p@tchwork.com> */ class SerializerErrorRenderer implements ErrorRendererInterface { private $serializer; private string|\Closure $format; private $fallbackErrorRenderer; private bool|\Closure $debug; /** * @param string|callable(FlattenException) $format The format as a string or a callable that should return it * formats not supported by Request::getMimeTypes() should be given as mime types * @param bool|callable $debug The debugging mode as a boolean or a callable that should return it */ public function __construct(SerializerInterface $serializer, string|callable $format, ErrorRendererInterface $fallbackErrorRenderer = null, bool|callable $debug = false) { $this->serializer = $serializer; $this->format = \is_string($format) || $format instanceof \Closure ? $format : \Closure::fromCallable($format); $this->fallbackErrorRenderer = $fallbackErrorRenderer ?? new HtmlErrorRenderer(); $this->debug = \is_bool($debug) || $debug instanceof \Closure ? $debug : \Closure::fromCallable($debug); } /** * {@inheritdoc} */ public function render(\Throwable $exception): FlattenException { $headers = []; $debug = \is_bool($this->debug) ? $this->debug : ($this->debug)($exception); if ($debug) { $headers['X-Debug-Exception'] = rawurlencode($exception->getMessage()); $headers['X-Debug-Exception-File'] = rawurlencode($exception->getFile()).':'.$exception->getLine(); } $flattenException = FlattenException::createFromThrowable($exception, null, $headers); try { $format = \is_string($this->format) ? $this->format : ($this->format)($flattenException); $headers = [ 'Content-Type' => Request::getMimeTypes($format)[0] ?? $format, 'Vary' => 'Accept', ]; return $flattenException->setAsString($this->serializer->serialize($flattenException, $format, [ 'exception' => $exception, 'debug' => $debug, ])) ->setHeaders($flattenException->getHeaders() + $headers); } catch (NotEncodableValueException $e) { return $this->fallbackErrorRenderer->render($exception); } } public static function getPreferredFormat(RequestStack $requestStack): \Closure { return static function () use ($requestStack) { if (!$request = $requestStack->getCurrentRequest()) { throw new NotEncodableValueException(); } return $request->getPreferredFormat(); }; } } error-handler/ErrorRenderer/CliErrorRenderer.php 0000644 00000002564 15025017654 0016025 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\ErrorRenderer; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\CliDumper; // Help opcache.preload discover always-needed symbols class_exists(CliDumper::class); /** * @author Nicolas Grekas <p@tchwork.com> */ class CliErrorRenderer implements ErrorRendererInterface { /** * {@inheritdoc} */ public function render(\Throwable $exception): FlattenException { $cloner = new VarCloner(); $dumper = new class() extends CliDumper { protected function supportsColors(): bool { $outputStream = $this->outputStream; $this->outputStream = fopen('php://stdout', 'w'); try { return parent::supportsColors(); } finally { $this->outputStream = $outputStream; } } }; return FlattenException::createFromThrowable($exception) ->setAsString($dumper->dump($cloner->cloneVar($exception), true)); } } error-handler/LICENSE 0000644 00000002051 15025017654 0010320 0 ustar 00 Copyright (c) 2019-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. error-handler/Debug.php 0000644 00000002124 15025017654 0011053 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler; /** * Registers all the debug tools. * * @author Fabien Potencier <fabien@symfony.com> */ class Debug { public static function enable(): ErrorHandler { error_reporting(-1); if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { ini_set('display_errors', 0); } elseif (!filter_var(\ini_get('log_errors'), \FILTER_VALIDATE_BOOLEAN) || \ini_get('error_log')) { // CLI - display errors only if they're not already logged to STDERR ini_set('display_errors', 1); } @ini_set('zend.assertions', 1); ini_set('assert.active', 1); ini_set('assert.warning', 0); ini_set('assert.exception', 1); DebugClassLoader::enable(); return ErrorHandler::register(new ErrorHandler(new BufferingLogger(), true)); } } error-handler/ThrowableUtils.php 0000644 00000001541 15025017654 0012777 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler; use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; /** * @internal */ class ThrowableUtils { public static function getSeverity(SilencedErrorContext|\Throwable $throwable): int { if ($throwable instanceof \ErrorException || $throwable instanceof SilencedErrorContext) { return $throwable->getSeverity(); } if ($throwable instanceof \ParseError) { return \E_PARSE; } if ($throwable instanceof \TypeError) { return \E_RECOVERABLE_ERROR; } return \E_ERROR; } } error-handler/ErrorHandler.php 0000644 00000064671 15025017654 0012433 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\ErrorHandler\Error\FatalError; use Symfony\Component\ErrorHandler\Error\OutOfMemoryError; use Symfony\Component\ErrorHandler\ErrorEnhancer\ClassNotFoundErrorEnhancer; use Symfony\Component\ErrorHandler\ErrorEnhancer\ErrorEnhancerInterface; use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedFunctionErrorEnhancer; use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedMethodErrorEnhancer; use Symfony\Component\ErrorHandler\ErrorRenderer\CliErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; /** * A generic ErrorHandler for the PHP engine. * * Provides five bit fields that control how errors are handled: * - thrownErrors: errors thrown as \ErrorException * - loggedErrors: logged errors, when not @-silenced * - scopedErrors: errors thrown or logged with their local context * - tracedErrors: errors logged with their stack trace * - screamedErrors: never @-silenced errors * * Each error level can be logged by a dedicated PSR-3 logger object. * Screaming only applies to logging. * Throwing takes precedence over logging. * Uncaught exceptions are logged as E_ERROR. * E_DEPRECATED and E_USER_DEPRECATED levels never throw. * E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw. * Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so. * As errors have a performance cost, repeated errors are all logged, so that the developer * can see them and weight them as more important to fix than others of the same level. * * @author Nicolas Grekas <p@tchwork.com> * @author Grégoire Pineau <lyrixx@lyrixx.info> * * @final */ class ErrorHandler { private array $levels = [ \E_DEPRECATED => 'Deprecated', \E_USER_DEPRECATED => 'User Deprecated', \E_NOTICE => 'Notice', \E_USER_NOTICE => 'User Notice', \E_STRICT => 'Runtime Notice', \E_WARNING => 'Warning', \E_USER_WARNING => 'User Warning', \E_COMPILE_WARNING => 'Compile Warning', \E_CORE_WARNING => 'Core Warning', \E_USER_ERROR => 'User Error', \E_RECOVERABLE_ERROR => 'Catchable Fatal Error', \E_COMPILE_ERROR => 'Compile Error', \E_PARSE => 'Parse Error', \E_ERROR => 'Error', \E_CORE_ERROR => 'Core Error', ]; private array $loggers = [ \E_DEPRECATED => [null, LogLevel::INFO], \E_USER_DEPRECATED => [null, LogLevel::INFO], \E_NOTICE => [null, LogLevel::WARNING], \E_USER_NOTICE => [null, LogLevel::WARNING], \E_STRICT => [null, LogLevel::WARNING], \E_WARNING => [null, LogLevel::WARNING], \E_USER_WARNING => [null, LogLevel::WARNING], \E_COMPILE_WARNING => [null, LogLevel::WARNING], \E_CORE_WARNING => [null, LogLevel::WARNING], \E_USER_ERROR => [null, LogLevel::CRITICAL], \E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL], \E_COMPILE_ERROR => [null, LogLevel::CRITICAL], \E_PARSE => [null, LogLevel::CRITICAL], \E_ERROR => [null, LogLevel::CRITICAL], \E_CORE_ERROR => [null, LogLevel::CRITICAL], ]; private int $thrownErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED private int $scopedErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED private int $tracedErrors = 0x77FB; // E_ALL - E_STRICT - E_PARSE private int $screamedErrors = 0x55; // E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE private int $loggedErrors = 0; private \Closure $configureException; private bool $debug; private bool $isRecursive = false; private bool $isRoot = false; private $exceptionHandler; private $bootstrappingLogger = null; private static ?string $reservedMemory = null; private static array $silencedErrorCache = []; private static int $silencedErrorCount = 0; private static int $exitCode = 0; /** * Registers the error handler. */ public static function register(self $handler = null, bool $replace = true): self { if (null === self::$reservedMemory) { self::$reservedMemory = str_repeat('x', 32768); register_shutdown_function(__CLASS__.'::handleFatalError'); } if ($handlerIsNew = null === $handler) { $handler = new static(); } if (null === $prev = set_error_handler([$handler, 'handleError'])) { restore_error_handler(); // Specifying the error types earlier would expose us to https://bugs.php.net/63206 set_error_handler([$handler, 'handleError'], $handler->thrownErrors | $handler->loggedErrors); $handler->isRoot = true; } if ($handlerIsNew && \is_array($prev) && $prev[0] instanceof self) { $handler = $prev[0]; $replace = false; } if (!$replace && $prev) { restore_error_handler(); $handlerIsRegistered = \is_array($prev) && $handler === $prev[0]; } else { $handlerIsRegistered = true; } if (\is_array($prev = set_exception_handler([$handler, 'handleException'])) && $prev[0] instanceof self) { restore_exception_handler(); if (!$handlerIsRegistered) { $handler = $prev[0]; } elseif ($handler !== $prev[0] && $replace) { set_exception_handler([$handler, 'handleException']); $p = $prev[0]->setExceptionHandler(null); $handler->setExceptionHandler($p); $prev[0]->setExceptionHandler($p); } } else { $handler->setExceptionHandler($prev ?? [$handler, 'renderException']); } $handler->throwAt(\E_ALL & $handler->thrownErrors, true); return $handler; } /** * Calls a function and turns any PHP error into \ErrorException. * * @throws \ErrorException When $function(...$arguments) triggers a PHP error */ public static function call(callable $function, mixed ...$arguments): mixed { set_error_handler(static function (int $type, string $message, string $file, int $line) { if (__FILE__ === $file) { $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 3); $file = $trace[2]['file'] ?? $file; $line = $trace[2]['line'] ?? $line; } throw new \ErrorException($message, 0, $type, $file, $line); }); try { return $function(...$arguments); } finally { restore_error_handler(); } } public function __construct(BufferingLogger $bootstrappingLogger = null, bool $debug = false) { if ($bootstrappingLogger) { $this->bootstrappingLogger = $bootstrappingLogger; $this->setDefaultLogger($bootstrappingLogger); } $traceReflector = new \ReflectionProperty(\Exception::class, 'trace'); $traceReflector->setAccessible(true); $this->configureException = \Closure::bind(static function ($e, $trace, $file = null, $line = null) use ($traceReflector) { $traceReflector->setValue($e, $trace); $e->file = $file ?? $e->file; $e->line = $line ?? $e->line; }, null, new class() extends \Exception { }); $this->debug = $debug; } /** * Sets a logger to non assigned errors levels. * * @param LoggerInterface $logger A PSR-3 logger to put as default for the given levels * @param array|int|null $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants * @param bool $replace Whether to replace or not any existing logger */ public function setDefaultLogger(LoggerInterface $logger, array|int|null $levels = \E_ALL, bool $replace = false): void { $loggers = []; if (\is_array($levels)) { foreach ($levels as $type => $logLevel) { if (empty($this->loggers[$type][0]) || $replace || $this->loggers[$type][0] === $this->bootstrappingLogger) { $loggers[$type] = [$logger, $logLevel]; } } } else { if (null === $levels) { $levels = \E_ALL; } foreach ($this->loggers as $type => $log) { if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) { $log[0] = $logger; $loggers[$type] = $log; } } } $this->setLoggers($loggers); } /** * Sets a logger for each error level. * * @param array $loggers Error levels to [LoggerInterface|null, LogLevel::*] map * * @throws \InvalidArgumentException */ public function setLoggers(array $loggers): array { $prevLogged = $this->loggedErrors; $prev = $this->loggers; $flush = []; foreach ($loggers as $type => $log) { if (!isset($prev[$type])) { throw new \InvalidArgumentException('Unknown error type: '.$type); } if (!\is_array($log)) { $log = [$log]; } elseif (!\array_key_exists(0, $log)) { throw new \InvalidArgumentException('No logger provided.'); } if (null === $log[0]) { $this->loggedErrors &= ~$type; } elseif ($log[0] instanceof LoggerInterface) { $this->loggedErrors |= $type; } else { throw new \InvalidArgumentException('Invalid logger provided.'); } $this->loggers[$type] = $log + $prev[$type]; if ($this->bootstrappingLogger && $prev[$type][0] === $this->bootstrappingLogger) { $flush[$type] = $type; } } $this->reRegister($prevLogged | $this->thrownErrors); if ($flush) { foreach ($this->bootstrappingLogger->cleanLogs() as $log) { $type = ThrowableUtils::getSeverity($log[2]['exception']); if (!isset($flush[$type])) { $this->bootstrappingLogger->log($log[0], $log[1], $log[2]); } elseif ($this->loggers[$type][0]) { $this->loggers[$type][0]->log($this->loggers[$type][1], $log[1], $log[2]); } } } return $prev; } public function setExceptionHandler(?callable $handler): ?callable { $prev = $this->exceptionHandler; $this->exceptionHandler = $handler; return $prev; } /** * Sets the PHP error levels that throw an exception when a PHP error occurs. * * @param int $levels A bit field of E_* constants for thrown errors * @param bool $replace Replace or amend the previous value */ public function throwAt(int $levels, bool $replace = false): int { $prev = $this->thrownErrors; $this->thrownErrors = ($levels | \E_RECOVERABLE_ERROR | \E_USER_ERROR) & ~\E_USER_DEPRECATED & ~\E_DEPRECATED; if (!$replace) { $this->thrownErrors |= $prev; } $this->reRegister($prev | $this->loggedErrors); return $prev; } /** * Sets the PHP error levels for which local variables are preserved. * * @param int $levels A bit field of E_* constants for scoped errors * @param bool $replace Replace or amend the previous value */ public function scopeAt(int $levels, bool $replace = false): int { $prev = $this->scopedErrors; $this->scopedErrors = $levels; if (!$replace) { $this->scopedErrors |= $prev; } return $prev; } /** * Sets the PHP error levels for which the stack trace is preserved. * * @param int $levels A bit field of E_* constants for traced errors * @param bool $replace Replace or amend the previous value */ public function traceAt(int $levels, bool $replace = false): int { $prev = $this->tracedErrors; $this->tracedErrors = $levels; if (!$replace) { $this->tracedErrors |= $prev; } return $prev; } /** * Sets the error levels where the @-operator is ignored. * * @param int $levels A bit field of E_* constants for screamed errors * @param bool $replace Replace or amend the previous value */ public function screamAt(int $levels, bool $replace = false): int { $prev = $this->screamedErrors; $this->screamedErrors = $levels; if (!$replace) { $this->screamedErrors |= $prev; } return $prev; } /** * Re-registers as a PHP error handler if levels changed. */ private function reRegister(int $prev): void { if ($prev !== ($this->thrownErrors | $this->loggedErrors)) { $handler = set_error_handler('is_int'); $handler = \is_array($handler) ? $handler[0] : null; restore_error_handler(); if ($handler === $this) { restore_error_handler(); if ($this->isRoot) { set_error_handler([$this, 'handleError'], $this->thrownErrors | $this->loggedErrors); } else { set_error_handler([$this, 'handleError']); } } } } /** * Handles errors by filtering then logging them according to the configured bit fields. * * @return bool Returns false when no handling happens so that the PHP engine can handle the error itself * * @throws \ErrorException When $this->thrownErrors requests so * * @internal */ public function handleError(int $type, string $message, string $file, int $line): bool { if (\E_WARNING === $type && '"' === $message[0] && str_contains($message, '" targeting switch is equivalent to "break')) { $type = \E_DEPRECATED; } // Level is the current error reporting level to manage silent error. $level = error_reporting(); $silenced = 0 === ($level & $type); // Strong errors are not authorized to be silenced. $level |= \E_RECOVERABLE_ERROR | \E_USER_ERROR | \E_DEPRECATED | \E_USER_DEPRECATED; $log = $this->loggedErrors & $type; $throw = $this->thrownErrors & $type & $level; $type &= $level | $this->screamedErrors; // Never throw on warnings triggered by assert() if (\E_WARNING === $type && 'a' === $message[0] && 0 === strncmp($message, 'assert(): ', 10)) { $throw = 0; } if (!$type || (!$log && !$throw)) { return false; } $logMessage = $this->levels[$type].': '.$message; if (!$throw && !($type & $level)) { if (!isset(self::$silencedErrorCache[$id = $file.':'.$line])) { $lightTrace = $this->tracedErrors & $type ? $this->cleanTrace(debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 5), $type, $file, $line, false) : []; $errorAsException = new SilencedErrorContext($type, $file, $line, isset($lightTrace[1]) ? [$lightTrace[0]] : $lightTrace); } elseif (isset(self::$silencedErrorCache[$id][$message])) { $lightTrace = null; $errorAsException = self::$silencedErrorCache[$id][$message]; ++$errorAsException->count; } else { $lightTrace = []; $errorAsException = null; } if (100 < ++self::$silencedErrorCount) { self::$silencedErrorCache = $lightTrace = []; self::$silencedErrorCount = 1; } if ($errorAsException) { self::$silencedErrorCache[$id][$message] = $errorAsException; } if (null === $lightTrace) { return true; } } else { if (str_contains($message, '@anonymous')) { $backtrace = debug_backtrace(false, 5); for ($i = 1; isset($backtrace[$i]); ++$i) { if (isset($backtrace[$i]['function'], $backtrace[$i]['args'][0]) && ('trigger_error' === $backtrace[$i]['function'] || 'user_error' === $backtrace[$i]['function']) ) { if ($backtrace[$i]['args'][0] !== $message) { $message = $this->parseAnonymousClass($backtrace[$i]['args'][0]); $logMessage = $this->levels[$type].': '.$message; } break; } } } $errorAsException = new \ErrorException($logMessage, 0, $type, $file, $line); if ($throw || $this->tracedErrors & $type) { $backtrace = $errorAsException->getTrace(); $lightTrace = $this->cleanTrace($backtrace, $type, $file, $line, $throw); ($this->configureException)($errorAsException, $lightTrace, $file, $line); } else { ($this->configureException)($errorAsException, []); $backtrace = []; } } if ($throw) { throw $errorAsException; } if ($this->isRecursive) { $log = 0; } else { try { $this->isRecursive = true; $level = ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG; $this->loggers[$type][0]->log($level, $logMessage, $errorAsException ? ['exception' => $errorAsException] : []); } finally { $this->isRecursive = false; } } return !$silenced && $type && $log; } /** * Handles an exception by logging then forwarding it to another handler. * * @internal */ public function handleException(\Throwable $exception) { $handlerException = null; if (!$exception instanceof FatalError) { self::$exitCode = 255; $type = ThrowableUtils::getSeverity($exception); } else { $type = $exception->getError()['type']; } if ($this->loggedErrors & $type) { if (str_contains($message = $exception->getMessage(), "@anonymous\0")) { $message = $this->parseAnonymousClass($message); } if ($exception instanceof FatalError) { $message = 'Fatal '.$message; } elseif ($exception instanceof \Error) { $message = 'Uncaught Error: '.$message; } elseif ($exception instanceof \ErrorException) { $message = 'Uncaught '.$message; } else { $message = 'Uncaught Exception: '.$message; } try { $this->loggers[$type][0]->log($this->loggers[$type][1], $message, ['exception' => $exception]); } catch (\Throwable $handlerException) { } } if (!$exception instanceof OutOfMemoryError) { foreach ($this->getErrorEnhancers() as $errorEnhancer) { if ($e = $errorEnhancer->enhance($exception)) { $exception = $e; break; } } } $exceptionHandler = $this->exceptionHandler; $this->exceptionHandler = [$this, 'renderException']; if (null === $exceptionHandler || $exceptionHandler === $this->exceptionHandler) { $this->exceptionHandler = null; } try { if (null !== $exceptionHandler) { return $exceptionHandler($exception); } $handlerException = $handlerException ?: $exception; } catch (\Throwable $handlerException) { } if ($exception === $handlerException && null === $this->exceptionHandler) { self::$reservedMemory = null; // Disable the fatal error handler throw $exception; // Give back $exception to the native handler } $loggedErrors = $this->loggedErrors; if ($exception === $handlerException) { $this->loggedErrors &= ~$type; } try { $this->handleException($handlerException); } finally { $this->loggedErrors = $loggedErrors; } } /** * Shutdown registered function for handling PHP fatal errors. * * @param array|null $error An array as returned by error_get_last() * * @internal */ public static function handleFatalError(array $error = null): void { if (null === self::$reservedMemory) { return; } $handler = self::$reservedMemory = null; $handlers = []; $previousHandler = null; $sameHandlerLimit = 10; while (!\is_array($handler) || !$handler[0] instanceof self) { $handler = set_exception_handler('is_int'); restore_exception_handler(); if (!$handler) { break; } restore_exception_handler(); if ($handler !== $previousHandler) { array_unshift($handlers, $handler); $previousHandler = $handler; } elseif (0 === --$sameHandlerLimit) { $handler = null; break; } } foreach ($handlers as $h) { set_exception_handler($h); } if (!$handler) { return; } if ($handler !== $h) { $handler[0]->setExceptionHandler($h); } $handler = $handler[0]; $handlers = []; if ($exit = null === $error) { $error = error_get_last(); } if ($error && $error['type'] &= \E_PARSE | \E_ERROR | \E_CORE_ERROR | \E_COMPILE_ERROR) { // Let's not throw anymore but keep logging $handler->throwAt(0, true); $trace = $error['backtrace'] ?? null; if (str_starts_with($error['message'], 'Allowed memory') || str_starts_with($error['message'], 'Out of memory')) { $fatalError = new OutOfMemoryError($handler->levels[$error['type']].': '.$error['message'], 0, $error, 2, false, $trace); } else { $fatalError = new FatalError($handler->levels[$error['type']].': '.$error['message'], 0, $error, 2, true, $trace); } } else { $fatalError = null; } try { if (null !== $fatalError) { self::$exitCode = 255; $handler->handleException($fatalError); } } catch (FatalError $e) { // Ignore this re-throw } if ($exit && self::$exitCode) { $exitCode = self::$exitCode; register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); }); } } /** * Renders the given exception. * * As this method is mainly called during boot where nothing is yet available, * the output is always either HTML or CLI depending where PHP runs. */ private function renderException(\Throwable $exception): void { $renderer = \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? new CliErrorRenderer() : new HtmlErrorRenderer($this->debug); $exception = $renderer->render($exception); if (!headers_sent()) { http_response_code($exception->getStatusCode()); foreach ($exception->getHeaders() as $name => $value) { header($name.': '.$value, false); } } echo $exception->getAsString(); } /** * Override this method if you want to define more error enhancers. * * @return ErrorEnhancerInterface[] */ protected function getErrorEnhancers(): iterable { return [ new UndefinedFunctionErrorEnhancer(), new UndefinedMethodErrorEnhancer(), new ClassNotFoundErrorEnhancer(), ]; } /** * Cleans the trace by removing function arguments and the frames added by the error handler and DebugClassLoader. */ private function cleanTrace(array $backtrace, int $type, string &$file, int &$line, bool $throw): array { $lightTrace = $backtrace; for ($i = 0; isset($backtrace[$i]); ++$i) { if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { $lightTrace = \array_slice($lightTrace, 1 + $i); break; } } if (\E_USER_DEPRECATED === $type) { for ($i = 0; isset($lightTrace[$i]); ++$i) { if (!isset($lightTrace[$i]['file'], $lightTrace[$i]['line'], $lightTrace[$i]['function'])) { continue; } if (!isset($lightTrace[$i]['class']) && 'trigger_deprecation' === $lightTrace[$i]['function']) { $file = $lightTrace[$i]['file']; $line = $lightTrace[$i]['line']; $lightTrace = \array_slice($lightTrace, 1 + $i); break; } } } if (class_exists(DebugClassLoader::class, false)) { for ($i = \count($lightTrace) - 2; 0 < $i; --$i) { if (DebugClassLoader::class === ($lightTrace[$i]['class'] ?? null)) { array_splice($lightTrace, --$i, 2); } } } if (!($throw || $this->scopedErrors & $type)) { for ($i = 0; isset($lightTrace[$i]); ++$i) { unset($lightTrace[$i]['args'], $lightTrace[$i]['object']); } } return $lightTrace; } /** * Parse the error message by removing the anonymous class notation * and using the parent class instead if possible. */ private function parseAnonymousClass(string $message): string { return preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', static function ($m) { return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0]; }, $message); } } error-handler/DebugClassLoader.php 0000644 00000133377 15025017654 0013207 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler; use Composer\InstalledVersions; use Doctrine\Common\Persistence\Proxy as LegacyProxy; use Doctrine\Persistence\Proxy; use Mockery\MockInterface; use Phake\IMock; use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation; use PHPUnit\Framework\MockObject\MockObject; use Prophecy\Prophecy\ProphecySubjectInterface; use ProxyManager\Proxy\ProxyInterface; use Symfony\Component\ErrorHandler\Internal\TentativeTypes; /** * Autoloader checking if the class is really defined in the file found. * * The ClassLoader will wrap all registered autoloaders * and will throw an exception if a file is found but does * not declare the class. * * It can also patch classes to turn docblocks into actual return types. * This behavior is controlled by the SYMFONY_PATCH_TYPE_DECLARATIONS env var, * which is a url-encoded array with the follow parameters: * - "force": any value enables deprecation notices - can be any of: * - "phpdoc" to patch only docblock annotations * - "2" to add all possible return types * - "1" to add return types but only to tests/final/internal/private methods * - "php": the target version of PHP - e.g. "7.1" doesn't generate "object" types * - "deprecations": "1" to trigger a deprecation notice when a child class misses a * return type while the parent declares an "@return" annotation * * Note that patching doesn't care about any coding style so you'd better to run * php-cs-fixer after, with rules "phpdoc_trim_consecutive_blank_line_separation" * and "no_superfluous_phpdoc_tags" enabled typically. * * @author Fabien Potencier <fabien@symfony.com> * @author Christophe Coevoet <stof@notk.org> * @author Nicolas Grekas <p@tchwork.com> * @author Guilhem Niot <guilhem.niot@gmail.com> */ class DebugClassLoader { private const SPECIAL_RETURN_TYPES = [ 'void' => 'void', 'null' => 'null', 'resource' => 'resource', 'boolean' => 'bool', 'true' => 'true', 'false' => 'false', 'integer' => 'int', 'array' => 'array', 'bool' => 'bool', 'callable' => 'callable', 'float' => 'float', 'int' => 'int', 'iterable' => 'iterable', 'object' => 'object', 'string' => 'string', 'self' => 'self', 'parent' => 'parent', 'mixed' => 'mixed', 'static' => 'static', '$this' => 'static', 'list' => 'array', 'class-string' => 'string', 'never' => 'never', ]; private const BUILTIN_RETURN_TYPES = [ 'void' => true, 'array' => true, 'false' => true, 'bool' => true, 'callable' => true, 'float' => true, 'int' => true, 'iterable' => true, 'object' => true, 'string' => true, 'self' => true, 'parent' => true, 'mixed' => true, 'static' => true, 'null' => true, 'true' => true, 'never' => true, ]; private const MAGIC_METHODS = [ '__isset' => 'bool', '__sleep' => 'array', '__toString' => 'string', '__debugInfo' => 'array', '__serialize' => 'array', ]; /** * @var callable */ private $classLoader; private bool $isFinder; private array $loaded = []; private array $patchTypes = []; private static int $caseCheck; private static array $checkedClasses = []; private static array $final = []; private static array $finalMethods = []; private static array $deprecated = []; private static array $internal = []; private static array $internalMethods = []; private static array $annotatedParameters = []; private static array $darwinCache = ['/' => ['/', []]]; private static array $method = []; private static array $returnTypes = []; private static array $methodTraits = []; private static array $fileOffsets = []; public function __construct(callable $classLoader) { $this->classLoader = $classLoader; $this->isFinder = \is_array($classLoader) && method_exists($classLoader[0], 'findFile'); parse_str(getenv('SYMFONY_PATCH_TYPE_DECLARATIONS') ?: '', $this->patchTypes); $this->patchTypes += [ 'force' => null, 'php' => \PHP_MAJOR_VERSION.'.'.\PHP_MINOR_VERSION, 'deprecations' => true, ]; if ('phpdoc' === $this->patchTypes['force']) { $this->patchTypes['force'] = 'docblock'; } if (!isset(self::$caseCheck)) { $file = is_file(__FILE__) ? __FILE__ : rtrim(realpath('.'), \DIRECTORY_SEPARATOR); $i = strrpos($file, \DIRECTORY_SEPARATOR); $dir = substr($file, 0, 1 + $i); $file = substr($file, 1 + $i); $test = strtoupper($file) === $file ? strtolower($file) : strtoupper($file); $test = realpath($dir.$test); if (false === $test || false === $i) { // filesystem is case sensitive self::$caseCheck = 0; } elseif (str_ends_with($test, $file)) { // filesystem is case insensitive and realpath() normalizes the case of characters self::$caseCheck = 1; } elseif ('Darwin' === \PHP_OS_FAMILY) { // on MacOSX, HFS+ is case insensitive but realpath() doesn't normalize the case of characters self::$caseCheck = 2; } else { // filesystem case checks failed, fallback to disabling them self::$caseCheck = 0; } } } public function getClassLoader(): callable { return $this->classLoader; } /** * Wraps all autoloaders. */ public static function enable(): void { // Ensures we don't hit https://bugs.php.net/42098 class_exists(\Symfony\Component\ErrorHandler\ErrorHandler::class); class_exists(\Psr\Log\LogLevel::class); if (!\is_array($functions = spl_autoload_functions())) { return; } foreach ($functions as $function) { spl_autoload_unregister($function); } foreach ($functions as $function) { if (!\is_array($function) || !$function[0] instanceof self) { $function = [new static($function), 'loadClass']; } spl_autoload_register($function); } } /** * Disables the wrapping. */ public static function disable(): void { if (!\is_array($functions = spl_autoload_functions())) { return; } foreach ($functions as $function) { spl_autoload_unregister($function); } foreach ($functions as $function) { if (\is_array($function) && $function[0] instanceof self) { $function = $function[0]->getClassLoader(); } spl_autoload_register($function); } } public static function checkClasses(): bool { if (!\is_array($functions = spl_autoload_functions())) { return false; } $loader = null; foreach ($functions as $function) { if (\is_array($function) && $function[0] instanceof self) { $loader = $function[0]; break; } } if (null === $loader) { return false; } static $offsets = [ 'get_declared_interfaces' => 0, 'get_declared_traits' => 0, 'get_declared_classes' => 0, ]; foreach ($offsets as $getSymbols => $i) { $symbols = $getSymbols(); for (; $i < \count($symbols); ++$i) { if (!is_subclass_of($symbols[$i], MockObject::class) && !is_subclass_of($symbols[$i], ProphecySubjectInterface::class) && !is_subclass_of($symbols[$i], Proxy::class) && !is_subclass_of($symbols[$i], ProxyInterface::class) && !is_subclass_of($symbols[$i], LegacyProxy::class) && !is_subclass_of($symbols[$i], MockInterface::class) && !is_subclass_of($symbols[$i], IMock::class) ) { $loader->checkClass($symbols[$i]); } } $offsets[$getSymbols] = $i; } return true; } public function findFile(string $class): ?string { return $this->isFinder ? ($this->classLoader[0]->findFile($class) ?: null) : null; } /** * Loads the given class or interface. * * @throws \RuntimeException */ public function loadClass(string $class): void { $e = error_reporting(error_reporting() | \E_PARSE | \E_ERROR | \E_CORE_ERROR | \E_COMPILE_ERROR); try { if ($this->isFinder && !isset($this->loaded[$class])) { $this->loaded[$class] = true; if (!$file = $this->classLoader[0]->findFile($class) ?: '') { // no-op } elseif (\function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file)) { include $file; return; } elseif (false === include $file) { return; } } else { ($this->classLoader)($class); $file = ''; } } finally { error_reporting($e); } $this->checkClass($class, $file); } private function checkClass(string $class, string $file = null): void { $exists = null === $file || class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false); if (null !== $file && $class && '\\' === $class[0]) { $class = substr($class, 1); } if ($exists) { if (isset(self::$checkedClasses[$class])) { return; } self::$checkedClasses[$class] = true; $refl = new \ReflectionClass($class); if (null === $file && $refl->isInternal()) { return; } $name = $refl->getName(); if ($name !== $class && 0 === strcasecmp($name, $class)) { throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name)); } $deprecations = $this->checkAnnotations($refl, $name); foreach ($deprecations as $message) { @trigger_error($message, \E_USER_DEPRECATED); } } if (!$file) { return; } if (!$exists) { if (str_contains($class, '/')) { throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class)); } throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); } if (self::$caseCheck && $message = $this->checkCase($refl, $file, $class)) { throw new \RuntimeException(sprintf('Case mismatch between class and real file names: "%s" vs "%s" in "%s".', $message[0], $message[1], $message[2])); } } public function checkAnnotations(\ReflectionClass $refl, string $class): array { if ( 'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV7' === $class || 'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV6' === $class ) { return []; } $deprecations = []; $className = str_contains($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class; // Don't trigger deprecations for classes in the same vendor if ($class !== $className) { $vendor = preg_match('/^namespace ([^;\\\\\s]++)[;\\\\]/m', @file_get_contents($refl->getFileName()), $vendor) ? $vendor[1].'\\' : ''; $vendorLen = \strlen($vendor); } elseif (2 > $vendorLen = 1 + (strpos($class, '\\') ?: strpos($class, '_'))) { $vendorLen = 0; $vendor = ''; } else { $vendor = str_replace('_', '\\', substr($class, 0, $vendorLen)); } $parent = get_parent_class($class) ?: null; self::$returnTypes[$class] = []; $classIsTemplate = false; // Detect annotations on the class if ($doc = $this->parsePhpDoc($refl)) { $classIsTemplate = isset($doc['template']); foreach (['final', 'deprecated', 'internal'] as $annotation) { if (null !== $description = $doc[$annotation][0] ?? null) { self::${$annotation}[$class] = '' !== $description ? ' '.$description.(preg_match('/[.!]$/', $description) ? '' : '.') : '.'; } } if ($refl->isInterface() && isset($doc['method'])) { foreach ($doc['method'] as $name => [$static, $returnType, $signature, $description]) { self::$method[$class][] = [$class, $static, $returnType, $name.$signature, $description]; if ('' !== $returnType) { $this->setReturnType($returnType, $refl->name, $name, $refl->getFileName(), $parent); } } } } $parentAndOwnInterfaces = $this->getOwnInterfaces($class, $parent); if ($parent) { $parentAndOwnInterfaces[$parent] = $parent; if (!isset(self::$checkedClasses[$parent])) { $this->checkClass($parent); } if (isset(self::$final[$parent])) { $deprecations[] = sprintf('The "%s" class is considered final%s It may change without further notice as of its next major version. You should not extend it from "%s".', $parent, self::$final[$parent], $className); } } // Detect if the parent is annotated foreach ($parentAndOwnInterfaces + class_uses($class, false) as $use) { if (!isset(self::$checkedClasses[$use])) { $this->checkClass($use); } if (isset(self::$deprecated[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && !isset(self::$deprecated[$class])) { $type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait'); $verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses'); $deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s', $className, $type, $verb, $use, self::$deprecated[$use]); } if (isset(self::$internal[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen)) { $deprecations[] = sprintf('The "%s" %s is considered internal%s It may change without further notice. You should not use it from "%s".', $use, class_exists($use, false) ? 'class' : (interface_exists($use, false) ? 'interface' : 'trait'), self::$internal[$use], $className); } if (isset(self::$method[$use])) { if ($refl->isAbstract()) { if (isset(self::$method[$class])) { self::$method[$class] = array_merge(self::$method[$class], self::$method[$use]); } else { self::$method[$class] = self::$method[$use]; } } elseif (!$refl->isInterface()) { if (!strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && str_starts_with($className, 'Symfony\\') && (!class_exists(InstalledVersions::class) || 'symfony/symfony' !== InstalledVersions::getRootPackage()['name']) ) { // skip "same vendor" @method deprecations for Symfony\* classes unless symfony/symfony is being tested continue; } $hasCall = $refl->hasMethod('__call'); $hasStaticCall = $refl->hasMethod('__callStatic'); foreach (self::$method[$use] as [$interface, $static, $returnType, $name, $description]) { if ($static ? $hasStaticCall : $hasCall) { continue; } $realName = substr($name, 0, strpos($name, '(')); if (!$refl->hasMethod($realName) || !($methodRefl = $refl->getMethod($realName))->isPublic() || ($static && !$methodRefl->isStatic()) || (!$static && $methodRefl->isStatic())) { $deprecations[] = sprintf('Class "%s" should implement method "%s::%s%s"%s', $className, ($static ? 'static ' : '').$interface, $name, $returnType ? ': '.$returnType : '', null === $description ? '.' : ': '.$description); } } } } } if (trait_exists($class)) { $file = $refl->getFileName(); foreach ($refl->getMethods() as $method) { if ($method->getFileName() === $file) { self::$methodTraits[$file][$method->getStartLine()] = $class; } } return $deprecations; } // Inherit @final, @internal, @param and @return annotations for methods self::$finalMethods[$class] = []; self::$internalMethods[$class] = []; self::$annotatedParameters[$class] = []; foreach ($parentAndOwnInterfaces as $use) { foreach (['finalMethods', 'internalMethods', 'annotatedParameters', 'returnTypes'] as $property) { if (isset(self::${$property}[$use])) { self::${$property}[$class] = self::${$property}[$class] ? self::${$property}[$use] + self::${$property}[$class] : self::${$property}[$use]; } } if (null !== (TentativeTypes::RETURN_TYPES[$use] ?? null)) { foreach (TentativeTypes::RETURN_TYPES[$use] as $method => $returnType) { $returnType = explode('|', $returnType); foreach ($returnType as $i => $t) { if ('?' !== $t && !isset(self::BUILTIN_RETURN_TYPES[$t])) { $returnType[$i] = '\\'.$t; } } $returnType = implode('|', $returnType); self::$returnTypes[$class] += [$method => [$returnType, 0 === strpos($returnType, '?') ? substr($returnType, 1).'|null' : $returnType, $use, '']]; } } } foreach ($refl->getMethods() as $method) { if ($method->class !== $class) { continue; } if (null === $ns = self::$methodTraits[$method->getFileName()][$method->getStartLine()] ?? null) { $ns = $vendor; $len = $vendorLen; } elseif (2 > $len = 1 + (strpos($ns, '\\') ?: strpos($ns, '_'))) { $len = 0; $ns = ''; } else { $ns = str_replace('_', '\\', substr($ns, 0, $len)); } if ($parent && isset(self::$finalMethods[$parent][$method->name])) { [$declaringClass, $message] = self::$finalMethods[$parent][$method->name]; $deprecations[] = sprintf('The "%s::%s()" method is considered final%s It may change without further notice as of its next major version. You should not extend it from "%s".', $declaringClass, $method->name, $message, $className); } if (isset(self::$internalMethods[$class][$method->name])) { [$declaringClass, $message] = self::$internalMethods[$class][$method->name]; if (strncmp($ns, $declaringClass, $len)) { $deprecations[] = sprintf('The "%s::%s()" method is considered internal%s It may change without further notice. You should not extend it from "%s".', $declaringClass, $method->name, $message, $className); } } // To read method annotations $doc = $this->parsePhpDoc($method); if (($classIsTemplate || isset($doc['template'])) && $method->hasReturnType()) { unset($doc['return']); } if (isset(self::$annotatedParameters[$class][$method->name])) { $definedParameters = []; foreach ($method->getParameters() as $parameter) { $definedParameters[$parameter->name] = true; } foreach (self::$annotatedParameters[$class][$method->name] as $parameterName => $deprecation) { if (!isset($definedParameters[$parameterName]) && !isset($doc['param'][$parameterName])) { $deprecations[] = sprintf($deprecation, $className); } } } $forcePatchTypes = $this->patchTypes['force']; if ($canAddReturnType = null !== $forcePatchTypes && !str_contains($method->getFileName(), \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR)) { if ('void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) { $this->patchTypes['force'] = $forcePatchTypes ?: 'docblock'; } $canAddReturnType = 2 === (int) $forcePatchTypes || false !== stripos($method->getFileName(), \DIRECTORY_SEPARATOR.'Tests'.\DIRECTORY_SEPARATOR) || $refl->isFinal() || $method->isFinal() || $method->isPrivate() || ('.' === (self::$internal[$class] ?? null) && !$refl->isAbstract()) || '.' === (self::$final[$class] ?? null) || '' === ($doc['final'][0] ?? null) || '' === ($doc['internal'][0] ?? null) ; } if (null !== ($returnType = self::$returnTypes[$class][$method->name] ?? null) && 'docblock' === $this->patchTypes['force'] && !$method->hasReturnType() && isset(TentativeTypes::RETURN_TYPES[$returnType[2]][$method->name])) { $this->patchReturnTypeWillChange($method); } if (null !== ($returnType ?? $returnType = self::MAGIC_METHODS[$method->name] ?? null) && !$method->hasReturnType() && !isset($doc['return'])) { [$normalizedType, $returnType, $declaringClass, $declaringFile] = \is_string($returnType) ? [$returnType, $returnType, '', ''] : $returnType; if ($canAddReturnType && 'docblock' !== $this->patchTypes['force']) { $this->patchMethod($method, $returnType, $declaringFile, $normalizedType); } if (!isset($doc['deprecated']) && strncmp($ns, $declaringClass, $len)) { if ('docblock' === $this->patchTypes['force']) { $this->patchMethod($method, $returnType, $declaringFile, $normalizedType); } elseif ('' !== $declaringClass && $this->patchTypes['deprecations']) { $deprecations[] = sprintf('Method "%s::%s()" might add "%s" as a native return type declaration in the future. Do the same in %s "%s" now to avoid errors or add an explicit @return annotation to suppress this message.', $declaringClass, $method->name, $normalizedType, interface_exists($declaringClass) ? 'implementation' : 'child class', $className); } } } if (!$doc) { $this->patchTypes['force'] = $forcePatchTypes; continue; } if (isset($doc['return']) || 'void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) { $this->setReturnType($doc['return'] ?? self::MAGIC_METHODS[$method->name], $method->class, $method->name, $method->getFileName(), $parent, $method->getReturnType()); if (isset(self::$returnTypes[$class][$method->name][0]) && $canAddReturnType) { $this->fixReturnStatements($method, self::$returnTypes[$class][$method->name][0]); } if ($method->isPrivate()) { unset(self::$returnTypes[$class][$method->name]); } } $this->patchTypes['force'] = $forcePatchTypes; if ($method->isPrivate()) { continue; } $finalOrInternal = false; foreach (['final', 'internal'] as $annotation) { if (null !== $description = $doc[$annotation][0] ?? null) { self::${$annotation.'Methods'}[$class][$method->name] = [$class, '' !== $description ? ' '.$description.(preg_match('/[[:punct:]]$/', $description) ? '' : '.') : '.']; $finalOrInternal = true; } } if ($finalOrInternal || $method->isConstructor() || !isset($doc['param']) || StatelessInvocation::class === $class) { continue; } if (!isset(self::$annotatedParameters[$class][$method->name])) { $definedParameters = []; foreach ($method->getParameters() as $parameter) { $definedParameters[$parameter->name] = true; } } foreach ($doc['param'] as $parameterName => $parameterType) { if (!isset($definedParameters[$parameterName])) { self::$annotatedParameters[$class][$method->name][$parameterName] = sprintf('The "%%s::%s()" method will require a new "%s$%s" argument in the next major version of its %s "%s", not defining it is deprecated.', $method->name, $parameterType ? $parameterType.' ' : '', $parameterName, interface_exists($className) ? 'interface' : 'parent class', $className); } } } return $deprecations; } public function checkCase(\ReflectionClass $refl, string $file, string $class): ?array { $real = explode('\\', $class.strrchr($file, '.')); $tail = explode(\DIRECTORY_SEPARATOR, str_replace('/', \DIRECTORY_SEPARATOR, $file)); $i = \count($tail) - 1; $j = \count($real) - 1; while (isset($tail[$i], $real[$j]) && $tail[$i] === $real[$j]) { --$i; --$j; } array_splice($tail, 0, $i + 1); if (!$tail) { return null; } $tail = \DIRECTORY_SEPARATOR.implode(\DIRECTORY_SEPARATOR, $tail); $tailLen = \strlen($tail); $real = $refl->getFileName(); if (2 === self::$caseCheck) { $real = $this->darwinRealpath($real); } if (0 === substr_compare($real, $tail, -$tailLen, $tailLen, true) && 0 !== substr_compare($real, $tail, -$tailLen, $tailLen, false) ) { return [substr($tail, -$tailLen + 1), substr($real, -$tailLen + 1), substr($real, 0, -$tailLen + 1)]; } return null; } /** * `realpath` on MacOSX doesn't normalize the case of characters. */ private function darwinRealpath(string $real): string { $i = 1 + strrpos($real, '/'); $file = substr($real, $i); $real = substr($real, 0, $i); if (isset(self::$darwinCache[$real])) { $kDir = $real; } else { $kDir = strtolower($real); if (isset(self::$darwinCache[$kDir])) { $real = self::$darwinCache[$kDir][0]; } else { $dir = getcwd(); if (!@chdir($real)) { return $real.$file; } $real = getcwd().'/'; chdir($dir); $dir = $real; $k = $kDir; $i = \strlen($dir) - 1; while (!isset(self::$darwinCache[$k])) { self::$darwinCache[$k] = [$dir, []]; self::$darwinCache[$dir] = &self::$darwinCache[$k]; while ('/' !== $dir[--$i]) { } $k = substr($k, 0, ++$i); $dir = substr($dir, 0, $i--); } } } $dirFiles = self::$darwinCache[$kDir][1]; if (!isset($dirFiles[$file]) && ') : eval()\'d code' === substr($file, -17)) { // Get the file name from "file_name.php(123) : eval()'d code" $file = substr($file, 0, strrpos($file, '(', -17)); } if (isset($dirFiles[$file])) { return $real.$dirFiles[$file]; } $kFile = strtolower($file); if (!isset($dirFiles[$kFile])) { foreach (scandir($real, 2) as $f) { if ('.' !== $f[0]) { $dirFiles[$f] = $f; if ($f === $file) { $kFile = $k = $file; } elseif ($f !== $k = strtolower($f)) { $dirFiles[$k] = $f; } } } self::$darwinCache[$kDir][1] = $dirFiles; } return $real.$dirFiles[$kFile]; } /** * `class_implements` includes interfaces from the parents so we have to manually exclude them. * * @return string[] */ private function getOwnInterfaces(string $class, ?string $parent): array { $ownInterfaces = class_implements($class, false); if ($parent) { foreach (class_implements($parent, false) as $interface) { unset($ownInterfaces[$interface]); } } foreach ($ownInterfaces as $interface) { foreach (class_implements($interface) as $interface) { unset($ownInterfaces[$interface]); } } return $ownInterfaces; } private function setReturnType(string $types, string $class, string $method, string $filename, ?string $parent, \ReflectionType $returnType = null): void { if ('__construct' === $method) { return; } if ('null' === $types) { self::$returnTypes[$class][$method] = ['null', 'null', $class, $filename]; return; } if ($nullable = 0 === strpos($types, 'null|')) { $types = substr($types, 5); } elseif ($nullable = '|null' === substr($types, -5)) { $types = substr($types, 0, -5); } $arrayType = ['array' => 'array']; $typesMap = []; $glue = false !== strpos($types, '&') ? '&' : '|'; foreach (explode($glue, $types) as $t) { $t = self::SPECIAL_RETURN_TYPES[strtolower($t)] ?? $t; $typesMap[$this->normalizeType($t, $class, $parent, $returnType)][$t] = $t; } if (isset($typesMap['array'])) { if (isset($typesMap['Traversable']) || isset($typesMap['\Traversable'])) { $typesMap['iterable'] = $arrayType !== $typesMap['array'] ? $typesMap['array'] : ['iterable']; unset($typesMap['array'], $typesMap['Traversable'], $typesMap['\Traversable']); } elseif ($arrayType !== $typesMap['array'] && isset(self::$returnTypes[$class][$method]) && !$returnType) { return; } } if (isset($typesMap['array']) && isset($typesMap['iterable'])) { if ($arrayType !== $typesMap['array']) { $typesMap['iterable'] = $typesMap['array']; } unset($typesMap['array']); } $iterable = $object = true; foreach ($typesMap as $n => $t) { if ('null' !== $n) { $iterable = $iterable && (\in_array($n, ['array', 'iterable']) || str_contains($n, 'Iterator')); $object = $object && (\in_array($n, ['callable', 'object', '$this', 'static']) || !isset(self::SPECIAL_RETURN_TYPES[$n])); } } $phpTypes = []; $docTypes = []; foreach ($typesMap as $n => $t) { if ('null' === $n) { $nullable = true; continue; } $docTypes[] = $t; if ('mixed' === $n || 'void' === $n) { $nullable = false; $phpTypes = ['' => $n]; continue; } if ('resource' === $n) { // there is no native type for "resource" return; } if (!isset($phpTypes[''])) { $phpTypes[] = $n; } } $docTypes = array_merge([], ...$docTypes); if (!$phpTypes) { return; } if (1 < \count($phpTypes)) { if ($iterable && '8.0' > $this->patchTypes['php']) { $phpTypes = $docTypes = ['iterable']; } elseif ($object && 'object' === $this->patchTypes['force']) { $phpTypes = $docTypes = ['object']; } elseif ('8.0' > $this->patchTypes['php']) { // ignore multi-types return declarations return; } } $phpType = sprintf($nullable ? (1 < \count($phpTypes) ? '%s|null' : '?%s') : '%s', implode($glue, $phpTypes)); $docType = sprintf($nullable ? '%s|null' : '%s', implode($glue, $docTypes)); self::$returnTypes[$class][$method] = [$phpType, $docType, $class, $filename]; } private function normalizeType(string $type, string $class, ?string $parent, ?\ReflectionType $returnType): string { if (isset(self::SPECIAL_RETURN_TYPES[$lcType = strtolower($type)])) { if ('parent' === $lcType = self::SPECIAL_RETURN_TYPES[$lcType]) { $lcType = null !== $parent ? '\\'.$parent : 'parent'; } elseif ('self' === $lcType) { $lcType = '\\'.$class; } return $lcType; } // We could resolve "use" statements to return the FQDN // but this would be too expensive for a runtime checker if ('[]' !== substr($type, -2)) { return $type; } if ($returnType instanceof \ReflectionNamedType) { $type = $returnType->getName(); if ('mixed' !== $type) { return isset(self::SPECIAL_RETURN_TYPES[$type]) ? $type : '\\'.$type; } } return 'array'; } /** * Utility method to add #[ReturnTypeWillChange] where php triggers deprecations. */ private function patchReturnTypeWillChange(\ReflectionMethod $method) { if (\count($method->getAttributes(\ReturnTypeWillChange::class))) { return; } if (!is_file($file = $method->getFileName())) { return; } $fileOffset = self::$fileOffsets[$file] ?? 0; $code = file($file); $startLine = $method->getStartLine() + $fileOffset - 2; if (false !== stripos($code[$startLine], 'ReturnTypeWillChange')) { return; } $code[$startLine] .= " #[\\ReturnTypeWillChange]\n"; self::$fileOffsets[$file] = 1 + $fileOffset; file_put_contents($file, $code); } /** * Utility method to add @return annotations to the Symfony code-base where it triggers self-deprecations. */ private function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile, string $normalizedType) { static $patchedMethods = []; static $useStatements = []; if (!is_file($file = $method->getFileName()) || isset($patchedMethods[$file][$startLine = $method->getStartLine()])) { return; } $patchedMethods[$file][$startLine] = true; $fileOffset = self::$fileOffsets[$file] ?? 0; $startLine += $fileOffset - 2; if ($nullable = '|null' === substr($returnType, -5)) { $returnType = substr($returnType, 0, -5); } $glue = false !== strpos($returnType, '&') ? '&' : '|'; $returnType = explode($glue, $returnType); $code = file($file); foreach ($returnType as $i => $type) { if (preg_match('/((?:\[\])+)$/', $type, $m)) { $type = substr($type, 0, -\strlen($m[1])); $format = '%s'.$m[1]; } else { $format = null; } if (isset(self::SPECIAL_RETURN_TYPES[$type]) || ('\\' === $type[0] && !$p = strrpos($type, '\\', 1))) { continue; } [$namespace, $useOffset, $useMap] = $useStatements[$file] ?? $useStatements[$file] = self::getUseStatements($file); if ('\\' !== $type[0]) { [$declaringNamespace, , $declaringUseMap] = $useStatements[$declaringFile] ?? $useStatements[$declaringFile] = self::getUseStatements($declaringFile); $p = strpos($type, '\\', 1); $alias = $p ? substr($type, 0, $p) : $type; if (isset($declaringUseMap[$alias])) { $type = '\\'.$declaringUseMap[$alias].($p ? substr($type, $p) : ''); } else { $type = '\\'.$declaringNamespace.$type; } $p = strrpos($type, '\\', 1); } $alias = substr($type, 1 + $p); $type = substr($type, 1); if (!isset($useMap[$alias]) && (class_exists($c = $namespace.$alias) || interface_exists($c) || trait_exists($c))) { $useMap[$alias] = $c; } if (!isset($useMap[$alias])) { $useStatements[$file][2][$alias] = $type; $code[$useOffset] = "use $type;\n".$code[$useOffset]; ++$fileOffset; } elseif ($useMap[$alias] !== $type) { $alias .= 'FIXME'; $useStatements[$file][2][$alias] = $type; $code[$useOffset] = "use $type as $alias;\n".$code[$useOffset]; ++$fileOffset; } $returnType[$i] = null !== $format ? sprintf($format, $alias) : $alias; } if ('docblock' === $this->patchTypes['force'] || ('object' === $normalizedType && '7.1' === $this->patchTypes['php'])) { $returnType = implode($glue, $returnType).($nullable ? '|null' : ''); if (false !== strpos($code[$startLine], '#[')) { --$startLine; } if ($method->getDocComment()) { $code[$startLine] = " * @return $returnType\n".$code[$startLine]; } else { $code[$startLine] .= <<<EOTXT /** * @return $returnType */ EOTXT; } $fileOffset += substr_count($code[$startLine], "\n") - 1; } self::$fileOffsets[$file] = $fileOffset; file_put_contents($file, $code); $this->fixReturnStatements($method, $normalizedType); } private static function getUseStatements(string $file): array { $namespace = ''; $useMap = []; $useOffset = 0; if (!is_file($file)) { return [$namespace, $useOffset, $useMap]; } $file = file($file); for ($i = 0; $i < \count($file); ++$i) { if (preg_match('/^(class|interface|trait|abstract) /', $file[$i])) { break; } if (str_starts_with($file[$i], 'namespace ')) { $namespace = substr($file[$i], \strlen('namespace '), -2).'\\'; $useOffset = $i + 2; } if (str_starts_with($file[$i], 'use ')) { $useOffset = $i; for (; str_starts_with($file[$i], 'use '); ++$i) { $u = explode(' as ', substr($file[$i], 4, -2), 2); if (1 === \count($u)) { $p = strrpos($u[0], '\\'); $useMap[substr($u[0], false !== $p ? 1 + $p : 0)] = $u[0]; } else { $useMap[$u[1]] = $u[0]; } } break; } } return [$namespace, $useOffset, $useMap]; } private function fixReturnStatements(\ReflectionMethod $method, string $returnType) { if ('docblock' !== $this->patchTypes['force']) { if ('7.1' === $this->patchTypes['php'] && 'object' === ltrim($returnType, '?')) { return; } if ('7.4' > $this->patchTypes['php'] && $method->hasReturnType()) { return; } if ('8.0' > $this->patchTypes['php'] && (false !== strpos($returnType, '|') || \in_array($returnType, ['mixed', 'static'], true))) { return; } if ('8.1' > $this->patchTypes['php'] && false !== strpos($returnType, '&')) { return; } } if (!is_file($file = $method->getFileName())) { return; } $fixedCode = $code = file($file); $i = (self::$fileOffsets[$file] ?? 0) + $method->getStartLine(); if ('?' !== $returnType && 'docblock' !== $this->patchTypes['force']) { $fixedCode[$i - 1] = preg_replace('/\)(?::[^;\n]++)?(;?\n)/', "): $returnType\\1", $code[$i - 1]); } $end = $method->isGenerator() ? $i : $method->getEndLine(); for (; $i < $end; ++$i) { if ('void' === $returnType) { $fixedCode[$i] = str_replace(' return null;', ' return;', $code[$i]); } elseif ('mixed' === $returnType || '?' === $returnType[0]) { $fixedCode[$i] = str_replace(' return;', ' return null;', $code[$i]); } else { $fixedCode[$i] = str_replace(' return;', " return $returnType!?;", $code[$i]); } } if ($fixedCode !== $code) { file_put_contents($file, $fixedCode); } } /** * @param \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector */ private function parsePhpDoc(\Reflector $reflector): array { if (!$doc = $reflector->getDocComment()) { return []; } $tagName = ''; $tagContent = ''; $tags = []; foreach (explode("\n", substr($doc, 3, -2)) as $line) { $line = ltrim($line); $line = ltrim($line, '*'); if ('' === $line = trim($line)) { if ('' !== $tagName) { $tags[$tagName][] = $tagContent; } $tagName = $tagContent = ''; continue; } if ('@' === $line[0]) { if ('' !== $tagName) { $tags[$tagName][] = $tagContent; $tagContent = ''; } if (preg_match('{^@([-a-zA-Z0-9_:]++)(\s|$)}', $line, $m)) { $tagName = $m[1]; $tagContent = str_replace("\t", ' ', ltrim(substr($line, 2 + \strlen($tagName)))); } else { $tagName = ''; } } elseif ('' !== $tagName) { $tagContent .= ' '.str_replace("\t", ' ', $line); } } if ('' !== $tagName) { $tags[$tagName][] = $tagContent; } foreach ($tags['method'] ?? [] as $i => $method) { unset($tags['method'][$i]); $parts = preg_split('{(\s++|\((?:[^()]*+|(?R))*\)(?: *: *[^ ]++)?|<(?:[^<>]*+|(?R))*>|\{(?:[^{}]*+|(?R))*\})}', $method, -1, \PREG_SPLIT_DELIM_CAPTURE); $returnType = ''; $static = 'static' === $parts[0]; for ($i = $static ? 2 : 0; null !== $p = $parts[$i] ?? null; $i += 2) { if (\in_array($p, ['', '|', '&', 'callable'], true) || \in_array(substr($returnType, -1), ['|', '&'], true)) { $returnType .= trim($parts[$i - 1] ?? '').$p; continue; } $signature = '(' === ($parts[$i + 1][0] ?? '(') ? $parts[$i + 1] ?? '()' : null; if (null === $signature && '' === $returnType) { $returnType = $p; continue; } if ($static && 2 === $i) { $static = false; $returnType = 'static'; } if (\in_array($description = trim(implode('', \array_slice($parts, 2 + $i))), ['', '.'], true)) { $description = null; } elseif (!preg_match('/[.!]$/', $description)) { $description .= '.'; } $tags['method'][$p] = [$static, $returnType, $signature ?? '()', $description]; break; } } foreach ($tags['param'] ?? [] as $i => $param) { unset($tags['param'][$i]); if (\strlen($param) !== strcspn($param, '<{(')) { $param = preg_replace('{\(([^()]*+|(?R))*\)(?: *: *[^ ]++)?|<([^<>]*+|(?R))*>|\{([^{}]*+|(?R))*\}}', '', $param); } if (false === $i = strpos($param, '$')) { continue; } $type = 0 === $i ? '' : rtrim(substr($param, 0, $i), ' &'); $param = substr($param, 1 + $i, (strpos($param, ' ', $i) ?: (1 + $i + \strlen($param))) - $i - 1); $tags['param'][$param] = $type; } foreach (['var', 'return'] as $k) { if (null === $v = $tags[$k][0] ?? null) { continue; } if (\strlen($v) !== strcspn($v, '<{(')) { $v = preg_replace('{\(([^()]*+|(?R))*\)(?: *: *[^ ]++)?|<([^<>]*+|(?R))*>|\{([^{}]*+|(?R))*\}}', '', $v); } $tags[$k] = substr($v, 0, strpos($v, ' ') ?: \strlen($v)) ?: null; } return $tags; } } error-handler/Error/FatalError.php 0000644 00000005423 15025017654 0013164 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\Error; class FatalError extends \Error { private array $error; /** * {@inheritdoc} * * @param array $error An array as returned by error_get_last() */ public function __construct(string $message, int $code, array $error, int $traceOffset = null, bool $traceArgs = true, array $trace = null) { parent::__construct($message, $code); $this->error = $error; if (null !== $trace) { if (!$traceArgs) { foreach ($trace as &$frame) { unset($frame['args'], $frame['this'], $frame); } } } elseif (null !== $traceOffset) { if (\function_exists('xdebug_get_function_stack') && $trace = @xdebug_get_function_stack()) { if (0 < $traceOffset) { array_splice($trace, -$traceOffset); } foreach ($trace as &$frame) { if (!isset($frame['type'])) { // XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695 if (isset($frame['class'])) { $frame['type'] = '::'; } } elseif ('dynamic' === $frame['type']) { $frame['type'] = '->'; } elseif ('static' === $frame['type']) { $frame['type'] = '::'; } // XDebug also has a different name for the parameters array if (!$traceArgs) { unset($frame['params'], $frame['args']); } elseif (isset($frame['params']) && !isset($frame['args'])) { $frame['args'] = $frame['params']; unset($frame['params']); } } unset($frame); $trace = array_reverse($trace); } else { $trace = []; } } foreach ([ 'file' => $error['file'], 'line' => $error['line'], 'trace' => $trace, ] as $property => $value) { if (null !== $value) { $refl = new \ReflectionProperty(\Error::class, $property); $refl->setAccessible(true); $refl->setValue($this, $value); } } } /** * {@inheritdoc} */ public function getError(): array { return $this->error; } } error-handler/Error/UndefinedMethodError.php 0000644 00000001577 15025017654 0015205 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\Error; class UndefinedMethodError extends \Error { /** * {@inheritdoc} */ public function __construct(string $message, \Throwable $previous) { parent::__construct($message, $previous->getCode(), $previous->getPrevious()); foreach ([ 'file' => $previous->getFile(), 'line' => $previous->getLine(), 'trace' => $previous->getTrace(), ] as $property => $value) { $refl = new \ReflectionProperty(\Error::class, $property); $refl->setAccessible(true); $refl->setValue($this, $value); } } } error-handler/Error/UndefinedFunctionError.php 0000644 00000001601 15025017654 0015536 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\Error; class UndefinedFunctionError extends \Error { /** * {@inheritdoc} */ public function __construct(string $message, \Throwable $previous) { parent::__construct($message, $previous->getCode(), $previous->getPrevious()); foreach ([ 'file' => $previous->getFile(), 'line' => $previous->getLine(), 'trace' => $previous->getTrace(), ] as $property => $value) { $refl = new \ReflectionProperty(\Error::class, $property); $refl->setAccessible(true); $refl->setValue($this, $value); } } } error-handler/Error/OutOfMemoryError.php 0000644 00000000515 15025017654 0014357 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\Error; class OutOfMemoryError extends FatalError { } error-handler/Error/ClassNotFoundError.php 0000644 00000001575 15025017654 0014663 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\Error; class ClassNotFoundError extends \Error { /** * {@inheritdoc} */ public function __construct(string $message, \Throwable $previous) { parent::__construct($message, $previous->getCode(), $previous->getPrevious()); foreach ([ 'file' => $previous->getFile(), 'line' => $previous->getLine(), 'trace' => $previous->getTrace(), ] as $property => $value) { $refl = new \ReflectionProperty(\Error::class, $property); $refl->setAccessible(true); $refl->setValue($this, $value); } } } error-handler/Resources/views/exception_full.html.php 0000644 00000003461 15025017654 0017124 0 ustar 00 <!-- <?= $_message = sprintf('%s (%d %s)', $exceptionMessage, $statusCode, $statusText); ?> --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="<?= $this->charset; ?>" /> <meta name="robots" content="noindex,nofollow" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title><?= $_message; ?></title> <link rel="icon" type="image/png" href="<?= $this->include('assets/images/favicon.png.base64'); ?>"> <style><?= $this->include('assets/css/exception.css'); ?></style> <style><?= $this->include('assets/css/exception_full.css'); ?></style> </head> <body> <script> document.body.classList.add( localStorage.getItem('symfony/profiler/theme') || (matchMedia('(prefers-color-scheme: dark)').matches ? 'theme-dark' : 'theme-light') ); </script> <?php if (class_exists(\Symfony\Component\HttpKernel\Kernel::class)) { ?> <header> <div class="container"> <h1 class="logo"><?= $this->include('assets/images/symfony-logo.svg'); ?> Symfony Exception</h1> <div class="help-link"> <a href="https://symfony.com/doc/<?= Symfony\Component\HttpKernel\Kernel::VERSION; ?>/index.html"> <span class="icon"><?= $this->include('assets/images/icon-book.svg'); ?></span> <span class="hidden-xs-down">Symfony</span> Docs </a> </div> </div> </header> <?php } ?> <?= $this->include('views/exception.html.php', $context); ?> <script> <?= $this->include('assets/js/exception.js'); ?> </script> </body> </html> <!-- <?= $_message; ?> --> error-handler/Resources/views/exception.html.php 0000644 00000010676 15025017654 0016110 0 ustar 00 <div class="exception-summary <?= !$exceptionMessage ? 'exception-without-message' : ''; ?>"> <div class="exception-metadata"> <div class="container"> <h2 class="exception-hierarchy"> <?php foreach (array_reverse($exception->getAllPrevious(), true) as $index => $previousException) { ?> <a href="#trace-box-<?= $index + 2; ?>"><?= $this->abbrClass($previousException->getClass()); ?></a> <span class="icon"><?= $this->include('assets/images/chevron-right.svg'); ?></span> <?php } ?> <a href="#trace-box-1"><?= $this->abbrClass($exception->getClass()); ?></a> </h2> <h2 class="exception-http"> HTTP <?= $statusCode; ?> <small><?= $statusText; ?></small> </h2> </div> </div> <div class="exception-message-wrapper"> <div class="container"> <h1 class="break-long-words exception-message<?= mb_strlen($exceptionMessage) > 180 ? ' long' : ''; ?>"><?= $this->formatFileFromText(nl2br($exceptionMessage)); ?></h1> <div class="exception-illustration hidden-xs-down"> <?= $this->include('assets/images/symfony-ghost.svg.php'); ?> </div> </div> </div> </div> <div class="container"> <div class="sf-tabs"> <div class="tab"> <?php $exceptionAsArray = $exception->toArray(); $exceptionWithUserCode = []; $exceptionAsArrayCount = count($exceptionAsArray); $last = $exceptionAsArrayCount - 1; foreach ($exceptionAsArray as $i => $e) { foreach ($e['trace'] as $trace) { if ($trace['file'] && false === mb_strpos($trace['file'], '/vendor/') && false === mb_strpos($trace['file'], '/var/cache/') && $i < $last) { $exceptionWithUserCode[] = $i; } } } ?> <h3 class="tab-title"> <?php if ($exceptionAsArrayCount > 1) { ?> Exceptions <span class="badge"><?= $exceptionAsArrayCount; ?></span> <?php } else { ?> Exception <?php } ?> </h3> <div class="tab-content"> <?php foreach ($exceptionAsArray as $i => $e) { echo $this->include('views/traces.html.php', [ 'exception' => $e, 'index' => $i + 1, 'expand' => in_array($i, $exceptionWithUserCode, true) || ([] === $exceptionWithUserCode && 0 === $i), ]); } ?> </div> </div> <?php if ($logger) { ?> <div class="tab <?= !$logger->getLogs() ? 'disabled' : ''; ?>"> <h3 class="tab-title"> Logs <?php if ($logger->countErrors()) { ?><span class="badge status-error"><?= $logger->countErrors(); ?></span><?php } ?> </h3> <div class="tab-content"> <?php if ($logger->getLogs()) { ?> <?= $this->include('views/logs.html.php', ['logs' => $logger->getLogs()]); ?> <?php } else { ?> <div class="empty"> <p>No log messages</p> </div> <?php } ?> </div> </div> <?php } ?> <div class="tab"> <h3 class="tab-title"> <?php if ($exceptionAsArrayCount > 1) { ?> Stack Traces <span class="badge"><?= $exceptionAsArrayCount; ?></span> <?php } else { ?> Stack Trace <?php } ?> </h3> <div class="tab-content"> <?php foreach ($exceptionAsArray as $i => $e) { echo $this->include('views/traces_text.html.php', [ 'exception' => $e, 'index' => $i + 1, 'numExceptions' => $exceptionAsArrayCount, ]); } ?> </div> </div> <?php if ($currentContent) { ?> <div class="tab"> <h3 class="tab-title">Output content</h3> <div class="tab-content"> <?= $currentContent; ?> </div> </div> <?php } ?> </div> </div> error-handler/Resources/views/traces.html.php 0000644 00000004610 15025017654 0015362 0 ustar 00 <div class="trace trace-as-html" id="trace-box-<?= $index; ?>"> <div class="trace-details"> <div class="trace-head"> <div class="sf-toggle" data-toggle-selector="#trace-html-<?= $index; ?>" data-toggle-initial="<?= $expand ? 'display' : ''; ?>"> <span class="icon icon-close"><?= $this->include('assets/images/icon-minus-square-o.svg'); ?></span> <span class="icon icon-open"><?= $this->include('assets/images/icon-plus-square-o.svg'); ?></span> <?php $separator = strrpos($exception['class'], '\\'); $separator = false === $separator ? 0 : $separator + 1; $namespace = substr($exception['class'], 0, $separator); $class = substr($exception['class'], $separator); ?> <?php if ('' === $class) { ?> <br> <?php } else { ?> <h3 class="trace-class"> <?php if ('' !== $namespace) { ?> <span class="trace-namespace"><?= $namespace; ?></span> <?php } ?> <?= $class; ?> </h3> <?php } ?> <?php if ($exception['message'] && $index > 1) { ?> <p class="break-long-words trace-message"><?= $this->escape($exception['message']); ?></p> <?php } ?> </div> </div> <div id="trace-html-<?= $index; ?>" class="sf-toggle-content"> <?php $isFirstUserCode = true; foreach ($exception['trace'] as $i => $trace) { $isVendorTrace = $trace['file'] && (false !== mb_strpos($trace['file'], '/vendor/') || false !== mb_strpos($trace['file'], '/var/cache/')); $displayCodeSnippet = $isFirstUserCode && !$isVendorTrace; if ($displayCodeSnippet) { $isFirstUserCode = false; } ?> <div class="trace-line <?= $isVendorTrace ? 'trace-from-vendor' : ''; ?>"> <?= $this->include('views/trace.html.php', [ 'prefix' => $index, 'i' => $i, 'trace' => $trace, 'style' => $isVendorTrace ? 'compact' : ($displayCodeSnippet ? 'expanded' : ''), ]); ?> </div> <?php } ?> </div> </div> </div> error-handler/Resources/views/traces_text.html.php 0000644 00000004020 15025017654 0016421 0 ustar 00 <table class="trace trace-as-text"> <thead class="trace-head"> <tr> <th class="sf-toggle" data-toggle-selector="#trace-text-<?= $index; ?>" data-toggle-initial="<?= 1 === $index ? 'display' : ''; ?>"> <div class="trace-class"> <?php if ($numExceptions > 1) { ?> <span class="text-muted">[<?= $numExceptions - $index + 1; ?>/<?= $numExceptions; ?>]</span> <?php } ?> <?= ($parts = explode('\\', $exception['class'])) ? end($parts) : ''; ?> <span class="icon icon-close"><?= $this->include('assets/images/icon-minus-square-o.svg'); ?></span> <span class="icon icon-open"><?= $this->include('assets/images/icon-plus-square-o.svg'); ?></span> </div> </th> </tr> </thead> <tbody id="trace-text-<?= $index; ?>"> <tr> <td> <?php if ($exception['trace']) { ?> <pre class="stacktrace"> <?php echo $this->escape($exception['class']).":\n"; if ($exception['message']) { echo $this->escape($exception['message'])."\n"; } foreach ($exception['trace'] as $trace) { echo "\n "; if ($trace['function']) { echo $this->escape('at '.$trace['class'].$trace['type'].$trace['function']).'('.(isset($trace['args']) ? $this->formatArgsAsText($trace['args']) : '').')'; } if ($trace['file'] && $trace['line']) { echo($trace['function'] ? "\n (" : 'at ').strtr(strip_tags($this->formatFile($trace['file'], $trace['line'])), [' at line '.$trace['line'] => '']).':'.$trace['line'].($trace['function'] ? ')' : ''); } } ?> </pre> <?php } ?> </td> </tr> </tbody> </table> error-handler/Resources/views/error.html.php 0000644 00000001171 15025017654 0015231 0 ustar 00 <!DOCTYPE html> <html> <head> <meta charset="<?= $this->charset; ?>" /> <meta name="robots" content="noindex,nofollow,noarchive" /> <title>An Error Occurred: <?= $statusText; ?></title> <style><?= $this->include('assets/css/error.css'); ?></style> </head> <body> <div class="container"> <h1>Oops! An Error Occurred</h1> <h2>The server returned a "<?= $statusCode; ?> <?= $statusText; ?>".</h2> <p> Something is broken. Please let us know what you were doing when this error occurred. We will fix it as soon as possible. Sorry for any inconvenience caused. </p> </div> </body> </html> error-handler/Resources/views/trace.html.php 0000644 00000005004 15025017654 0015175 0 ustar 00 <div class="trace-line-header break-long-words <?= $trace['file'] ? 'sf-toggle' : ''; ?>" data-toggle-selector="#trace-html-<?= $prefix; ?>-<?= $i; ?>" data-toggle-initial="<?= 'expanded' === $style ? 'display' : ''; ?>"> <?php if ($trace['file']) { ?> <span class="icon icon-close"><?= $this->include('assets/images/icon-minus-square.svg'); ?></span> <span class="icon icon-open"><?= $this->include('assets/images/icon-plus-square.svg'); ?></span> <?php } ?> <?php if ('compact' !== $style && $trace['function']) { ?> <span class="trace-class"><?= $this->abbrClass($trace['class']); ?></span><?php if ($trace['type']) { ?><span class="trace-type"><?= $trace['type']; ?></span><?php } ?><span class="trace-method"><?= $trace['function']; ?></span><?php if (isset($trace['args'])) { ?><span class="trace-arguments">(<?= $this->formatArgs($trace['args']); ?>)</span><?php } ?> <?php } ?> <?php if ($trace['file']) { ?> <?php $lineNumber = $trace['line'] ?: 1; $fileLink = $this->getFileLink($trace['file'], $lineNumber); $filePath = strtr(strip_tags($this->formatFile($trace['file'], $lineNumber)), [' at line '.$lineNumber => '']); $filePathParts = explode(\DIRECTORY_SEPARATOR, $filePath); ?> <span class="block trace-file-path"> in <a href="<?= $fileLink; ?>"> <?= implode(\DIRECTORY_SEPARATOR, array_slice($filePathParts, 0, -1)).\DIRECTORY_SEPARATOR; ?><strong><?= end($filePathParts); ?></strong> </a> <?php if ('compact' === $style && $trace['function']) { ?> <span class="trace-type"><?= $trace['type']; ?></span> <span class="trace-method"><?= $trace['function']; ?></span> <?php } ?> (line <?= $lineNumber; ?>) <span class="icon icon-copy hidden" data-clipboard-text="<?php echo implode(\DIRECTORY_SEPARATOR, $filePathParts).':'.$lineNumber; ?>"> <?php echo $this->include('assets/images/icon-copy.svg'); ?> </span> </span> <?php } ?> </div> <?php if ($trace['file']) { ?> <div id="trace-html-<?= $prefix.'-'.$i; ?>" class="trace-code sf-toggle-content"> <?= strtr($this->fileExcerpt($trace['file'], $trace['line'], 5), [ '#DD0000' => 'var(--highlight-string)', '#007700' => 'var(--highlight-keyword)', '#0000BB' => 'var(--highlight-default)', '#FF8000' => 'var(--highlight-comment)', ]); ?> </div> <?php } ?> error-handler/Resources/views/logs.html.php 0000644 00000004133 15025017654 0015045 0 ustar 00 <table class="logs" data-filter-level="Emergency,Alert,Critical,Error,Warning,Notice,Info,Debug" data-filters> <?php $channelIsDefined = isset($logs[0]['channel']); ?> <thead> <tr> <th data-filter="level">Level</th> <?php if ($channelIsDefined) { ?><th data-filter="channel">Channel</th><?php } ?> <th class="full-width">Message</th> </tr> </thead> <tbody> <?php foreach ($logs as $log) { if ($log['priority'] >= 400) { $status = 'error'; } elseif ($log['priority'] >= 300) { $status = 'warning'; } else { $severity = 0; if (($exception = $log['context']['exception'] ?? null) instanceof \ErrorException || $exception instanceof \Symfony\Component\ErrorHandler\Exception\SilencedErrorContext) { $severity = $exception->getSeverity(); } $status = \E_DEPRECATED === $severity || \E_USER_DEPRECATED === $severity ? 'warning' : 'normal'; } ?> <tr class="status-<?= $status; ?>" data-filter-level="<?= strtolower($this->escape($log['priorityName'])); ?>"<?php if ($channelIsDefined) { ?> data-filter-channel="<?= $this->escape($log['channel']); ?>"<?php } ?>> <td class="text-small nowrap"> <span class="colored text-bold"><?= $this->escape($log['priorityName']); ?></span> <span class="text-muted newline"><?= date('H:i:s', $log['timestamp']); ?></span> </td> <?php if ($channelIsDefined) { ?> <td class="text-small text-bold nowrap"> <?= $this->escape($log['channel']); ?> </td> <?php } ?> <td> <?= $this->formatLogMessage($log['message'], $log['context']); ?> <?php if ($log['context']) { ?> <pre class="text-muted prewrap m-t-5"><?= $this->escape(json_encode($log['context'], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES)); ?></pre> <?php } ?> </td> </tr> <?php } ?> </tbody> </table> error-handler/Resources/bin/patch-type-declarations 0000644 00000007171 15025017654 0016514 0 ustar 00 #!/usr/bin/env php <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if ('cli' !== \PHP_SAPI) { throw new Exception('This script must be run from the command line.'); } if (\in_array('-h', $argv) || \in_array('--help', $argv)) { echo implode(PHP_EOL, [ ' Patches type declarations based on "@return" PHPDoc and triggers deprecations for', ' incompatible method declarations.', '', ' This assists you to make your package compatible with Symfony 6, but it can be used', ' for any class/package.', '', ' Available configuration via environment variables:', ' SYMFONY_PATCH_TYPE_DECLARATIONS', ' An url-encoded string to change the behavior of the script. Available parameters:', ' - "force": any value enables deprecation notices - can be any of:', ' - "phpdoc" to patch only docblock annotations', ' - "2" to add all possible return types', ' - "1" to add return types but only to tests/final/internal/private methods', ' - "php": the target version of PHP - e.g. "7.1" doesn\'t generate "object" types', ' - "deprecations": "1" to trigger a deprecation notice when a child class misses a', ' return type while the parent declares an "@return" annotation', '', ' SYMFONY_PATCH_TYPE_EXCLUDE', ' A regex matched against the full path to the class - any match will be excluded', '', ' Example: "SYMFONY_PATCH_TYPE_DECLARATIONS=php=7.4 ./patch-type-declarations"', ]); exit; } if (false === getenv('SYMFONY_PATCH_TYPE_DECLARATIONS')) { putenv('SYMFONY_PATCH_TYPE_DECLARATIONS=force=2'); echo 'No SYMFONY_PATCH_TYPE_DECLARATIONS env var set, patching type declarations in all methods (run the command with "-h" for more information).'.PHP_EOL; } if (is_file($autoload = __DIR__.'/../../../../autoload.php')) { // noop } elseif (is_file($autoload = __DIR__.'/../../../../../../../autoload.php')) { // noop } else { echo PHP_EOL.' /!\ Cannot find the Composer autoloader, did you forget to run "composer install"?'.PHP_EOL; exit(1); } if (is_file($phpunitAutoload = dirname($autoload).'/bin/.phpunit/phpunit/vendor/autoload.php')) { require $phpunitAutoload; } $loader = require $autoload; Symfony\Component\ErrorHandler\DebugClassLoader::enable(); $deprecations = []; set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$deprecations) { if (\E_USER_DEPRECATED !== $type) { return; } [,,,,, $class,] = explode('"', $msg); $deprecations[$class][] = $msg; }); $exclude = getenv('SYMFONY_PATCH_TYPE_EXCLUDE') ?: null; foreach ($loader->getClassMap() as $class => $file) { if (false !== strpos($file = realpath($file), \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR)) { continue; } if ($exclude && preg_match($exclude, $file)) { continue; } class_exists($class); } Symfony\Component\ErrorHandler\DebugClassLoader::checkClasses(); foreach ($deprecations as $class => $classDeprecations) { echo $class.' ('.\count($classDeprecations).')'.PHP_EOL; echo implode(PHP_EOL, $classDeprecations).PHP_EOL.PHP_EOL; } if ($deprecations && false !== strpos(getenv('SYMFONY_PATCH_TYPE_DECLARATIONS') ?? '', 'force')) { echo 'These deprecations might be fixed by the patch script, run this again to check for type deprecations.'.PHP_EOL; } error-handler/Resources/bin/extract-tentative-return-types.php 0000644 00000003753 15025017654 0020712 0 ustar 00 #!/usr/bin/env php <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if ('cli' !== \PHP_SAPI) { throw new Exception('This script must be run from the command line.'); } // Run from the root of the php-src repository, this script generates // a table with all the methods that have a tentative return type. // // Usage: find -name *.stub.php | sort | /path/to/extract-tentative-return-types.php > /path/to/TentativeTypes.php echo <<<EOPHP <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\ErrorHandler\Internal; /** * This class has been generated by extract-tentative-return-types.php. * * @internal */ class TentativeTypes { public const RETURN_TYPES = [ EOPHP; while (false !== $file = fgets(\STDIN)) { $code = file_get_contents(substr($file, 0, -1)); if (!str_contains($code, '@tentative-return-type')) { continue; } $code = preg_split('{^\s*(?:(?:abstract )?class|interface|trait) ([^\s]++)}m', $code, -1, \PREG_SPLIT_DELIM_CAPTURE); if (1 === count($code)) { continue; } for ($i = 1; null !== $class = $code[$i] ?? null; $i += 2) { $methods = $code[1 + $i]; if (!str_contains($methods, '@tentative-return-type')) { continue; } echo " '$class' => [\n"; preg_replace_callback('{@tentative-return-type.*?[\s]function ([^(]++)[^)]++\)\s*+:\s*+([^\n;\{]++)}s', function ($m) { $m[2] = str_replace(' ', '', $m[2]); echo " '$m[1]' => '$m[2]',\n"; return ''; }, $methods); echo " ],\n"; } } echo <<<EOPHP ]; } EOPHP; error-handler/Resources/assets/css/exception_full.css 0000644 00000005300 15025017654 0017111 0 ustar 00 .sf-reset .traces { padding-bottom: 14px; } .sf-reset .traces li { font-size: 12px; color: #868686; padding: 5px 4px; list-style-type: decimal; margin-left: 20px; } .sf-reset #logs .traces li.error { font-style: normal; color: #AA3333; background: #f9ecec; } .sf-reset #logs .traces li.warning { font-style: normal; background: #ffcc00; } /* fix for Opera not liking empty <li> */ .sf-reset .traces li:after { content: "\00A0"; } .sf-reset .trace { border: 1px solid #D3D3D3; padding: 10px; overflow: auto; margin: 10px 0 20px; } .sf-reset .block-exception { -moz-border-radius: 16px; -webkit-border-radius: 16px; border-radius: 16px; margin-bottom: 20px; background-color: #f6f6f6; border: 1px solid #dfdfdf; padding: 30px 28px; word-wrap: break-word; overflow: hidden; } .sf-reset .block-exception div { color: #313131; font-size: 10px; } .sf-reset .block-exception-detected .illustration-exception, .sf-reset .block-exception-detected .text-exception { float: left; } .sf-reset .block-exception-detected .illustration-exception { width: 152px; } .sf-reset .block-exception-detected .text-exception { width: 670px; padding: 30px 44px 24px 46px; position: relative; } .sf-reset .text-exception .open-quote, .sf-reset .text-exception .close-quote { font-family: Arial, Helvetica, sans-serif; position: absolute; color: #C9C9C9; font-size: 8em; } .sf-reset .open-quote { top: 0; left: 0; } .sf-reset .close-quote { bottom: -0.5em; right: 50px; } .sf-reset .block-exception p { font-family: Arial, Helvetica, sans-serif; } .sf-reset .block-exception p a, .sf-reset .block-exception p a:hover { color: #565656; } .sf-reset .logs h2 { float: left; width: 654px; } .sf-reset .error-count, .sf-reset .support { float: right; width: 170px; text-align: right; } .sf-reset .error-count span { display: inline-block; background-color: #aacd4e; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px; padding: 4px; color: white; margin-right: 2px; font-size: 11px; font-weight: bold; } .sf-reset .support a { display: inline-block; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px; padding: 4px; color: #000000; margin-right: 2px; font-size: 11px; font-weight: bold; } .sf-reset .toggle { vertical-align: middle; } .sf-reset .linked ul, .sf-reset .linked li { display: inline; } .sf-reset #output-content { color: #000; font-size: 12px; } .sf-reset #traces-text pre { white-space: pre; font-size: 12px; font-family: monospace; } error-handler/Resources/assets/css/error.css 0000644 00000000423 15025017654 0015223 0 ustar 00 body { background-color: #fff; color: #222; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; } .container { margin: 30px; max-width: 600px; } h1 { color: #dc3545; font-size: 24px; } h2 { font-size: 18px; } error-handler/Resources/assets/css/exception.css 0000644 00000033500 15025017654 0016072 0 ustar 00 /* This file is based on WebProfilerBundle/Resources/views/Profiler/profiler.css.twig. If you make any change in this file, verify the same change is needed in the other file. */ :root { --font-sans-serif: Helvetica, Arial, sans-serif; --page-background: #f9f9f9; --color-text: #222; /* when updating any of these colors, do the same in toolbar.css.twig */ --color-success: #4f805d; --color-warning: #a46a1f; --color-error: #b0413e; --color-muted: #999; --tab-background: #fff; --tab-color: #444; --tab-active-background: #666; --tab-active-color: #fafafa; --tab-disabled-background: #f5f5f5; --tab-disabled-color: #999; --metric-value-background: #fff; --metric-value-color: inherit; --metric-unit-color: #999; --metric-label-background: #e0e0e0; --metric-label-color: inherit; --table-border: #e0e0e0; --table-background: #fff; --table-header: #e0e0e0; --trace-selected-background: #F7E5A1; --tree-active-background: #F7E5A1; --exception-title-color: var(--base-2); --shadow: 0px 0px 1px rgba(128, 128, 128, .2); --border: 1px solid #e0e0e0; --background-error: var(--color-error); --highlight-comment: #969896; --highlight-default: #222222; --highlight-keyword: #a71d5d; --highlight-string: #183691; --base-0: #fff; --base-1: #f5f5f5; --base-2: #e0e0e0; --base-3: #ccc; --base-4: #666; --base-5: #444; --base-6: #222; } .theme-dark { --page-background: #36393e; --color-text: #e0e0e0; --color-muted: #777; --color-error: #d43934; --tab-background: #555; --tab-color: #ccc; --tab-active-background: #888; --tab-active-color: #fafafa; --tab-disabled-background: var(--page-background); --tab-disabled-color: #777; --metric-value-background: #555; --metric-value-color: inherit; --metric-unit-color: #999; --metric-label-background: #777; --metric-label-color: #e0e0e0; --trace-selected-background: #71663acc; --table-border: #444; --table-background: #333; --table-header: #555; --info-background: rgba(79, 148, 195, 0.5); --tree-active-background: var(--metric-label-background); --exception-title-color: var(--base-2); --shadow: 0px 0px 1px rgba(32, 32, 32, .2); --border: 1px solid #666; --background-error: #b0413e; --highlight-comment: #dedede; --highlight-default: var(--base-6); --highlight-keyword: #ff413c; --highlight-string: #70a6fd; --base-0: #2e3136; --base-1: #444; --base-2: #666; --base-3: #666; --base-4: #666; --base-5: #e0e0e0; --base-6: #f5f5f5; --card-label-background: var(--tab-active-background); --card-label-color: var(--tab-active-color); } html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0} html { /* always display the vertical scrollbar to avoid jumps when toggling contents */ overflow-y: scroll; } body { background-color: var(--page-background); color: var(--base-6); font: 14px/1.4 Helvetica, Arial, sans-serif; padding-bottom: 45px; } a { cursor: pointer; text-decoration: none; } a:hover { text-decoration: underline; } abbr[title] { border-bottom: none; cursor: help; text-decoration: none; } code, pre { font: 13px/1.5 Consolas, Monaco, Menlo, "Ubuntu Mono", "Liberation Mono", monospace; } table, tr, th, td { background: var(--base-0); border-collapse: collapse; vertical-align: top; } table { background: var(--base-0); border: var(--border); box-shadow: 0px 0px 1px rgba(128, 128, 128, .2); margin: 1em 0; width: 100%; } table th, table td { border: solid var(--base-2); border-width: 1px 0; padding: 8px 10px; } table th { background-color: var(--base-2); font-weight: bold; text-align: left; } .m-t-5 { margin-top: 5px; } .hidden-xs-down { display: none; } .block { display: block; } .full-width { width: 100%; } .hidden { display: none; } .prewrap { white-space: pre-wrap; } .nowrap { white-space: nowrap; } .newline { display: block; } .break-long-words { word-wrap: break-word; overflow-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; min-width: 0; } .text-small { font-size: 12px !important; } .text-muted { color: #999; } .text-bold { font-weight: bold; } .empty { border: 4px dashed var(--base-2); color: #999; margin: 1em 0; padding: .5em 2em; } .status-success { background: rgba(94, 151, 110, 0.3); } .status-warning { background: rgba(240, 181, 24, 0.3); } .status-error { background: rgba(176, 65, 62, 0.2); } .status-success td, .status-warning td, .status-error td { background: transparent; } tr.status-error td, tr.status-warning td { border-bottom: 1px solid var(--base-2); border-top: 1px solid var(--base-2); } .status-warning .colored { color: #A46A1F; } .status-error .colored { color: var(--color-error); } .sf-toggle { cursor: pointer; position: relative; } .sf-toggle-content { -moz-transition: display .25s ease; -webkit-transition: display .25s ease; transition: display .25s ease; } .sf-toggle-content.sf-toggle-hidden { display: none; } .sf-toggle-content.sf-toggle-visible { display: block; } thead.sf-toggle-content.sf-toggle-visible, tbody.sf-toggle-content.sf-toggle-visible { display: table-row-group; } .sf-toggle-off .icon-close, .sf-toggle-on .icon-open { display: none; } .sf-toggle-off .icon-open, .sf-toggle-on .icon-close { display: block; } .tab-navigation { margin: 0 0 1em 0; padding: 0; } .tab-navigation li { background: var(--tab-background); border: 1px solid var(--table-border); color: var(--tab-color); cursor: pointer; display: inline-block; font-size: 16px; margin: 0 0 0 -1px; padding: .5em .75em; z-index: 1; } .tab-navigation li .badge { background-color: var(--base-1); color: var(--base-4); display: inline-block; font-size: 14px; font-weight: bold; margin-left: 8px; min-width: 10px; padding: 1px 6px; text-align: center; white-space: nowrap; } .tab-navigation li.disabled { background: var(--tab-disabled-background); color: var(--tab-disabled-color); } .tab-navigation li.active { background: var(--tab-active-background); color: var(--tab-active-color); z-index: 1100; } .tab-navigation li.active .badge { background-color: var(--base-5); color: var(--base-2); } .tab-content > *:first-child { margin-top: 0; } .tab-navigation li .badge.status-warning { background: var(--color-warning); color: #FFF; } .tab-navigation li .badge.status-error { background: var(--background-error); color: #FFF; } .sf-tabs .tab:not(:first-child) { display: none; } [data-filters] { position: relative; } [data-filtered] { cursor: pointer; } [data-filtered]:after { content: '\00a0\25BE'; } [data-filtered]:hover .filter-list li { display: inline-flex; } [class*="filter-hidden-"] { display: none; } .filter-list { position: absolute; border: var(--border); box-shadow: var(--shadow); margin: 0; padding: 0; display: flex; flex-direction: column; } .filter-list :after { content: ''; } .filter-list li { background: var(--tab-disabled-background); border-bottom: var(--border); color: var(--tab-disabled-color); display: none; list-style: none; margin: 0; padding: 5px 10px; text-align: left; font-weight: normal; } .filter-list li.active { background: var(--tab-background); color: var(--tab-color); } .filter-list li.last-active { background: var(--tab-active-background); color: var(--tab-active-color); } .filter-list-level li { cursor: s-resize; } .filter-list-level li.active { cursor: n-resize; } .filter-list-level li.last-active { cursor: default; } .filter-list-level li.last-active:before { content: '\2714\00a0'; } .filter-list-choice li:before { content: '\2714\00a0'; color: transparent; } .filter-list-choice li.active:before { color: unset; } .container { max-width: 1024px; margin: 0 auto; padding: 0 15px; } .container::after { content: ""; display: table; clear: both; } header { background-color: #222; color: rgba(255, 255, 255, 0.75); font-size: 13px; height: 33px; line-height: 33px; padding: 0; } header .container { display: flex; justify-content: space-between; } .logo { flex: 1; font-size: 13px; font-weight: normal; margin: 0; padding: 0; } .logo svg { height: 18px; width: 18px; opacity: .8; vertical-align: -5px; } .help-link { margin-left: 15px; } .help-link a { color: inherit; } .help-link .icon svg { height: 15px; width: 15px; opacity: .7; vertical-align: -2px; } .help-link a:hover { color: #EEE; text-decoration: none; } .help-link a:hover svg { opacity: .9; } .exception-summary { background: var(--background-error); border-bottom: 2px solid rgba(0, 0, 0, 0.1); border-top: 1px solid rgba(0, 0, 0, .3); flex: 0 0 auto; margin-bottom: 15px; } .exception-metadata { background: rgba(0, 0, 0, 0.1); padding: 7px 0; } .exception-metadata .container { display: flex; flex-direction: row; justify-content: space-between; } .exception-metadata h2, .exception-metadata h2 > a { color: rgba(255, 255, 255, 0.8); font-size: 13px; font-weight: 400; margin: 0; } .exception-http small { font-size: 13px; opacity: .7; } .exception-hierarchy { flex: 1; } .exception-hierarchy .icon { margin: 0 3px; opacity: .7; } .exception-hierarchy .icon svg { height: 13px; width: 13px; vertical-align: -2px; } .exception-without-message .exception-message-wrapper { display: none; } .exception-message-wrapper .container { display: flex; align-items: flex-start; min-height: 70px; padding: 10px 15px 8px; } .exception-message { flex-grow: 1; } .exception-message, .exception-message a { color: #FFF; font-size: 21px; font-weight: 400; margin: 0; } .exception-message.long { font-size: 18px; } .exception-message a { border-bottom: 1px solid rgba(255, 255, 255, 0.5); font-size: inherit; text-decoration: none; } .exception-message a:hover { border-bottom-color: #ffffff; } .exception-illustration { flex-basis: 111px; flex-shrink: 0; height: 66px; margin-left: 15px; opacity: .7; } .trace + .trace { margin-top: 30px; } .trace-head { background-color: var(--base-2); padding: 10px; position: relative; } .trace-head .trace-class { color: var(--base-6); font-size: 18px; font-weight: bold; line-height: 1.3; margin: 0; position: relative; } .trace-head .trace-namespace { color: #999; display: block; font-size: 13px; } .trace-head .icon { position: absolute; right: 0; top: 0; } .trace-head .icon svg { fill: var(--base-5); height: 24px; width: 24px; } .trace-details { background: var(--base-0); border: var(--border); box-shadow: 0px 0px 1px rgba(128, 128, 128, .2); margin: 1em 0; table-layout: fixed; } .trace-message { font-size: 14px; font-weight: normal; margin: .5em 0 0; } .trace-line { position: relative; padding-top: 8px; padding-bottom: 8px; } .trace-line + .trace-line { border-top: var(--border); } .trace-line:hover { background: var(--base-1); } .trace-line a { color: var(--base-6); } .trace-line .icon { opacity: .4; position: absolute; left: 10px; } .trace-line .icon svg { fill: var(--base-5); height: 16px; width: 16px; } .trace-line .icon.icon-copy { left: auto; top: auto; padding-left: 5px; display: none } .trace-line:hover .icon.icon-copy:not(.hidden) { display: inline-block } .trace-line-header { padding-left: 36px; padding-right: 10px; } .trace-file-path, .trace-file-path a { color: var(--base-6); font-size: 13px; } .trace-class { color: var(--color-error); } .trace-type { padding: 0 2px; } .trace-method { color: var(--color-error); font-weight: bold; } .trace-arguments { color: #777; font-weight: normal; padding-left: 2px; } .trace-code { background: var(--base-0); font-size: 12px; margin: 10px 10px 2px 10px; padding: 10px; overflow-x: auto; white-space: nowrap; } .trace-code ol { margin: 0; float: left; } .trace-code li { color: #969896; margin: 0; padding-left: 10px; float: left; width: 100%; } .trace-code li + li { margin-top: 5px; } .trace-code li.selected { background: var(--trace-selected-background); margin-top: 2px; } .trace-code li code { color: var(--base-6); white-space: nowrap; } .trace-as-text .stacktrace { line-height: 1.8; margin: 0 0 15px; white-space: pre-wrap; } @media (min-width: 575px) { .hidden-xs-down { display: initial; } .help-link { margin-left: 30px; } } error-handler/Resources/assets/js/exception.js 0000644 00000036127 15025017654 0015552 0 ustar 00 /* This file is based on WebProfilerBundle/Resources/views/Profiler/base_js.html.twig. If you make any change in this file, verify the same change is needed in the other file. */ /*<![CDATA[*/ if (typeof Sfjs === 'undefined') { Sfjs = (function() { "use strict"; if ('classList' in document.documentElement) { var hasClass = function (el, cssClass) { return el.classList.contains(cssClass); }; var removeClass = function(el, cssClass) { el.classList.remove(cssClass); }; var addClass = function(el, cssClass) { el.classList.add(cssClass); }; var toggleClass = function(el, cssClass) { el.classList.toggle(cssClass); }; } else { var hasClass = function (el, cssClass) { return el.className.match(new RegExp('\\b' + cssClass + '\\b')); }; var removeClass = function(el, cssClass) { el.className = el.className.replace(new RegExp('\\b' + cssClass + '\\b'), ' '); }; var addClass = function(el, cssClass) { if (!hasClass(el, cssClass)) { el.className += " " + cssClass; } }; var toggleClass = function(el, cssClass) { hasClass(el, cssClass) ? removeClass(el, cssClass) : addClass(el, cssClass); }; } var addEventListener; var el = document.createElement('div'); if (!('addEventListener' in el)) { addEventListener = function (element, eventName, callback) { element.attachEvent('on' + eventName, callback); }; } else { addEventListener = function (element, eventName, callback) { element.addEventListener(eventName, callback, false); }; } if (navigator.clipboard) { document.querySelectorAll('[data-clipboard-text]').forEach(function(element) { removeClass(element, 'hidden'); element.addEventListener('click', function() { navigator.clipboard.writeText(element.getAttribute('data-clipboard-text')); }) }); } return { addEventListener: addEventListener, createTabs: function() { var tabGroups = document.querySelectorAll('.sf-tabs:not([data-processed=true])'); /* create the tab navigation for each group of tabs */ for (var i = 0; i < tabGroups.length; i++) { var tabs = tabGroups[i].querySelectorAll(':scope > .tab'); var tabNavigation = document.createElement('ul'); tabNavigation.className = 'tab-navigation'; var selectedTabId = 'tab-' + i + '-0'; /* select the first tab by default */ for (var j = 0; j < tabs.length; j++) { var tabId = 'tab-' + i + '-' + j; var tabTitle = tabs[j].querySelector('.tab-title').innerHTML; var tabNavigationItem = document.createElement('li'); tabNavigationItem.setAttribute('data-tab-id', tabId); if (hasClass(tabs[j], 'active')) { selectedTabId = tabId; } if (hasClass(tabs[j], 'disabled')) { addClass(tabNavigationItem, 'disabled'); } tabNavigationItem.innerHTML = tabTitle; tabNavigation.appendChild(tabNavigationItem); var tabContent = tabs[j].querySelector('.tab-content'); tabContent.parentElement.setAttribute('id', tabId); } tabGroups[i].insertBefore(tabNavigation, tabGroups[i].firstChild); addClass(document.querySelector('[data-tab-id="' + selectedTabId + '"]'), 'active'); } /* display the active tab and add the 'click' event listeners */ for (i = 0; i < tabGroups.length; i++) { tabNavigation = tabGroups[i].querySelectorAll(':scope >.tab-navigation li'); for (j = 0; j < tabNavigation.length; j++) { tabId = tabNavigation[j].getAttribute('data-tab-id'); document.getElementById(tabId).querySelector('.tab-title').className = 'hidden'; if (hasClass(tabNavigation[j], 'active')) { document.getElementById(tabId).className = 'block'; } else { document.getElementById(tabId).className = 'hidden'; } tabNavigation[j].addEventListener('click', function(e) { var activeTab = e.target || e.srcElement; /* needed because when the tab contains HTML contents, user can click */ /* on any of those elements instead of their parent '<li>' element */ while (activeTab.tagName.toLowerCase() !== 'li') { activeTab = activeTab.parentNode; } /* get the full list of tabs through the parent of the active tab element */ var tabNavigation = activeTab.parentNode.children; for (var k = 0; k < tabNavigation.length; k++) { var tabId = tabNavigation[k].getAttribute('data-tab-id'); document.getElementById(tabId).className = 'hidden'; removeClass(tabNavigation[k], 'active'); } addClass(activeTab, 'active'); var activeTabId = activeTab.getAttribute('data-tab-id'); document.getElementById(activeTabId).className = 'block'; }); } tabGroups[i].setAttribute('data-processed', 'true'); } }, createToggles: function() { var toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])'); for (var i = 0; i < toggles.length; i++) { var elementSelector = toggles[i].getAttribute('data-toggle-selector'); var element = document.querySelector(elementSelector); addClass(element, 'sf-toggle-content'); if (toggles[i].hasAttribute('data-toggle-initial') && toggles[i].getAttribute('data-toggle-initial') == 'display') { addClass(toggles[i], 'sf-toggle-on'); addClass(element, 'sf-toggle-visible'); } else { addClass(toggles[i], 'sf-toggle-off'); addClass(element, 'sf-toggle-hidden'); } addEventListener(toggles[i], 'click', function(e) { e.preventDefault(); if ('' !== window.getSelection().toString()) { /* Don't do anything on text selection */ return; } var toggle = e.target || e.srcElement; /* needed because when the toggle contains HTML contents, user can click */ /* on any of those elements instead of their parent '.sf-toggle' element */ while (!hasClass(toggle, 'sf-toggle')) { toggle = toggle.parentNode; } var element = document.querySelector(toggle.getAttribute('data-toggle-selector')); toggleClass(toggle, 'sf-toggle-on'); toggleClass(toggle, 'sf-toggle-off'); toggleClass(element, 'sf-toggle-hidden'); toggleClass(element, 'sf-toggle-visible'); /* the toggle doesn't change its contents when clicking on it */ if (!toggle.hasAttribute('data-toggle-alt-content')) { return; } if (!toggle.hasAttribute('data-toggle-original-content')) { toggle.setAttribute('data-toggle-original-content', toggle.innerHTML); } var currentContent = toggle.innerHTML; var originalContent = toggle.getAttribute('data-toggle-original-content'); var altContent = toggle.getAttribute('data-toggle-alt-content'); toggle.innerHTML = currentContent !== altContent ? altContent : originalContent; }); /* Prevents from disallowing clicks on links inside toggles */ var toggleLinks = toggles[i].querySelectorAll('a'); for (var j = 0; j < toggleLinks.length; j++) { addEventListener(toggleLinks[j], 'click', function(e) { e.stopPropagation(); }); } /* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */ var copyToClipboardElements = toggles[i].querySelectorAll('span[data-clipboard-text]'); for (var k = 0; k < copyToClipboardElements.length; k++) { addEventListener(copyToClipboardElements[k], 'click', function(e) { e.stopPropagation(); }); } toggles[i].setAttribute('data-processed', 'true'); } }, createFilters: function() { document.querySelectorAll('[data-filters] [data-filter]').forEach(function (filter) { var filters = filter.closest('[data-filters]'), type = 'choice', name = filter.dataset.filter, ucName = name.charAt(0).toUpperCase()+name.slice(1), list = document.createElement('ul'), values = filters.dataset['filter'+ucName] || filters.querySelectorAll('[data-filter-'+name+']'), labels = {}, defaults = null, indexed = {}, processed = {}; if (typeof values === 'string') { type = 'level'; labels = values.split(','); values = values.toLowerCase().split(','); defaults = values.length - 1; } addClass(list, 'filter-list'); addClass(list, 'filter-list-'+type); values.forEach(function (value, i) { if (value instanceof HTMLElement) { value = value.dataset['filter'+ucName]; } if (value in processed) { return; } var option = document.createElement('li'), label = i in labels ? labels[i] : value, active = false, matches; if ('' === label) { option.innerHTML = '<em>(none)</em>'; } else { option.innerText = label; } option.dataset.filter = value; option.setAttribute('title', 1 === (matches = filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').length) ? 'Matches 1 row' : 'Matches '+matches+' rows'); indexed[value] = i; list.appendChild(option); addEventListener(option, 'click', function () { if ('choice' === type) { filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { if (option.dataset.filter === row.dataset['filter'+ucName]) { toggleClass(row, 'filter-hidden-'+name); } }); toggleClass(option, 'active'); } else if ('level' === type) { if (i === this.parentNode.querySelectorAll('.active').length - 1) { return; } this.parentNode.querySelectorAll('li').forEach(function (currentOption, j) { if (j <= i) { addClass(currentOption, 'active'); if (i === j) { addClass(currentOption, 'last-active'); } else { removeClass(currentOption, 'last-active'); } } else { removeClass(currentOption, 'active'); removeClass(currentOption, 'last-active'); } }); filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { if (i < indexed[row.dataset['filter'+ucName]]) { addClass(row, 'filter-hidden-'+name); } else { removeClass(row, 'filter-hidden-'+name); } }); } }); if ('choice' === type) { active = null === defaults || 0 <= defaults.indexOf(value); } else if ('level' === type) { active = i <= defaults; if (active && i === defaults) { addClass(option, 'last-active'); } } if (active) { addClass(option, 'active'); } else { filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').forEach(function (row) { toggleClass(row, 'filter-hidden-'+name); }); } processed[value] = true; }); if (1 < list.childNodes.length) { filter.appendChild(list); filter.dataset.filtered = ''; } }); } }; })(); Sfjs.addEventListener(document, 'DOMContentLoaded', function() { Sfjs.createTabs(); Sfjs.createToggles(); Sfjs.createFilters(); }); } /*]]>*/ error-handler/Resources/assets/images/symfony-ghost.svg.php 0000644 00000017764 15025017654 0020212 0 ustar 00 <svg viewBox="0 0 136 81" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.4"><path d="M92.4 20.4a23.2 23.2 0 0 1 9 1.9 23.7 23.7 0 0 1 5.2 3 24.3 24.3 0 0 1 3.4 3.4 24.8 24.8 0 0 1 5 9.4c.5 1.7.8 3.4 1 5.2v14.5h.4l.5.2a7.4 7.4 0 0 0 2.5.2l.2-.2.6-.8.8-1.3-.2-.1a5.5 5.5 0 0 1-.8-.3 5.6 5.6 0 0 1-2.3-1.8 5.7 5.7 0 0 1-.9-1.6 6.5 6.5 0 0 1-.2-2.8 7.3 7.3 0 0 1 .5-2l.3-.3.8-.9.3-.3c.2-.2.5-.3.8-.3H120.7c.2 0 .3-.1.4 0h.4l.2.1.3.2.2-.4.3-.4.1-.1 1.2-1 .3-.2.4-.1.4-.1h.3l1.5.1.4.1.8.5.1.2 1 1.1v.2H129.4l.4-.2 1.4-.5h1.1c.3 0 .7.2 1 .4.2 0 .3.2.5.3l.2.2.5.3.4.6.1.3.4 1.4.1.4v.6a7.8 7.8 0 0 1-.1.6 9.9 9.9 0 0 1-.8 2.4 7.8 7.8 0 0 1-3 3.3 6.4 6.4 0 0 1-1 .5 6.1 6.1 0 0 1-.6.2l-.7.1h-.1a23.4 23.4 0 0 1-.2 1.7 14.3 14.3 0 0 1-.6 2.1l-.8 2a9.2 9.2 0 0 1-.4.6l-.7 1a9.1 9.1 0 0 1-2.3 2.2c-.9.5-2 .6-3 .7l-1.4.1h-.5l-.4.1a15.8 15.8 0 0 1-2.8-.1v4.2a9.7 9.7 0 0 1-.7 3.5 9.6 9.6 0 0 1-1.7 2.8 9.3 9.3 0 0 1-3 2.3 9 9 0 0 1-5.4.7 9 9 0 0 1-3-1 9.4 9.4 0 0 1-2.7-2.5 10 10 0 0 1-1 1.2 9.3 9.3 0 0 1-2 1.3 9 9 0 0 1-2.4 1 9 9 0 0 1-6.5-1.1A9.4 9.4 0 0 1 85 77V77a10.9 10.9 0 0 1-.6.6 9.3 9.3 0 0 1-2.7 2 9 9 0 0 1-6 .8 9 9 0 0 1-2.4-1 9.3 9.3 0 0 1-2.3-1.7 9.6 9.6 0 0 1-1.8-2.8 9.7 9.7 0 0 1-.8-3.7v-4a18.5 18.5 0 0 1-2.9.2l-1.2-.1c-1.9-.3-3.7-1-5.1-2.2a8.2 8.2 0 0 1-1.1-1 10.2 10.2 0 0 1-.9-1.2 15.3 15.3 0 0 1-.7-1.3 20.8 20.8 0 0 1-1.9-6.2v-.2a6.5 6.5 0 0 1-1-.3 6.1 6.1 0 0 1-.6-.3 6.6 6.6 0 0 1-.9-.6 8.2 8.2 0 0 1-2.7-3.7 10 10 0 0 1-.3-1 10.3 10.3 0 0 1-.3-1.9V47v-.4l.1-.4.6-1.4.1-.2a2 2 0 0 1 .8-.8l.3-.2.3-.2a3.2 3.2 0 0 1 1.8-.5h.4l.3.2 1.4.6.2.2.4.3.3.4.7-.7.2-.2.4-.2.6-.2h2.1l.4.2.4.2.3.2.8 1 .2-.1h.1v-.1H63l1.1.1h.3l.8.5.3.4.7 1 .2.3.1.5a11 11 0 0 1 .2 1.5c0 .8 0 1.6-.3 2.3a6 6 0 0 1-.5 1.2 5.5 5.5 0 0 1-3.3 2.5 12.3 12.3 0 0 0 1.4 3h.1l.2.1 1 .2h1.5l.5-.2H67.8l.5-.2h.1V44v-.4a26.7 26.7 0 0 1 .3-2.3 24.7 24.7 0 0 1 5.7-12.5 24.2 24.2 0 0 1 3.5-3.3 23.7 23.7 0 0 1 4.9-3 23.2 23.2 0 0 1 5.6-1.7 23.7 23.7 0 0 1 4-.3zm-.3 2a21.2 21.2 0 0 0-8 1.7 21.6 21.6 0 0 0-4.8 2.7 22.2 22.2 0 0 0-3.2 3 22.7 22.7 0 0 0-5 9.2 23.4 23.4 0 0 0-.7 4.9v15.7l-.5.1a34.3 34.3 0 0 1-1.5.3h-.2l-.4.1h-.4l-.9.2a10 10 0 0 1-1.9 0c-.5 0-1-.2-1.5-.4a1.8 1.8 0 0 1-.3-.2 2 2 0 0 1-.3-.3 5.2 5.2 0 0 1-.1-.2 9 9 0 0 1-.6-.9 13.8 13.8 0 0 1-1-2 14.3 14.3 0 0 1-.6-2 14 14 0 0 1-.1-.8v-.2h.3a12.8 12.8 0 0 0 1.4-.2 4.4 4.4 0 0 0 .3 0 3.6 3.6 0 0 0 1.1-.7 3.4 3.4 0 0 0 1.2-1.7l.2-1.2a5.1 5.1 0 0 0 0-.8 7.2 7.2 0 0 0-.1-.8l-.7-1-1.2-.2-1 .7-.1 1.3a5 5 0 0 1 .1.4v.6a1 1 0 0 1 0 .3c-.1.3-.4.4-.7.5l-1.2.4v-.7A9.9 9.9 0 0 1 60 49l.3-.6v-.2l.1-.1v-1.6l-1-1.2h-1.5l-1 1.1v.4a5.3 5.3 0 0 0-.2.6 5.5 5.5 0 0 0 0 .5c0 .7 0 1.4.3 2 0 .4.2.8.4 1.2L57 51a9.5 9.5 0 0 1-1.1-.5h-.2a2 2 0 0 1-.4-.3c-.4-.4-.5-1-.6-1.6a5.6 5.6 0 0 1 0-.5v-.5-.5l-.6-1.5-1.4-.6-.9.3s-.2 0-.3.2a2 2 0 0 1-.1 0l-.6 1.4v.7a8.5 8.5 0 0 0 .5 2c.4 1.1 1 2.1 2 2.8a4.7 4.7 0 0 0 2.1.9h1a22.8 22.8 0 0 0 .1 1 18.1 18.1 0 0 0 .8 3.8 18.2 18.2 0 0 0 1.6 3.7l1 1.3c1 1 2.3 1.6 3.7 2a11.7 11.7 0 0 0 4.8 0h.4l.5-.2.5-.1.6-.2v6.6a8 8 0 0 0 .1 1.3 7.5 7.5 0 0 0 2.4 4.3 7.2 7.2 0 0 0 2.3 1.3 7 7 0 0 0 7-1.1 7.5 7.5 0 0 0 2-2.6A7.7 7.7 0 0 0 85 72V71a8.2 8.2 0 0 0 .2 1.3c0 .7.3 1.4.6 2a7.5 7.5 0 0 0 1.7 2.3 7.3 7.3 0 0 0 2.2 1.4 7.1 7.1 0 0 0 4.6.2 7.2 7.2 0 0 0 2.4-1.2 7.5 7.5 0 0 0 2.1-2.7 7.8 7.8 0 0 0 .7-2.4V71a9.3 9.3 0 0 0 .1.6 7.6 7.6 0 0 0 .6 2.5 7.5 7.5 0 0 0 2.4 3 7.1 7.1 0 0 0 7 .8 7.3 7.3 0 0 0 2.3-1.5 7.5 7.5 0 0 0 1.6-2.3 7.6 7.6 0 0 0 .5-2l.1-1.1v-6.7l.4.1a12.2 12.2 0 0 0 2 .5 11.1 11.1 0 0 0 2.5 0h.8l1.2-.1a9.5 9.5 0 0 0 1.4-.2l.9-.3a3.5 3.5 0 0 0 .6-.4l1.2-1.4a12.2 12.2 0 0 0 .8-1.2c0-.3.2-.5.3-.7a15.9 15.9 0 0 0 .7-2l.3-1.6v-1.3l.2-.9V54.6a15.5 15.5 0 0 0 1.8 0 4.5 4.5 0 0 0 1.4-.5 5.7 5.7 0 0 0 2.5-3.2 7.6 7.6 0 0 0 .4-1.5v-.3l-.4-1.4a5.2 5.2 0 0 1-.2-.1l-.4-.4a3.8 3.8 0 0 0-.2 0 1.4 1.4 0 0 0-.5-.2l-1.4.4-.7 1.3v.7a5.7 5.7 0 0 1-.1.8l-.7 1.4a1.9 1.9 0 0 1-.5.3h-.3a9.6 9.6 0 0 1-.8.3 8.8 8.8 0 0 1-.6 0l.2-.4.2-.5.2-.3v-.4l.1-.2V50l.1-1 .1-.6v-.6a4.8 4.8 0 0 0 0-.8v-.2l-1-1.1-1.5-.2-1.1 1-.2 1.4v.1l.2.4.2.3v.4l.1 1.1v.3l.1.5v.8a9.6 9.6 0 0 1-.8-.3l-.2-.1h-.3l-.8-.1h-.2a1.6 1.6 0 0 1-.2-.2.9.9 0 0 1-.2-.2 1 1 0 0 1-.1-.5l.2-.9v-1.2l-.9-.8h-1.2l-.8.9v.3a4.8 4.8 0 0 0-.3 2l.3.9a3.5 3.5 0 0 0 1.2 1.6l1 .5.8.2 1.4.1h.4l.2.1a12.1 12.1 0 0 1-1 2.6 13.2 13.2 0 0 1-.8 1.5 9.5 9.5 0 0 1-1 1.2l-.2.3a1.7 1.7 0 0 1-.4.3 2.4 2.4 0 0 1-.7.2h-2.5a7.8 7.8 0 0 1-.6-.2l-.7-.2h-.2a14.8 14.8 0 0 1-.6-.2 23.4 23.4 0 0 1-.4-.1l-.4-.1-.3-.1V43.9a34.6 34.6 0 0 0 0-.6 23.6 23.6 0 0 0-.4-3 22.7 22.7 0 0 0-1.5-4.7 22.6 22.6 0 0 0-4.6-6.7 21.9 21.9 0 0 0-6.9-4.7 21.2 21.2 0 0 0-8.1-1.8H92zm9.1 33.7l.3.1a1 1 0 0 1 .6.8v.4a8.4 8.4 0 0 1 0 .5 8.8 8.8 0 0 1-1.6 4.2l-1 1.3A10 10 0 0 1 95 66c-1.3.3-2.7.4-4 .3a10.4 10.4 0 0 1-2.7-.8 10 10 0 0 1-3.6-2.5 9.3 9.3 0 0 1-.8-1 9 9 0 0 1-.7-1.2 8.6 8.6 0 0 1-.8-3.4V57a1 1 0 0 1 .3-.6 1 1 0 0 1 1.3-.2 1 1 0 0 1 .4.8v.4a6.5 6.5 0 0 0 .5 2.2 7 7 0 0 0 2.1 2.8l1 .6c2.6 1.6 6 1.6 8.5 0a8 8 0 0 0 1.1-.6 7.6 7.6 0 0 0 1.2-1.2 7 7 0 0 0 1-1.7 6.5 6.5 0 0 0 .4-2.5 1 1 0 0 1 .7-1h.4zM30.7 43.7c-15.5 1-28.5-6-30.1-16.4C-1.2 15.7 11.6 4 29 1.3 46.6-1.7 62.3 5.5 64 17.1c1.6 10.4-8.7 21-23.7 25a31.2 31.2 0 0 0 0 .9v.3a19 19 0 0 0 .1 1l.1.4.1.9a4.7 4.7 0 0 0 .5 1l.7 1a9.2 9.2 0 0 0 1.2 1l1.5.8.6.8-.7.6-1.1.3a11.2 11.2 0 0 1-2.6.4 8.6 8.6 0 0 1-3-.5 8.5 8.5 0 0 1-1-.4 11.2 11.2 0 0 1-1.8-1.2 13.3 13.3 0 0 1-1-1 18 18 0 0 1-.7-.6l-.4-.4a23.4 23.4 0 0 1-1.3-1.8l-.1-.1-.3-.5V45l-.3-.6v-.7zM83.1 36c3.6 0 6.5 3.2 6.5 7.1 0 4-3 7.2-6.5 7.2S76.7 47 76.7 43 79.6 36 83 36zm18 0c3.6 0 6.5 3.2 6.5 7.1 0 4-2.9 7.2-6.4 7.2S94.7 47 94.7 43s3-7.1 6.5-7.1zm-18 6.1c2 0 3.5 1.6 3.5 3.6S85 49.2 83 49.2s-3.4-1.6-3.4-3.6S81.2 42 83 42zm17.9 0c1.9 0 3.4 1.6 3.4 3.6s-1.5 3.6-3.4 3.6c-2 0-3.5-1.6-3.5-3.6S99.1 42 101 42zM17 28c-.3 1.6-1.8 5-5.2 5.8-2.5.6-4.1-.8-4.5-2.6-.4-1.9.7-3.5 2.1-4.5A3.5 3.5 0 0 1 8 24.6c-.4-2 .8-3.7 3.2-4.2 1.9-.5 3.1.2 3.4 1.5.3 1.1-.5 2.2-1.8 2.5-.9.3-1.6 0-1.7-.6a1.4 1.4 0 0 1 0-.7s.3.2 1 0c.7-.1 1-.7.9-1.2-.2-.6-1-.8-1.8-.6-1 .2-2 1-1.7 2.6.3 1 .9 1.6 1.5 1.8l.7-.2c1-.2 1.5 0 1.6.5 0 .4-.2 1-1.2 1.2a3.3 3.3 0 0 1-1.5 0c-.9.7-1.6 1.9-1.3 3.2.3 1.3 1.3 2.2 3 1.8 2.5-.7 3.8-3.7 4.2-5-.3-.5-.6-1-.7-1.6-.1-.5.1-1 .9-1.2.4 0 .7.2.8.8a2.8 2.8 0 0 1 0 1l.7 1c.6-2 1.4-4 1.7-4 .6-.2 1.5.6 1.5.6-.8.7-1.7 2.4-2.3 4.2.8.6 1.6 1 2.1 1 .5-.1.8-.6 1-1.2-.3-2.2 1-4.3 2.3-4.6.7-.2 1.3.2 1.4.8.1.5 0 1.3-.9 1.7-.2-1-.6-1.3-1-1.3-.4.1-.7 1.4-.4 2.8.2 1 .7 1.5 1.3 1.4.8-.2 1.3-1.2 1.7-2.1-.3-2.1.9-4.2 2.2-4.5.7-.2 1.2.1 1.4 1 .4 1.4-1 2.8-2.2 3.4.3.7.7 1 1.3.9 1-.3 1.6-1.5 2-2.5l-.5-3v-.3s1.6-.3 1.8.6v.1c.2-.6.7-1.2 1.3-1.4.8-.1 1.5.6 1.7 1.6.5 2.2-.5 4.4-1.8 4.7H33a31.9 31.9 0 0 0 1 5.2c-.4.1-1.8.4-2-.4l-.5-5.6c-.5 1-1.3 2.2-2.5 2.4-1 .3-1.6-.3-2-1.1-.5 1-1.3 2.1-2.4 2.4-.8.2-1.5-.1-2-1-.3.8-.9 1.5-1.5 1.7-.7.1-1.5-.3-2.4-1-.3.8-.4 1.6-.4 2.2 0 0-.7 0-.8-.4-.1-.5 0-1.5.3-2.7a10.3 10.3 0 0 1-.7-.8zm38.2-17.8l.2.9c.5 1.9.4 4.4.8 6.4 0 .6-.4 3-1.4 3.3-.2 0-.3 0-.4-.4-.1-.7 0-1.6-.3-2.6-.2-1.1-.8-1.6-1.5-1.5-.8.2-1.3 1-1.6 2l-.1-.5c-.2-1-1.8-.6-1.8-.6a6.2 6.2 0 0 1 .4 1.3l.2 1c-.2.5-.6 1-1.2 1l-.2.1a7 7 0 0 0-.1-.8c-.3-1.1-1-2-1.6-1.8a.7.7 0 0 0-.4.3c-1.3.3-2.4 2-2.1 3.9-.2.9-.6 1.7-1 1.9-.5 0-.8-.5-1.1-1.8l-.1-1.2a4 4 0 0 0 0-1.7c0-.4-.4-.7-.8-.6-.7.2-.9 1.7-.5 3.8-.2 1-.6 2-1.3 2-.4.2-.8-.2-1-1l-.2-3c1.2-.5 2-1 1.8-1.7-.1-.5-.8-.7-.8-.7s0 .7-1 1.2l-.2-1.4c-.1-.6-.4-1-1.7-.6l.4 1 .2 1.5h-1v.8c0 .3.4.3 1 .2 0 1.3 0 2.7.2 3.6.3 1.4 1.2 2 2 1.7 1-.2 1.6-1.3 2-2.3.3 1.2 1 2 1.9 1.7.7-.2 1.2-1.1 1.6-2.2.4.8 1.1 1.1 2 1 1.2-.4 1.7-1.6 1.8-2.8h.2c.6-.2 1-.6 1.3-1 0 .8 0 1.5.2 2.1.1.5.3.7.6.6.5-.1 1-.9 1-.9a4 4 0 0 1-.3-1c-.3-1.3.3-3.6 1-3.7.2 0 .3.2.5.7v.8l.2 1.5v.7c.2.7.7 1.3 1.5 1 1.3-.2 2-2.6 2.1-3.9.3.2.6.2 1 .1-.6-2.2 0-6.1-.3-7.9-.1-.4-1-.5-1.7-.5h-.4zm-21.5 12c.4 0 .7.3 1 1.1.2 1.3-.3 2.6-.9 2.8-.2 0-.7 0-1-1.2v-.4c0-1.3.4-2 1-2.2zm-5.2 1c.3 0 .6.2.6.5.2.6-.3 1.3-1.2 2-.3-1.4.1-2.3.6-2.5zm18-.4c-.5.2-1-.4-1.2-1.2-.2-1 0-2.1.7-2.5v.5c.2.7.6 1.5 1.3 1.9 0 .7-.2 1.2-.7 1.3zm10-1.6c0 .5.4.7 1 .6.8-.2 1-1 .8-1.6 0-.5-.4-1-1-.8-.5.1-1 .9-.8 1.8zm-14.3-5.5c0-.4-.5-.7-1-.5-.8.2-1 1-.9 1.5.2.6.5 1 1 .8.5 0 1.1-1 1-1.8z" fill="#fff" fill-opacity=".6"/><?= $this->addElementToGhost(); ?></svg> error-handler/Resources/assets/images/icon-copy.svg 0000644 00000000411 15025017654 0016453 0 ustar 00 <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>