init
This commit is contained in:
+186
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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, '://');
|
||||
}
|
||||
}
|
||||
+1284
File diff suppressed because it is too large
Load Diff
+524
@@ -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
@@ -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.
|
||||
@@ -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
@@ -0,0 +1,91 @@
|
||||
[](https://packagist.org/packages/cakephp/utility)
|
||||
[](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
@@ -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;
|
||||
}
|
||||
}
|
||||
+1184
File diff suppressed because it is too large
Load Diff
+534
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user