This commit is contained in:
Sebastian Molenda
2026-05-12 21:10:38 +02:00
commit ab96d82fcf
2544 changed files with 721700 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.1.6
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Utility;
use InvalidArgumentException;
/**
* Cookie Crypt Trait.
*
* Provides the encrypt/decrypt logic for the CookieComponent.
*
* @link https://book.cakephp.org/5/en/controllers/components/cookie.html
*/
trait CookieCryptTrait
{
/**
* Valid cipher names for encrypted cookies.
*
* @var array<string>
*/
protected array $_validCiphers = ['aes'];
/**
* Returns the encryption key to be used.
*
* @return string
*/
abstract protected function _getCookieEncryptionKey(): string;
/**
* Encrypts $value using public $type method in Security class
*
* @param array|string $value Value to encrypt
* @param string|false $encrypt Encryption mode to use. False
* disabled encryption.
* @param string|null $key Used as the security salt if specified.
* @return string Encoded values
*/
protected function _encrypt(array|string $value, string|false $encrypt, ?string $key = null): string
{
if (is_array($value)) {
$value = $this->_implode($value);
}
if ($encrypt === false) {
return $value;
}
$this->_checkCipher($encrypt);
$prefix = 'Q2FrZQ==.';
$cipher = '';
$key ??= $this->_getCookieEncryptionKey();
if ($encrypt === 'aes') {
$cipher = Security::encrypt($value, $key);
}
return $prefix . base64_encode($cipher);
}
/**
* Helper method for validating encryption cipher names.
*
* @param string $encrypt The cipher name.
* @return void
* @throws \RuntimeException When an invalid cipher is provided.
*/
protected function _checkCipher(string $encrypt): void
{
if (!in_array($encrypt, $this->_validCiphers, true)) {
$msg = sprintf(
'Invalid encryption cipher. Must be one of %s or false.',
implode(', ', $this->_validCiphers),
);
throw new InvalidArgumentException($msg);
}
}
/**
* Decrypts $value using public $type method in Security class
*
* @param array<string>|string $values Values to decrypt
* @param string|false $mode Encryption mode
* @param string|null $key Used as the security salt if specified.
* @return array|string Decrypted values
*/
protected function _decrypt(array|string $values, string|false $mode, ?string $key = null): array|string
{
if (is_string($values)) {
return $this->_decode($values, $mode, $key);
}
$decrypted = [];
foreach ($values as $name => $value) {
$decrypted[$name] = $this->_decode($value, $mode, $key);
}
return $decrypted;
}
/**
* Decodes and decrypts a single value.
*
* @param string $value The value to decode & decrypt.
* @param string|false $encrypt The encryption cipher to use.
* @param string|null $key Used as the security salt if specified.
* @return array|string Decoded values.
*/
protected function _decode(string $value, string|false $encrypt, ?string $key): array|string
{
if (!$encrypt) {
return $this->_explode($value);
}
$this->_checkCipher($encrypt);
$prefix = 'Q2FrZQ==.';
$prefixLength = strlen($prefix);
if (strncmp($value, $prefix, $prefixLength) !== 0) {
return '';
}
$value = base64_decode(substr($value, $prefixLength), true);
if ($value === false || $value === '') {
return '';
}
$key ??= $this->_getCookieEncryptionKey();
if ($encrypt === 'aes') {
$value = Security::decrypt($value, $key);
}
if ($value === null) {
return '';
}
return $this->_explode($value);
}
/**
* Implode method to keep keys are multidimensional arrays
*
* @param array $array Map of key and values
* @return string A JSON encoded string.
*/
protected function _implode(array $array): string
{
return json_encode($array, JSON_THROW_ON_ERROR);
}
/**
* Explode method to return array from string set in CookieComponent::_implode()
* Maintains reading backwards compatibility with 1.x CookieComponent::_implode().
*
* @param string $string A string containing JSON encoded data, or a bare string.
* @return array|string Map of key and values
*/
protected function _explode(string $string): array|string
{
$first = substr($string, 0, 1);
if ($first === '{' || $first === '[') {
return json_decode($string, true) ?? $string;
}
$array = [];
foreach (explode(',', $string) as $pair) {
$key = explode('|', $pair);
if (!isset($key[1])) {
return $key[0];
}
$array[$key[0]] = $key[1];
}
return $array;
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Utility\Crypto;
use Cake\Core\Exception\CakeException;
/**
* OpenSSL implementation of crypto features for Cake\Utility\Security
*
* This class is not intended to be used directly and should only
* be used in the context of {@link \Cake\Utility\Security}.
*
* @internal
*/
class OpenSsl
{
/**
* @var string
*/
protected const METHOD_AES_256_CBC = 'aes-256-cbc';
/**
* Encrypt a value using AES-256.
*
* *Caveat* You cannot properly encrypt/decrypt data with trailing null bytes.
* Any trailing null bytes will be removed on decryption due to how PHP pads messages
* with nulls prior to encryption.
*
* @param string $plain The value to encrypt.
* @param string $key The 256 bit/32 byte key to use as a cipher key.
* @return string Encrypted data.
* @throws \InvalidArgumentException On invalid data or key.
*/
public static function encrypt(string $plain, string $key): string
{
$method = static::METHOD_AES_256_CBC;
$ivSize = openssl_cipher_iv_length($method);
if ($ivSize === false) {
throw new CakeException(sprintf('Cannot get the cipher iv length for `%s`', $method));
}
$iv = openssl_random_pseudo_bytes($ivSize);
return $iv . openssl_encrypt($plain, $method, $key, OPENSSL_RAW_DATA, $iv);
}
/**
* Decrypt a value using AES-256.
*
* @param string $cipher The ciphertext to decrypt.
* @param string $key The 256 bit/32 byte key to use as a cipher key.
* @return string|null Decrypted data. Any trailing null bytes will be removed.
* @throws \InvalidArgumentException On invalid data or key.
*/
public static function decrypt(string $cipher, string $key): ?string
{
$method = static::METHOD_AES_256_CBC;
$ivSize = openssl_cipher_iv_length($method);
if ($ivSize === false) {
throw new CakeException(sprintf('Cannot get the cipher iv length for `%s`', $method));
}
$iv = mb_substr($cipher, 0, $ivSize, '8bit');
$cipher = mb_substr($cipher, $ivSize, null, '8bit');
$value = openssl_decrypt($cipher, $method, $key, OPENSSL_RAW_DATA, $iv);
if ($value === false) {
return null;
}
return $value;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Utility\Exception;
use Cake\Core\Exception\CakeException;
/**
* Exception class for Xml. This exception will be thrown from Xml when it
* encounters an error.
*/
class XmlException extends CakeException
{
}
+278
View File
@@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Utility;
use Cake\Core\Exception\CakeException;
use CallbackFilterIterator;
use Closure;
use FilesystemIterator;
use Iterator;
use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use SplFileInfo;
/**
* This provides convenience wrappers around common filesystem queries.
*
* This is an internal helper class that should not be used in application code
* as it provides no guarantee for compatibility.
*
* @internal
*/
class Filesystem
{
/**
* Directory type constant
*
* @var string
*/
public const TYPE_DIR = 'dir';
/**
* Find files / directories (non-recursively) in given directory path.
*
* @param string $path Directory path.
* @param \Closure|string|null $filter If string will be used as regex for filtering using
* `RegexIterator`, if callable will be as callback for `CallbackFilterIterator`.
* @param int|null $flags Flags for FilesystemIterator::__construct();
* @return \Iterator
*/
public function find(string $path, Closure|string|null $filter = null, ?int $flags = null): Iterator
{
$flags ??= FilesystemIterator::KEY_AS_PATHNAME
| FilesystemIterator::CURRENT_AS_FILEINFO
| FilesystemIterator::SKIP_DOTS;
$directory = new FilesystemIterator($path, $flags);
if ($filter === null) {
return $directory;
}
return $this->filterIterator($directory, $filter);
}
/**
* Find files/ directories recursively in given directory path.
*
* @param string $path Directory path.
* @param \Closure|string|null $filter If string will be used as regex for filtering using
* `RegexIterator`, if callable will be as callback for `CallbackFilterIterator`.
* Hidden directories (starting with dot e.g. .git) are always skipped.
* @param int|null $flags Flags for FilesystemIterator::__construct();
* @return \Iterator
*/
public function findRecursive(string $path, Closure|string|null $filter = null, ?int $flags = null): Iterator
{
$flags ??= FilesystemIterator::KEY_AS_PATHNAME
| FilesystemIterator::CURRENT_AS_FILEINFO
| FilesystemIterator::SKIP_DOTS;
$directory = new RecursiveDirectoryIterator($path, $flags);
$dirFilter = new RecursiveCallbackFilterIterator(
$directory,
function (SplFileInfo $current) {
if (str_starts_with($current->getFilename(), '.') && $current->isDir()) {
return false;
}
return true;
},
);
$flatten = new RecursiveIteratorIterator(
$dirFilter,
RecursiveIteratorIterator::CHILD_FIRST,
);
if ($filter === null) {
return $flatten;
}
return $this->filterIterator($flatten, $filter);
}
/**
* Wrap iterator in additional filtering iterator.
*
* @param \Iterator $iterator Iterator
* @param \Closure|string $filter Regex string or callback.
* @return \Iterator
*/
protected function filterIterator(Iterator $iterator, Closure|string $filter): Iterator
{
if (is_string($filter)) {
return new RegexIterator($iterator, $filter);
}
return new CallbackFilterIterator($iterator, $filter);
}
/**
* Dump contents to file.
*
* @param string $filename File path.
* @param string $content Content to dump.
* @return void
* @throws \Cake\Core\Exception\CakeException When dumping fails.
*/
public function dumpFile(string $filename, string $content): void
{
$dir = dirname($filename);
if (!is_dir($dir)) {
$this->mkdir($dir);
}
$exists = file_exists($filename);
if ($this->isStream($filename)) {
// phpcs:ignore
$success = @file_put_contents($filename, $content);
} else {
// phpcs:ignore
$success = @file_put_contents($filename, $content, LOCK_EX);
}
if ($success === false) {
throw new CakeException(sprintf('Failed dumping content to file `%s`', $dir));
}
if (!$exists) {
chmod($filename, 0666 & ~umask());
}
}
/**
* Create directory.
*
* @param string $dir Directory path.
* @param int $mode Octal mode passed to mkdir(). Defaults to 0777.
* @return void
* @throws \Cake\Core\Exception\CakeException When directory creation fails.
*/
public function mkdir(string $dir, int $mode = 0777): void
{
if (is_dir($dir)) {
return;
}
$old = umask(0);
// phpcs:ignore
if (@mkdir($dir, $mode, true) === false) {
umask($old);
throw new CakeException(sprintf('Failed to create directory `%s`', $dir));
}
umask($old);
}
/**
* Delete directory along with all it's contents.
*
* @param string $path Directory path.
* @return bool
* @throws \Cake\Core\Exception\CakeException If path is not a directory.
*/
public function deleteDir(string $path): bool
{
if (!file_exists($path)) {
return true;
}
if (!is_dir($path)) {
throw new CakeException(sprintf('`%s` is not a directory', $path));
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST,
);
$result = true;
/** @var \SplFileInfo $fileInfo */
foreach ($iterator as $fileInfo) {
$isWindowsLink = DIRECTORY_SEPARATOR === '\\' && $fileInfo->getType() === 'link';
if ($fileInfo->getType() === self::TYPE_DIR || $isWindowsLink) {
// phpcs:ignore
$result = $result && @rmdir($fileInfo->getPathname());
unset($fileInfo);
continue;
}
// phpcs:ignore
$result = $result && @unlink($fileInfo->getPathname());
// possible inner iterators need to be unset too in order for locks on parents to be released
unset($fileInfo);
}
// unsetting iterators helps releasing possible locks in certain environments,
// which could otherwise make `rmdir()` fail
unset($iterator);
// phpcs:ignore
return $result && @rmdir($path);
}
/**
* Copies directory with all it's contents.
*
* @param string $source Source path.
* @param string $destination Destination path.
* @return bool
*/
public function copyDir(string $source, string $destination): bool
{
$destination = (new SplFileInfo($destination))->getPathname();
if (!is_dir($destination)) {
$this->mkdir($destination);
}
/** @var \FilesystemIterator<\SplFileInfo> $iterator */
$iterator = new FilesystemIterator($source);
$result = true;
foreach ($iterator as $fileInfo) {
if ($fileInfo->isDir()) {
$result = $result && $this->copyDir(
$fileInfo->getPathname(),
$destination . DIRECTORY_SEPARATOR . $fileInfo->getFilename(),
);
} else {
// phpcs:ignore
$result = $result && @copy(
$fileInfo->getPathname(),
$destination . DIRECTORY_SEPARATOR . $fileInfo->getFilename(),
);
}
}
return $result;
}
/**
* Check whether the given path is a stream path.
*
* @param string $path Path.
* @return bool
*/
public function isStream(string $path): bool
{
return str_contains($path, '://');
}
}
File diff suppressed because it is too large Load Diff
+524
View File
@@ -0,0 +1,524 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 0.2.9
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Utility;
/**
* Pluralize and singularize English words.
*
* Inflector pluralizes and singularizes English nouns.
* Used by CakePHP's naming conventions throughout the framework.
*
* @link https://book.cakephp.org/5/en/core-libraries/inflector.html
*/
class Inflector
{
/**
* Plural inflector rules
*
* @var array<string, string>
*/
protected static array $_plural = [
'/(s)tatus$/i' => '\1tatuses',
'/(quiz)$/i' => '\1zes',
'/^(ox)$/i' => '\1\2en',
'/([m|l])ouse$/i' => '\1ice',
'/(matr|vert)(ix|ex)$/i' => '\1ices',
'/(x|ch|ss|sh)$/i' => '\1es',
'/([^aeiouy]|qu)y$/i' => '\1ies',
'/(hive)$/i' => '\1s',
'/(chef)$/i' => '\1s',
'/(?:([^f])fe|([lre])f)$/i' => '\1\2ves',
'/sis$/i' => 'ses',
'/([ti])um$/i' => '\1a',
'/(p)erson$/i' => '\1eople',
'/(?<!u)(m)an$/i' => '\1en',
'/(c)hild$/i' => '\1hildren',
'/(buffal|tomat)o$/i' => '\1\2oes',
'/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin)us$/i' => '\1i',
'/us$/i' => 'uses',
'/(alias)$/i' => '\1es',
'/(ax|cris|test)is$/i' => '\1es',
'/s$/' => 's',
'/^$/' => '',
'/$/' => 's',
];
/**
* Singular inflector rules
*
* @var array<string, string>
*/
protected static array $_singular = [
'/(s)tatuses$/i' => '\1\2tatus',
'/^(.*)(menu)s$/i' => '\1\2',
'/(quiz)zes$/i' => '\\1',
'/(matr)ices$/i' => '\1ix',
'/(vert|ind)ices$/i' => '\1ex',
'/^(ox)en/i' => '\1',
'/(alias|lens)(es)*$/i' => '\1',
'/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us',
'/([ftw]ax)es/i' => '\1',
'/(cris|ax|test)es$/i' => '\1is',
'/(shoe)s$/i' => '\1',
'/(o)es$/i' => '\1',
'/ouses$/' => 'ouse',
'/([^a])uses$/' => '\1us',
'/([m|l])ice$/i' => '\1ouse',
'/(x|ch|ss|sh)es$/i' => '\1',
'/(m)ovies$/i' => '\1\2ovie',
'/(s)eries$/i' => '\1\2eries',
'/(s)pecies$/i' => '\1\2pecies',
'/([^aeiouy]|qu)ies$/i' => '\1y',
'/(tive)s$/i' => '\1',
'/(hive)s$/i' => '\1',
'/(drive)s$/i' => '\1',
'/([le])ves$/i' => '\1f',
'/([^rfoa])ves$/i' => '\1fe',
'/(^analy)ses$/i' => '\1sis',
'/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
'/([ti])a$/i' => '\1um',
'/(p)eople$/i' => '\1\2erson',
'/(m)en$/i' => '\1an',
'/(c)hildren$/i' => '\1\2hild',
'/(n)ews$/i' => '\1\2ews',
'/eaus$/' => 'eau',
'/^(.*us)$/' => '\\1',
'/s$/i' => '',
];
/**
* Irregular rules
*
* @var array<string, string>
*/
protected static array $_irregular = [
'atlas' => 'atlases',
'beef' => 'beefs',
'brief' => 'briefs',
'brother' => 'brothers',
'cafe' => 'cafes',
'child' => 'children',
'cookie' => 'cookies',
'corpus' => 'corpuses',
'cow' => 'cows',
'criterion' => 'criteria',
'ganglion' => 'ganglions',
'genie' => 'genies',
'genus' => 'genera',
'graffito' => 'graffiti',
'hoof' => 'hoofs',
'loaf' => 'loaves',
'man' => 'men',
'money' => 'monies',
'mongoose' => 'mongooses',
'move' => 'moves',
'mythos' => 'mythoi',
'niche' => 'niches',
'numen' => 'numina',
'occiput' => 'occiputs',
'octopus' => 'octopuses',
'opus' => 'opuses',
'ox' => 'oxen',
'penis' => 'penises',
'person' => 'people',
'sex' => 'sexes',
'soliloquy' => 'soliloquies',
'testis' => 'testes',
'trilby' => 'trilbys',
'turf' => 'turfs',
'potato' => 'potatoes',
'hero' => 'heroes',
'tooth' => 'teeth',
'goose' => 'geese',
'foot' => 'feet',
'foe' => 'foes',
'sieve' => 'sieves',
'cache' => 'caches',
];
/**
* Words that should not be inflected
*
* @var array<string>
*/
protected static array $_uninflected = [
'.*[nrlm]ese', '.*data', '.*deer', '.*fish', '.*measles', '.*ois',
'.*pox', '.*sheep', 'people', 'feedback', 'stadia', '.*?media',
'chassis', 'clippers', 'debris', 'diabetes', 'equipment', 'gallows',
'graffiti', 'headquarters', 'information', 'innings', 'news', 'nexus',
'pokemon', 'proceedings', 'research', 'sea[- ]bass', 'series', 'species', 'weather',
];
/**
* Method cache array.
*
* @var array<string, mixed>
*/
protected static array $_cache = [];
/**
* The initial state of Inflector so reset() works.
*
* @var array
*/
protected static array $_initialState = [];
/**
* Cache inflected values, and return if already available
*
* @param string $type Inflection type
* @param string $key Original value
* @param string|false $value Inflected value
* @return string|false Inflected value on cache hit or false on cache miss.
*/
protected static function _cache(string $type, string $key, string|false $value = false): string|false
{
$key = '_' . $key;
$type = '_' . $type;
if ($value !== false) {
static::$_cache[$type][$key] = $value;
return $value;
}
if (!isset(static::$_cache[$type][$key])) {
return false;
}
return static::$_cache[$type][$key];
}
/**
* Clears Inflectors inflected value caches. And resets the inflection
* rules to the initial values.
*
* @return void
*/
public static function reset(): void
{
if (static::$_initialState === []) {
static::$_initialState = get_class_vars(self::class);
return;
}
foreach (static::$_initialState as $key => $val) {
if ($key !== '_initialState') {
static::${$key} = $val;
}
}
}
/**
* Adds custom inflection $rules, of either 'plural', 'singular',
* 'uninflected' or 'irregular' $type.
*
* ### Usage:
*
* ```
* Inflector::rules('plural', ['/^(inflect)or$/i' => '\1ables']);
* Inflector::rules('irregular', ['red' => 'redlings']);
* Inflector::rules('uninflected', ['dontinflectme']);
* ```
*
* @param string $type The type of inflection, either 'plural', 'singular',
* or 'uninflected'.
* @param array $rules Array of rules to be added.
* @param bool $reset If true, will unset default inflections for all
* new rules that are being defined in $rules.
* @return void
*/
public static function rules(string $type, array $rules, bool $reset = false): void
{
$var = '_' . $type;
if ($reset) {
static::${$var} = $rules;
} elseif ($type === 'uninflected') {
static::$_uninflected = array_merge(
$rules,
static::$_uninflected,
);
} else {
static::${$var} = $rules + static::${$var};
}
static::$_cache = [];
}
/**
* Return $word in plural form.
*
* @param string $word Word in singular
* @return string Word in plural
* @link https://book.cakephp.org/5/en/core-libraries/inflector.html#creating-plural-singular-forms
*/
public static function pluralize(string $word): string
{
if (isset(static::$_cache['pluralize'][$word])) {
return static::$_cache['pluralize'][$word];
}
if (!isset(static::$_cache['irregular']['pluralize'])) {
$words = array_keys(static::$_irregular);
static::$_cache['irregular']['pluralize'] = '/(.*?(?:\\b|_))(' . implode('|', $words) . ')$/i';
$upperWords = array_map('ucfirst', $words);
static::$_cache['irregular']['upperPluralize'] = '/(.*?(?:\\b|[a-z]))(' . implode('|', $upperWords) . ')$/';
}
if (
preg_match(static::$_cache['irregular']['pluralize'], $word, $regs) ||
preg_match(static::$_cache['irregular']['upperPluralize'], $word, $regs)
) {
static::$_cache['pluralize'][$word] = $regs[1] . substr($regs[2], 0, 1) .
substr(static::$_irregular[strtolower($regs[2])], 1);
return static::$_cache['pluralize'][$word];
}
if (!isset(static::$_cache['uninflected'])) {
static::$_cache['uninflected'] = '/^(' . implode('|', static::$_uninflected) . ')$/i';
}
if (preg_match(static::$_cache['uninflected'], $word, $regs)) {
static::$_cache['pluralize'][$word] = $word;
return $word;
}
foreach (static::$_plural as $rule => $replacement) {
if (preg_match($rule, $word)) {
static::$_cache['pluralize'][$word] = (string)preg_replace($rule, $replacement, $word);
return static::$_cache['pluralize'][$word];
}
}
return $word;
}
/**
* Return $word in singular form.
*
* @param string $word Word in plural
* @return string Word in singular
* @link https://book.cakephp.org/5/en/core-libraries/inflector.html#creating-plural-singular-forms
*/
public static function singularize(string $word): string
{
if (isset(static::$_cache['singularize'][$word])) {
return static::$_cache['singularize'][$word];
}
if (!isset(static::$_cache['irregular']['singular'])) {
$wordList = array_values(static::$_irregular);
static::$_cache['irregular']['singular'] = '/(.*?(?:\\b|_))(' . implode('|', $wordList) . ')$/i';
$upperWordList = array_map('ucfirst', $wordList);
static::$_cache['irregular']['singularUpper'] = '/(.*?(?:\\b|[a-z]))(' .
implode('|', $upperWordList) .
')$/';
}
if (
preg_match(static::$_cache['irregular']['singular'], $word, $regs) ||
preg_match(static::$_cache['irregular']['singularUpper'], $word, $regs)
) {
$suffix = array_search(strtolower($regs[2]), static::$_irregular, true);
$suffix = $suffix ? substr($suffix, 1) : '';
static::$_cache['singularize'][$word] = $regs[1] . substr($regs[2], 0, 1) . $suffix;
return static::$_cache['singularize'][$word];
}
if (!isset(static::$_cache['uninflected'])) {
static::$_cache['uninflected'] = '/^(' . implode('|', static::$_uninflected) . ')$/i';
}
if (preg_match(static::$_cache['uninflected'], $word, $regs)) {
static::$_cache['pluralize'][$word] = $word;
return $word;
}
foreach (static::$_singular as $rule => $replacement) {
if (preg_match($rule, $word)) {
static::$_cache['singularize'][$word] = (string)preg_replace($rule, $replacement, $word);
return static::$_cache['singularize'][$word];
}
}
static::$_cache['singularize'][$word] = $word;
return $word;
}
/**
* Returns the input lower_case_delimited_string as a CamelCasedString.
*
* @param string $string String to camelize
* @param string $delimiter the delimiter in the input string
* @return string CamelizedStringLikeThis.
* @link https://book.cakephp.org/5/en/core-libraries/inflector.html#creating-camelcase-and-under-scored-forms
*/
public static function camelize(string $string, string $delimiter = '_'): string
{
$cacheKey = __FUNCTION__ . $delimiter;
$result = static::_cache($cacheKey, $string);
if ($result === false) {
$result = str_replace(' ', '', static::humanize($string, $delimiter));
static::_cache($cacheKey, $string, $result);
}
return $result;
}
/**
* Returns the input CamelCasedString as an underscored_string.
*
* Also replaces dashes with underscores
*
* @param string $string CamelCasedString to be "underscorized"
* @return string underscore_version of the input string
* @link https://book.cakephp.org/5/en/core-libraries/inflector.html#creating-camelcase-and-under-scored-forms
*/
public static function underscore(string $string): string
{
return static::delimit(str_replace('-', '_', $string), '_');
}
/**
* Returns the input CamelCasedString as an dashed-string.
*
* Also replaces underscores with dashes
*
* @param string $string The string to dasherize.
* @return string Dashed version of the input string
*/
public static function dasherize(string $string): string
{
return static::delimit(str_replace('_', '-', $string), '-');
}
/**
* Returns the input lower_case_delimited_string as 'A Human Readable String'.
* (Underscores are replaced by spaces and capitalized following words.)
*
* @param string $string String to be humanized
* @param string $delimiter the character to replace with a space
* @return string Human-readable string
* @link https://book.cakephp.org/5/en/core-libraries/inflector.html#creating-human-readable-forms
*/
public static function humanize(string $string, string $delimiter = '_'): string
{
$cacheKey = __FUNCTION__ . $delimiter;
$result = static::_cache($cacheKey, $string);
if ($result === false) {
$result = explode(' ', str_replace($delimiter, ' ', $string));
foreach ($result as &$word) {
$word = mb_strtoupper(mb_substr($word, 0, 1)) . mb_substr($word, 1);
}
$result = implode(' ', $result);
static::_cache($cacheKey, $string, $result);
}
return $result;
}
/**
* Expects a CamelCasedInputString, and produces a lower_case_delimited_string
*
* @param string $string String to delimit
* @param string $delimiter the character to use as a delimiter
* @return string delimited string
*/
public static function delimit(string $string, string $delimiter = '_'): string
{
$cacheKey = __FUNCTION__ . $delimiter;
$result = static::_cache($cacheKey, $string);
if ($result === false) {
$result = mb_strtolower((string)preg_replace('/(?<=\\w)([A-Z])/', $delimiter . '\\1', $string));
static::_cache($cacheKey, $string, $result);
}
return $result;
}
/**
* Returns corresponding table name for given model $className. ("people" for the class name "Person").
*
* @param string $className Name of class to get database table name for
* @return string Name of the database table for given class
* @link https://book.cakephp.org/5/en/core-libraries/inflector.html#creating-table-and-class-name-forms
*/
public static function tableize(string $className): string
{
$result = static::_cache(__FUNCTION__, $className);
if ($result === false) {
$result = static::pluralize(static::underscore($className));
static::_cache(__FUNCTION__, $className, $result);
}
return $result;
}
/**
* Returns a singular, CamelCase inflection for given database table. ("Person" for the table name "people")
*
* @param string $tableName Name of database table to get class name for
* @return string Class name
* @link https://book.cakephp.org/5/en/core-libraries/inflector.html#creating-table-and-class-name-forms
*/
public static function classify(string $tableName): string
{
$result = static::_cache(__FUNCTION__, $tableName);
if ($result === false) {
$result = static::camelize(static::singularize($tableName));
static::_cache(__FUNCTION__, $tableName, $result);
}
return $result;
}
/**
* Returns camelBacked version of an underscored string.
*
* @param string $string String to convert.
* @return string in variable form
* @link https://book.cakephp.org/5/en/core-libraries/inflector.html#creating-variable-names
*/
public static function variable(string $string): string
{
$result = static::_cache(__FUNCTION__, $string);
if ($result === false) {
$camelized = static::camelize(static::underscore($string));
$replace = strtolower(substr($camelized, 0, 1));
$result = $replace . substr($camelized, 1);
static::_cache(__FUNCTION__, $string, $result);
}
return $result;
}
}
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org)
Copyright (c) 2005-2020, Cake Software Foundation, Inc. (https://cakefoundation.org)
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.
+116
View File
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Utility;
/**
* Provides features for merging object properties recursively with
* parent classes.
*/
trait MergeVariablesTrait
{
/**
* Merge the list of $properties with all parent classes of the current class.
*
* ### Options:
*
* - `associative` - A list of properties that should be treated as associative arrays.
* Properties in this list will be passed through Hash::normalize() before merging.
*
* @param array<string> $properties An array of properties and the merge strategy for them.
* @param array<string, mixed> $options The options to use when merging properties.
* @return void
*/
protected function _mergeVars(array $properties, array $options = []): void
{
$class = static::class;
$parents = [];
while (true) {
$parent = get_parent_class($class);
if (!$parent) {
break;
}
$parents[] = $parent;
$class = $parent;
}
foreach ($properties as $property) {
if (!property_exists($this, $property)) {
continue;
}
$thisValue = $this->{$property};
if ($thisValue === null || $thisValue === false) {
continue;
}
$this->_mergeProperty($property, $parents, $options);
}
}
/**
* Merge a single property with the values declared in all parent classes.
*
* @param string $property The name of the property being merged.
* @param array<string> $parentClasses An array of classes you want to merge with.
* @param array<string, mixed> $options Options for merging the property, see _mergeVars()
* @return void
*/
protected function _mergeProperty(string $property, array $parentClasses, array $options): void
{
$thisValue = $this->{$property};
$isAssoc = false;
if (
isset($options['associative']) &&
in_array($property, (array)$options['associative'], true)
) {
$isAssoc = true;
}
if ($isAssoc) {
$thisValue = Hash::normalize($thisValue);
}
foreach ($parentClasses as $class) {
$parentProperties = get_class_vars($class);
if (empty($parentProperties[$property])) {
continue;
}
$parentProperty = $parentProperties[$property];
if (!is_array($parentProperty)) {
continue;
}
$thisValue = $this->_mergePropertyData($thisValue, $parentProperty, $isAssoc);
}
$this->{$property} = $thisValue;
}
/**
* Merge each of the keys in a property together.
*
* @param array $current The current merged value.
* @param array $parent The parent class' value.
* @param bool $isAssoc Whether the merging should be done in associative mode.
* @return array The updated value.
*/
protected function _mergePropertyData(array $current, array $parent, bool $isAssoc): array
{
if (!$isAssoc) {
return array_merge($parent, $current);
}
$parent = Hash::normalize($parent);
foreach ($parent as $key => $value) {
$current[$key] ??= $value;
}
return $current;
}
}
+91
View File
@@ -0,0 +1,91 @@
[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/utility.svg?style=flat-square)](https://packagist.org/packages/cakephp/utility)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt)
# CakePHP Utility Classes
This library provides a range of utility classes that are used throughout the CakePHP framework
## What's in the toolbox?
### Hash
A ``Hash`` (as in PHP arrays) class, capable of extracting data using an intuitive DSL:
```php
$things = [
['name' => 'Mark', 'age' => 15],
['name' => 'Susan', 'age' => 30],
['name' => 'Lucy', 'age' => 25]
];
$bigPeople = Hash::extract($things, '{n}[age>21].name');
// $bigPeople will contain ['Susan', 'Lucy']
```
Check the [official Hash class documentation](https://book.cakephp.org/5/en/core-libraries/hash.html)
### Inflector
The Inflector class takes a string and can manipulate it to handle word variations
such as pluralizations or camelizing.
```php
echo Inflector::pluralize('Apple'); // echoes Apples
echo Inflector::singularize('People'); // echoes Person
```
Check the [official Inflector class documentation](https://book.cakephp.org/5/en/core-libraries/inflector.html)
### Text
The Text class includes convenience methods for creating and manipulating strings.
```php
Text::insert(
'My name is :name and I am :age years old.',
['name' => 'Bob', 'age' => '65']
);
// Returns: "My name is Bob and I am 65 years old."
$text = 'This is the song that never ends.';
$result = Text::wrap($text, 22);
// Returns
This is the song
that never ends.
```
Check the [official Text class documentation](https://book.cakephp.org/5/en/core-libraries/text.html)
### Security
The security library handles basic security measures such as providing methods for hashing and encrypting data.
```php
$key = 'wt1U5MACWJFTXGenFoZoiLwQGrLgdbHA';
$result = Security::encrypt($value, $key);
Security::decrypt($result, $key);
```
Check the [official Security class documentation](https://book.cakephp.org/5/en/core-libraries/security.html)
### Xml
The Xml class allows you to easily transform arrays into SimpleXMLElement or DOMDocument objects
and back into arrays again
```php
$data = [
'post' => [
'id' => 1,
'title' => 'Best post',
'body' => ' ... '
]
];
$xml = Xml::build($data);
```
Check the [official Xml class documentation](https://book.cakephp.org/5/en/core-libraries/xml.html)
+308
View File
@@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 0.10.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Utility;
use Cake\Core\Exception\CakeException;
use Cake\Utility\Crypto\OpenSsl;
use InvalidArgumentException;
/**
* Security Library contains utility methods related to security
*/
class Security
{
/**
* Default hash method. If `$type` param for `Security::hash()` is not specified
* this value is used. Defaults to 'sha1'.
*
* @var string
*/
public static string $hashType = 'sha1';
/**
* The HMAC salt to use for encryption and decryption routines
*
* @var string|null
*/
protected static ?string $_salt = null;
/**
* The crypto implementation to use.
*
* @var object|null
*/
protected static ?object $_instance = null;
/**
* Create a hash from string using given method.
*
* @param string $string String to hash
* @param string|null $algorithm Hashing algo to use (i.e. sha1, sha256 etc.).
* Can be any valid algo included in list returned by hash_algos().
* If no value is passed the type specified by `Security::$hashType` is used.
* @param string|bool $salt If true, automatically prepends the value returned by
* Security::getSalt() to $string.
* @return string Hash
* @throws \InvalidArgumentException
* @link https://book.cakephp.org/5/en/core-libraries/security.html#hashing-data
*/
public static function hash(string $string, ?string $algorithm = null, string|bool $salt = false): string
{
if (!$algorithm) {
$algorithm = static::$hashType;
}
$algorithm = strtolower($algorithm);
$availableAlgorithms = hash_algos();
if (!in_array($algorithm, $availableAlgorithms, true)) {
throw new InvalidArgumentException(sprintf(
'The hash type `%s` was not found. Available algorithms are: `%s`.',
$algorithm,
implode(', ', $availableAlgorithms),
));
}
if ($salt) {
if (!is_string($salt)) {
$salt = static::getSalt();
}
$string = $salt . $string;
}
return hash($algorithm, $string);
}
/**
* Sets the default hash method for the Security object. This affects all objects
* using Security::hash().
*
* @param string $hash Method to use (sha1/sha256/md5 etc.)
* @return void
* @see \Cake\Utility\Security::hash()
*/
public static function setHash(string $hash): void
{
static::$hashType = $hash;
}
/**
* Get random bytes from a secure source.
*
* This method will fall back to an insecure source and trigger a warning
* if it cannot find a secure source of random data.
*
* @param int $length The number of bytes you want.
* @return string Random bytes in binary.
*/
public static function randomBytes(int $length): string
{
if ($length < 1) {
throw new InvalidArgumentException('Length must be `int<1, max>`');
}
return random_bytes($length);
}
/**
* Creates a secure random string.
*
* @param int $length String length. Default 64.
* @return string
*/
public static function randomString(int $length = 64): string
{
return substr(
bin2hex(Security::randomBytes((int)ceil($length / 2))),
0,
$length,
);
}
/**
* Like randomBytes() above, but not cryptographically secure.
*
* @param int $length The number of bytes you want.
* @return string Random bytes in binary.
* @see \Cake\Utility\Security::randomBytes()
*/
public static function insecureRandomBytes(int $length): string
{
$length *= 2;
$bytes = '';
$byteLength = 0;
while ($byteLength < $length) {
$bytes .= static::hash(Text::uuid() . uniqid((string)mt_rand(), true), 'sha512', true);
$byteLength = strlen($bytes);
}
$bytes = substr($bytes, 0, $length);
return pack('H*', $bytes);
}
/**
* Get the crypto implementation based on the loaded extensions.
*
* You can use this method to forcibly decide between openssl/custom implementations.
*
* @param \Cake\Utility\Crypto\OpenSsl|null $instance The crypto instance to use. If provided, sets and returns this instance.
* If null, returns the currently configured engine or creates a new OpenSsl instance.
* @return \Cake\Utility\Crypto\OpenSsl Crypto instance. By default, returns a \Cake\Utility\Crypto\OpenSsl instance.
* @throws \InvalidArgumentException When no compatible crypto extension is available.
*/
public static function engine(?object $instance = null): object
{
if ($instance) {
return static::$_instance = $instance;
}
if (isset(static::$_instance)) {
/** @var \Cake\Utility\Crypto\OpenSsl */
return static::$_instance;
}
if (extension_loaded('openssl')) {
return static::$_instance = new OpenSsl();
}
throw new InvalidArgumentException(
'No compatible crypto engine available. ' .
'Load the openssl extension.',
);
}
/**
* Encrypt a value using AES-256.
*
* *Caveat* You cannot properly encrypt/decrypt data with trailing null bytes.
* Any trailing null bytes will be removed on decryption due to how PHP pads messages
* with nulls prior to encryption.
*
* @param string $plain The value to encrypt.
* @param string $key The 256 bit/32 byte key to use as a cipher key.
* @param string|null $hmacSalt The salt to use for the HMAC process.
* Leave null to use value of Security::getSalt().
* @return string Encrypted data.
* @throws \InvalidArgumentException On invalid data or key.
*/
public static function encrypt(string $plain, string $key, ?string $hmacSalt = null): string
{
self::_checkKey($key, 'encrypt()');
$hmacSalt ??= static::getSalt();
// Generate the encryption and hmac key.
$key = mb_substr(hash('sha256', $key . $hmacSalt), 0, 32, '8bit');
$crypto = static::engine();
$ciphertext = $crypto->encrypt($plain, $key);
$hmac = hash_hmac('sha256', $ciphertext, $key);
return $hmac . $ciphertext;
}
/**
* Check the encryption key for proper length.
*
* @param string $key Key to check.
* @param string $method The method the key is being checked for.
* @return void
* @throws \InvalidArgumentException When key length is not 256 bit/32 bytes
*/
protected static function _checkKey(string $key, string $method): void
{
if (mb_strlen($key, '8bit') < 32) {
throw new InvalidArgumentException(
sprintf('Invalid key for %s, key must be at least 256 bits (32 bytes) long.', $method),
);
}
}
/**
* Decrypt a value using AES-256.
*
* @param string $cipher The ciphertext to decrypt.
* @param string $key The 256 bit/32 byte key to use as a cipher key.
* @param string|null $hmacSalt The salt to use for the HMAC process.
* Leave null to use value of Security::getSalt().
* @return string|null Decrypted data. Any trailing null bytes will be removed.
* @throws \InvalidArgumentException On invalid data or key.
*/
public static function decrypt(string $cipher, string $key, ?string $hmacSalt = null): ?string
{
self::_checkKey($key, 'decrypt()');
if (!$cipher) {
throw new InvalidArgumentException('The data to decrypt cannot be empty.');
}
$hmacSalt ??= static::getSalt();
// Generate the encryption and hmac key.
$key = mb_substr(hash('sha256', $key . $hmacSalt), 0, 32, '8bit');
// Split out hmac for comparison
$macSize = 64;
$hmac = mb_substr($cipher, 0, $macSize, '8bit');
$cipher = mb_substr($cipher, $macSize, null, '8bit');
$compareHmac = hash_hmac('sha256', $cipher, $key);
if (!static::constantEquals($hmac, $compareHmac)) {
return null;
}
$crypto = static::engine();
return $crypto->decrypt($cipher, $key);
}
/**
* A timing attack resistant comparison that prefers native PHP implementations.
*
* @param mixed $original The original value.
* @param mixed $compare The comparison value.
* @return bool
* @since 3.6.2
*/
public static function constantEquals(mixed $original, mixed $compare): bool
{
return is_string($original) && is_string($compare) && hash_equals($original, $compare);
}
/**
* Gets the HMAC salt to be used for encryption/decryption
* routines.
*
* @return string The currently configured salt
*/
public static function getSalt(): string
{
if (static::$_salt === null) {
throw new CakeException(
'Salt not set. Use Security::setSalt() to set one, ideally in `config/bootstrap.php`.',
);
}
return static::$_salt;
}
/**
* Sets the HMAC salt to be used for encryption/decryption
* routines.
*
* @param string $salt The salt to use for encryption routines.
* @return void
*/
public static function setSalt(string $salt): void
{
static::$_salt = $salt;
}
}
File diff suppressed because it is too large Load Diff
+534
View File
@@ -0,0 +1,534 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 0.10.3
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Utility;
use BackedEnum;
use Cake\Core\Exception\CakeException;
use Cake\Utility\Exception\XmlException;
use Closure;
use DOMDocument;
use DOMElement;
use DOMNode;
use DOMText;
use Exception;
use SimpleXMLElement;
use UnitEnum;
/**
* XML handling for CakePHP.
*
* The methods in these classes enable the datasources that use XML to work.
*/
class Xml
{
/**
* Initialize SimpleXMLElement or DOMDocument from a given XML string, file path, URL or array.
*
* ### Usage:
*
* Building XML from a string:
*
* ```
* $xml = Xml::build('<example>text</example>');
* ```
*
* Building XML from string (output DOMDocument):
*
* ```
* $xml = Xml::build('<example>text</example>', ['return' => 'domdocument']);
* ```
*
* Building XML from a file path:
*
* ```
* $xml = Xml::build('/path/to/an/xml/file.xml', ['readFile' => true]);
* ```
*
* Building XML from a remote URL:
*
* ```
* use Cake\Http\Client;
*
* $http = new Client();
* $response = $http->get('http://example.com/example.xml');
* $xml = Xml::build($response->body());
* ```
*
* Building from an array:
*
* ```
* $value = [
* 'tags' => [
* 'tag' => [
* [
* 'id' => '1',
* 'name' => 'defect'
* ],
* [
* 'id' => '2',
* 'name' => 'enhancement'
* ]
* ]
* ]
* ];
* $xml = Xml::build($value);
* ```
*
* When building XML from an array ensure that there is only one top level element.
*
* ### Options
*
* - `return` Can be 'simplexml' to return object of SimpleXMLElement or 'domdocument' to return DOMDocument.
* - `loadEntities` Defaults to false. Set to true to enable loading of `<!ENTITY` definitions. This
* is disabled by default for security reasons.
* - `readFile` Set to true to enable file reading. This is disabled by default to prevent
* local filesystem access. Only enable this setting when the input is safe.
* - `parseHuge` Enable the `LIBXML_PARSEHUGE` flag.
*
* If using array as input, you can pass `options` from Xml::fromArray.
*
* @param object|array|string $input XML string, a path to a file, a URL or an array
* @param array<string, mixed> $options The options to use
* @return \SimpleXMLElement|\DOMDocument SimpleXMLElement or DOMDocument
* @throws \Cake\Utility\Exception\XmlException
*/
public static function build(object|array|string $input, array $options = []): SimpleXMLElement|DOMDocument
{
$defaults = [
'return' => 'simplexml',
'loadEntities' => false,
'readFile' => false,
'parseHuge' => false,
];
$options += $defaults;
if (is_array($input) || is_object($input)) {
return static::fromArray($input, $options);
}
if ($options['readFile'] && file_exists($input)) {
$content = file_get_contents($input);
if ($content === false) {
throw new CakeException(sprintf('Cannot read file content of `%s`', $input));
}
return static::_loadXml($content, $options);
}
if (str_contains($input, '<')) {
return static::_loadXml($input, $options);
}
throw new XmlException('XML cannot be read.');
}
/**
* Parse the input data and create either a SimpleXmlElement object or a DOMDocument.
*
* @param string $input The input to load.
* @param array<string, mixed> $options The options to use. See Xml::build()
* @return \SimpleXMLElement|\DOMDocument
* @throws \Cake\Utility\Exception\XmlException
*/
protected static function _loadXml(string $input, array $options): SimpleXMLElement|DOMDocument
{
return static::load(
$input,
$options,
function ($input, $options, $flags) {
if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
$flags |= LIBXML_NOCDATA;
$xml = new SimpleXMLElement($input, $flags);
} else {
$xml = new DOMDocument();
$xml->loadXML($input, $flags);
}
return $xml;
},
);
}
/**
* Parse the input html string and create either a SimpleXmlElement object or a DOMDocument.
*
* @param string $input The input html string to load.
* @param array<string, mixed> $options The options to use. See Xml::build()
* @return \SimpleXMLElement|\DOMDocument
* @throws \Cake\Utility\Exception\XmlException
*/
public static function loadHtml(string $input, array $options = []): SimpleXMLElement|DOMDocument
{
$defaults = [
'return' => 'simplexml',
'loadEntities' => false,
];
$options += $defaults;
return static::load(
$input,
$options,
function ($input, $options, $flags) {
$xml = new DOMDocument();
$xml->loadHTML($input, $flags);
if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
return simplexml_import_dom($xml);
}
return $xml;
},
);
}
/**
* Parse the input data and create either a SimpleXmlElement object or a DOMDocument.
*
* @param string $input The input to load.
* @param array<string, mixed> $options The options to use. See Xml::build()
* @param \Closure $callable Closure that should return SimpleXMLElement or DOMDocument instance.
* @return \SimpleXMLElement|\DOMDocument
* @throws \Cake\Utility\Exception\XmlException
*/
protected static function load(string $input, array $options, Closure $callable): SimpleXMLElement|DOMDocument
{
$flags = 0;
if (!empty($options['parseHuge'])) {
$flags |= LIBXML_PARSEHUGE;
}
$internalErrors = libxml_use_internal_errors(true);
if ($options['loadEntities']) {
$flags |= LIBXML_NOENT;
}
try {
return $callable($input, $options, $flags);
} catch (Exception $e) {
throw new XmlException('Xml cannot be read. ' . $e->getMessage(), null, $e);
} finally {
libxml_use_internal_errors($internalErrors);
}
}
/**
* Transform an array into a SimpleXMLElement
*
* ### Options
*
* - `format` If create children ('tags') or attributes ('attributes').
* - `pretty` Returns formatted Xml when set to `true`. Defaults to `false`
* - `version` Version of XML document. Default is 1.0.
* - `encoding` Encoding of XML document. If null remove from XML header.
* Defaults to the application's encoding
* - `return` If return object of SimpleXMLElement ('simplexml')
* or DOMDocument ('domdocument'). Default is SimpleXMLElement.
*
* Using the following data:
*
* ```
* $value = [
* 'root' => [
* 'tag' => [
* 'id' => 1,
* 'value' => 'defect',
* '@' => 'description'
* ]
* ]
* ];
* ```
*
* Calling `Xml::fromArray($value, 'tags');` Will generate:
*
* `<root><tag><id>1</id><value>defect</value>description</tag></root>`
*
* And calling `Xml::fromArray($value, 'attributes');` Will generate:
*
* `<root><tag id="1" value="defect">description</tag></root>`
*
* @param object|array $input Array with data or a collection instance.
* @param array<string, mixed> $options The options to use.
* @return \SimpleXMLElement|\DOMDocument SimpleXMLElement or DOMDocument
* @throws \Cake\Utility\Exception\XmlException
*/
public static function fromArray(object|array $input, array $options = []): SimpleXMLElement|DOMDocument
{
// @phpstan-ignore function.alreadyNarrowedType (is_callable check for visibility)
if (is_object($input) && method_exists($input, 'toArray') && is_callable([$input, 'toArray'])) {
$input = $input->toArray();
}
if (!is_array($input) || count($input) !== 1) {
throw new XmlException(
'Invalid input of type `' . gettype($input) . '`'
. (is_array($input) ? ' (Count of ' . count($input) . ')' : '') . '.',
);
}
$key = key($input);
if (is_int($key)) {
throw new XmlException('The key of input must be alphanumeric');
}
$defaults = [
'format' => 'tags',
'version' => '1.0',
'encoding' => mb_internal_encoding(),
'return' => 'simplexml',
'pretty' => false,
];
$options += $defaults;
$dom = new DOMDocument($options['version'], $options['encoding']);
if ($options['pretty']) {
$dom->formatOutput = true;
}
self::_fromArray($dom, $dom, $input, $options['format']);
$options['return'] = strtolower($options['return']);
if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
$xmlString = (string)$dom->saveXML();
$check = new DOMDocument();
libxml_use_internal_errors(true);
if (!$check->loadXML($xmlString, LIBXML_NOWARNING | LIBXML_NOERROR)) {
$errors = libxml_get_errors();
$messages = [];
foreach ($errors as $error) {
$messages[] = trim(sprintf(
'File: %s, Line %d, Column %d: %s',
$error->file ?: '[string input]',
$error->line,
$error->column,
$error->message,
));
}
libxml_clear_errors();
throw new XmlException("Invalid XML string:\n" . implode("\n", $messages));
}
return new SimpleXMLElement($xmlString);
}
return $dom;
}
/**
* Recursive method to create children from array
*
* @param \DOMDocument $dom Handler to DOMDocument
* @param \DOMDocument|\DOMElement $node Handler to DOMElement (child)
* @param mixed $data Array of data to append to the $node.
* @param string $format Either 'attributes' or 'tags'. This determines where nested keys go.
* @return void
* @throws \Cake\Utility\Exception\XmlException
*/
protected static function _fromArray(
DOMDocument $dom,
DOMDocument|DOMElement $node,
mixed $data,
string $format,
): void {
if (!$data || !is_array($data)) {
return;
}
foreach ($data as $key => $value) {
if (is_string($key)) {
// @phpstan-ignore function.alreadyNarrowedType (is_callable check for visibility)
if (is_object($value) && method_exists($value, 'toArray') && is_callable([$value, 'toArray'])) {
$value = $value->toArray();
}
if (!is_array($value)) {
if (is_bool($value)) {
$value = (int)$value;
} elseif ($value === null) {
$value = '';
}
if (str_contains($key, 'xmlns:')) {
assert($node instanceof DOMElement);
$node->setAttributeNS('http://www.w3.org/2000/xmlns/', $key, (string)$value);
continue;
}
if (!str_starts_with($key, '@') && $format === 'tags') {
if (!is_numeric($value)) {
// Escape special characters
// https://www.w3.org/TR/REC-xml/#syntax
// https://bugs.php.net/bug.php?id=36795
$child = $dom->createElement($key, '');
if ($value instanceof BackedEnum) {
$value = (string)$value->value;
} elseif ($value instanceof UnitEnum) {
$value = $value->name;
} else {
$value = (string)$value;
}
$child->appendChild(new DOMText($value));
} else {
$child = $dom->createElement($key, (string)$value);
}
$node->appendChild($child);
} else {
if (str_starts_with($key, '@')) {
$key = substr($key, 1);
}
$attribute = $dom->createAttribute($key);
$attribute->appendChild($dom->createTextNode((string)$value));
$node->appendChild($attribute);
}
} else {
if (str_starts_with($key, '@')) {
throw new XmlException('Invalid array');
}
if (is_numeric(implode('', array_keys($value)))) {
// List
foreach ($value as $item) {
$itemData = compact('dom', 'node', 'key', 'format');
$itemData['value'] = $item;
static::_createChild($itemData);
}
} else {
// Struct
static::_createChild(compact('dom', 'node', 'key', 'value', 'format'));
}
}
} else {
throw new XmlException('Invalid array');
}
}
}
/**
* Helper to _fromArray(). It will create children of arrays
*
* @param array<string, mixed> $data Array with information to create children
* @return void
* @phpstan-param array{dom: \DOMDocument, node: \DOMNode, key: string, format: string, value?: mixed} $data
*/
protected static function _createChild(array $data): void
{
$data += [
'value' => null,
];
$key = $data['key'];
$format = $data['format'];
$value = $data['value'];
$dom = $data['dom'];
$node = $data['node'];
$childNS = null;
$childValue = null;
// @phpstan-ignore function.alreadyNarrowedType (is_callable check for visibility)
if (is_object($value) && method_exists($value, 'toArray') && is_callable([$value, 'toArray'])) {
$value = $value->toArray();
}
if (is_array($value)) {
if (isset($value['@'])) {
$childValue = (string)$value['@'];
unset($value['@']);
}
if (isset($value['xmlns:'])) {
$childNS = $value['xmlns:'];
unset($value['xmlns:']);
}
} elseif ($value || $value === 0 || $value === '0') {
$childValue = (string)$value;
}
$child = $dom->createElement($key);
if ($childValue !== null) {
$child->appendChild($dom->createTextNode($childValue));
}
if ($childNS) {
$child->setAttribute('xmlns', $childNS);
}
static::_fromArray($dom, $child, $value, $format);
$node->appendChild($child);
}
/**
* Returns this XML structure as an array.
*
* @param \SimpleXMLElement|\DOMNode $obj SimpleXMLElement, DOMNode instance
* @return array Array representation of the XML structure.
* @throws \Cake\Utility\Exception\XmlException
*/
public static function toArray(SimpleXMLElement|DOMNode $obj): array
{
if ($obj instanceof DOMNode) {
$obj = simplexml_import_dom($obj);
}
if ($obj === null) {
throw new XmlException('Failed converting DOMNode to SimpleXMLElement');
}
$result = [];
$namespaces = array_merge(['' => ''], $obj->getNamespaces(true));
static::_toArray($obj, $result, '', array_keys($namespaces));
return $result;
}
/**
* Recursive method to toArray
*
* @param \SimpleXMLElement $xml SimpleXMLElement object
* @param array<string, mixed> $parentData Parent array with data
* @param string $ns Namespace of current child
* @param array<string> $namespaces List of namespaces in XML
* @return void
*/
protected static function _toArray(SimpleXMLElement $xml, array &$parentData, string $ns, array $namespaces): void
{
$data = [];
foreach ($namespaces as $namespace) {
$attributes = $xml->attributes($namespace, true);
foreach ($attributes as $key => $value) {
if ($namespace) {
$key = $namespace . ':' . $key;
}
$data['@' . $key] = (string)$value;
}
foreach ($xml->children($namespace, true) as $child) {
static::_toArray($child, $data, $namespace, $namespaces);
}
}
$asString = trim((string)$xml);
if (!$data) {
$data = $asString;
} elseif ($asString !== '') {
$data['@'] = $asString;
}
if ($ns) {
$ns .= ':';
}
$name = $ns . $xml->getName();
if (isset($parentData[$name])) {
if (!is_array($parentData[$name]) || !isset($parentData[$name][0])) {
$parentData[$name] = [$parentData[$name]];
}
$parentData[$name][] = $data;
} else {
$parentData[$name] = $data;
}
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
use Cake\Utility\Inflector;
// Store the initial state
Inflector::reset();
+50
View File
@@ -0,0 +1,50 @@
{
"name": "cakephp/utility",
"description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
"type": "library",
"keywords": [
"cakephp",
"utility",
"inflector",
"string",
"hash",
"security"
],
"homepage": "https://cakephp.org",
"license": "MIT",
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/utility/graphs/contributors"
}
],
"support": {
"issues": "https://github.com/cakephp/cakephp/issues",
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"source": "https://github.com/cakephp/utility"
},
"require": {
"php": ">=8.2",
"cakephp/core": "^5.3.0"
},
"autoload": {
"psr-4": {
"Cake\\Utility\\": "."
},
"files": [
"bootstrap.php"
]
},
"suggest": {
"ext-intl": "To use Text::transliterate() or Text::slug()",
"lib-ICU": "To use Text::transliterate() or Text::slug()"
},
"minimum-stability": "dev",
"prefer-stable": true,
"extra": {
"branch-alias": {
"dev-5.next": "5.3.x-dev"
}
}
}